Completed
Push — master ( e8bc56...fa8b74 )
by Michael
03:30
created

VoteHandler::checkBallotValidity()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 0
cts 8
cp 0
rs 9.0534
c 0
b 0
f 0
cc 4
eloc 9
nc 4
nop 1
crap 20
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 = $candidate->getVotes();
148
    	$candidateId = $candidate->getId();
149
150
    	foreach ($this->ballots as $i => $ballot)
151
    	{
152
        	if ($ballot->getLastChoice() == $candidateId)
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() == $candidateId)
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->transferSurplusVotes($surplus, $candidate);
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
        $minimumCandidates = [];
219
220
        foreach ($candidates as $i => $candidate)
221
        {
222
            if ($candidate->getVotes() > $minimum)
223
            {
224
                $minimum = $candidate->getVotes();
225
                unset($minimumCandidates);
226
                $minimumCandidates[] = $candidate;
227
            }
228
            elseif ($candidate->getVotes() == $minimum)
229
            {
230
                $minimumCandidates[] = $candidate;
231
            }
232
        }
233
234
        foreach ($minimumCandidates as $minimumCandidate)
235
        {
236
            $this->transferEliminatedVotes($minimumCandidate);
237
            $minimumCandidate->setState(Candidate::DEFEATED);
238
        }
239
240
        return count($minimumCandidates);
241
    }
242
243
    /**
244
     * Reject any invalid ballots
245
     *
246
     * @return int    Number of rejected ballots
247
     */
248
    protected function rejectInvalidBallots(): int
249
    {
250
        foreach ($this->ballots as $i => $ballot)
251
        {
252
            if (!$this->checkBallotValidity($ballot))
253
            {
254
                $this->rejectedBallots[] = clone $ballot;
255
                unset($this->ballots[$i]);
256
            }
257
        }
258
259
        return count($this->rejectedBallots);
260
    }
261
262
    /**
263
     * Check if ballot is valid
264
     *
265
     * @param  Ballot &$Ballot  Ballot to test
266
     * @return bool             True if valid, false if invalid
267
     */
268
    private function checkBallotValidity(Ballot &$Ballot): bool
0 ignored issues
show
Unused Code introduced by
The parameter $Ballot is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
269
    {
270
        if (count($ballot->getRanking()) > $this->election->getCandidateCount())
0 ignored issues
show
Bug introduced by
The variable $ballot does not exist. Did you mean $Ballot?

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...
271
        {
272
            return false;
273
        }
274
        else
275
        {
276
            $candidateIds = $this->election->getCandidateIds();
277
278
            foreach ($ballot->getRanking() as $i => $candidate)
0 ignored issues
show
Bug introduced by
The variable $ballot does not exist. Did you mean $Ballot?

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...
279
            {
280
                if (!in_array($candidate, $candidateIds))
281
                {
282
                    return false;
283
                }
284
            }
285
        }
286
287
        return true;
288
    }
289
290
    /**
291
     * Get the quota to win
292
     *
293
     * @return int
294
     */
295 1
    public function getQuota(): int
296
    {
297 1
        return floor(
298 1
            ($this->election->getNumBallots() /
299 1
                ($this->election->getWinnersCount() + 1)
300
            )
301 1
            + 1);
302
    }
303
}
304