Completed
Push — master ( 27f6d9...d341e0 )
by Michael
03:41
created

VoteHandler::firstStep()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 0
cts 4
cp 0
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 0
crap 6
1
<?php
2
3
namespace Michaelc\Voting\STV;
4
5
class VoteHandler
6
{
7
    /**
8
     * Election object
9
     *
10
     * @var \Michaelc\Voting\STV\Election;
11
     */
12
    protected $election;
13
14
    /**
15
     * Array of all ballots in election
16
     *
17
     * @var \MichaelC\Voting\STV\Ballot[]
18
     */
19
    protected $ballots;
20
21
    /**
22
     * Quota of votes needed for a candidate to be elected
23
     *
24
     * @var int
25
     */
26
    protected $quota;
27
28
    /**
29
     * Number of candidates elected so far
30
     *
31
     * @var int
32
     */
33
    protected $electedCandidates;
34
35
    /**
36
     * Invalid ballots
37
     *
38
     * @var \MichaelC\Voting\STV\Ballot[]
39
     */
40
    protected $rejectedBallots;
41
42
    /**
43
     * Constructor
44
     *
45
     * @param Election $election
46
     */
47 1
    public function __construct(Election $election)
48
    {
49 1
        $this->election = $election;
50 1
        $this->ballots = $this->election->getBallots();
51 1
        $this->quota = $this->getQuota();
52 1
        $this->rejectedBallots = [];
53 1
    }
54
55
    /**
56
     * Run the election
57
     *
58
     * @return \MichaelC\Voting\STV\Candidate[]	Winning candidates
59
     */
60
    public function run()
61
    {
62
        $this->rejectInvalidBallots();
63
64
    	$this->firstStep();
65
66
        $candidates = $this->election->getActiveCandidates();
67
68
        while ($this->electedCandidates < $this->election->getWinnersCount())
69
        {
70
	    	if (!$this->checkCandidates($candidates))
71
	    	{
72
	    		$this->eliminateCandidates($candidates);
73
	    	}
74
        }
75
76
        return $this->election->getElectedCandidates();
77
    }
78
79
    /**
80
     * Perform the initial vote allocation
81
     *
82
     * @return
83
     */
84
    protected function firstStep()
85
    {
86
        foreach ($this->ballots as $i => $ballot)
87
        {
88
            $this->allocateVotes($ballot);
89
        }
90
91
        return;
92
    }
93
94
    /**
95
     * Check if any candidates have reached the quota and can be elected
96
     *
97
     * @param  array  $candidates 	Array of active candidates to check
98
     * @return bool 				Whether any candidates were changed to elected
99
     */
100
    protected function checkCandidates(array &$candidates): bool
101
    {
102
    	foreach ($candidates as $i => $candidate)
103
        {
104
            if ($candidate->getVotes() >= $this->quota)
105
            {
106
                $this->electCandidate($candidate);
107
                $elected = true;
108
            }
109
        }
110
111
        return ($elected ?? false);
0 ignored issues
show
Bug introduced by
The variable $elected does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
112
    }
113
114
    /**
115
     * Allocate the next votes from a Ballot
116
     *
117
     * @param Ballot $ballot 		The ballot to allocate the votes from
118
     * @param float  $multiplier 	Number to multiply the weight by (surplus)
119
     * @param float  $divisor 		The divisor of the weight (Total number of
120
     *                          	candidate votes)
121
     * @return Ballot 	The same ballot passed in modified
122
     */
123
    protected function allocateVotes(Ballot &$ballot, float $multiplier = 1.0, float $divisor = 1.0): Ballot
124
    {
125
        $weight = $ballot->setWeight(($ballot->getWeight() * $multiplier) / $divisor);
126
        $candidate = $ballot->getNextChoice();
127
128
        if ($candidate !== null)
129
        {
130
	        $this->election->getCandidate($candidate)->addVotes($weight);
131
	        $ballot->incrementLevelUsed();
132
        }
133
134
        return $ballot;
135
    }
136
137
    /**
138
     * Transfer the votes from one candidate to other candidates
139
     *
140
     * @param  float     $surplus   The number of surplus votes to transfer
141
     * @param  Candidate $candidate The candidate being elected to transfer
142
     *                              the votes from
143
     * @return
144
     */
145
    protected function transferSurplusVotes(float $surplus, Candidate $candidate)
146
    {
147
    	$totalVotes = $candiate->getVotes();
0 ignored issues
show
Bug introduced by
The variable $candiate does not exist. Did you mean $candidate?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
148
    	$candidateId = $candidate->getId();
149
150
    	foreach ($this->ballots as $i => $ballot)
151
    	{
152
        	if ($ballot->getLastChoice()->getId() == $candidateId)
0 ignored issues
show
Bug introduced by
The method getId cannot be called on $ballot->getLastChoice() (of type null|integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
153
        	{
154
		        $this->allocateVotes($ballot, $surplus, $totalVotes);
155
        	}
156
    	}
157
158
        return;
159
    }
160
161
    /**
162
     * Transfer the votes from one eliminated candidate to other candidates
163
     *
164
     * @param  Candidate $candidate  Candidate being eliminated to transfer
165
     *                               the votes from
166
     * @return
167
     */
168
    protected function transferEliminatedVotes(Candidate $candidate)
169
    {
170
    	$candidateId = $candidate->getId();
171
172
    	foreach ($this->ballots as $i => $ballot)
173
        {
174
        	if ($ballot->getLastChoice()->getId() == $candidateId)
0 ignored issues
show
Bug introduced by
The method getId cannot be called on $ballot->getLastChoice() (of type null|integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
175
        	{
176
		        $this->allocateVotes($ballot);
177
        	}
178
        }
179
180
        return;
181
    }
182
183
    /**
184
     * Elect a candidate after they've passed the threshold
185
     *
186
     * @param  \Michaelc\Voting\STV\Candidate $candidate
187
     * @return null
188
     */
189
    protected function electCandidate(Candidate $candidate)
190
    {
191
        if ($candidate->getVotes() < $this->quota)
192
        {
193
            throw new Exception("We shouldn't be electing someone who hasn't met the quota");
194
        }
195
196
        $candidate->setState(Candidate::ELECTED);
197
        $this->electedCandidates++;
198
199
        if ($this->electedCandidates < $this->election->getWinnersCount())
200
        {
201
        	$surplus = $candidate->getVotes() - $this->quota;
202
        	$this->transferVotes($surplus, $candidate);
0 ignored issues
show
Bug introduced by
The method transferVotes() does not seem to exist on object<Michaelc\Voting\STV\VoteHandler>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
203
        }
204
205
        return;
206
    }
207
208
    /**
209
     * Eliminate the candidate(s) with the lowest number of votes
210
     * and reallocated their votes
211
     *
212
     * @param  array  $candidates 	Array of active candidates
213
     * @return int 					Number of candidates eliminated
214
     */
215
    protected function eliminateCandidates(array &$candidates): int
216
    {
217
    	$minimum = 0;
218
219
    	foreach ($candidates as $i => $candidate)
220
        {
221
            if ($candidate->getVotes() > $minimum)
222
            {
223
                $minimum = $candidate->getVotes();
224
                unset($minimumCandidates);
225
                $minimumCandidates[] = $candidate;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$minimumCandidates was never initialized. Although not strictly required by PHP, it is generally a good practice to add $minimumCandidates = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
226
            }
227
            elseif ($candidate->getVotes() == $minimum)
228
            {
229
                $minimumCandidates[] = $candidate;
0 ignored issues
show
Bug introduced by
The variable $minimumCandidates does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
230
            }
231
        }
232
233
        foreach($minimumCandidates as $minimumCandidate)
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOREACH keyword; 0 found
Loading history...
234
        {
235
        	$this->transferEliminatedVotes($minimumCandidate);
236
        	$minimumCandidate->setState(Candidate::DEFEATED);
237
        }
238
239
        return count($minimumCandidates);
240
    }
241
242
    /**
243
     * Reject any invalid ballots
244
     *
245
     * @return int    Number of rejected ballots
246
     */
247
    protected function rejectInvalidBallots(): int
248
    {
249
        $rejected = false;
250
251
        foreach ($this->ballots as $i => $ballot)
252
        {
253
            if (count($ballot->getRanking()) > $this->election->getCandidateCount())
254
            {
255
                $rejected = true;
256
            }
257
            else
1 ignored issue
show
Coding Style introduced by
Expected 1 space after ELSE keyword; newline found
Loading history...
258
            {
259
                $candidateIds = $this->election->getCandidateIds();
260
261
                foreach ($ballot->getRanking() as $i => $candidate)
262
                {
263
                    $rejected = (in_array($candidate, $candidateIds)) ? $rejected : true;
264
                }
265
            }
266
267
            if ($rejected)
268
            {
269
                $this->rejectedBallots[] = clone $ballot;
270
                unset($this->ballots[$i]);
271
            }
272
        }
273
274
        return count($this->rejectedBallots);
275
    }
276
277
    /**
278
     * Get the quota to win
279
     *
280
     * @return int
281
     */
282 1
    public function getQuota(): int
283
    {
284 1
        return floor(
285 1
            ($this->election->getNumBallots() /
286 1
                ($this->election->getWinnersCount() + 1)
287
            )
288 1
            + 1);
289
    }
290
}
291