Completed
Push — master ( d341e0...5734ee )
by Michael
04:30
created

VoteHandler   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 287
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 13.1%

Importance

Changes 0
Metric Value
wmc 32
lcom 1
cbo 3
dl 0
loc 287
ccs 11
cts 84
cp 0.131
rs 9.6
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A run() 0 18 3
A firstStep() 0 9 2
A checkCandidates() 0 13 3
A allocateVotes() 0 13 2
A transferSurplusVotes() 0 15 3
A transferEliminatedVotes() 0 14 3
A electCandidate() 0 18 3
B eliminateCandidates() 0 27 5
B rejectInvalidBallots() 0 29 6
A getQuota() 0 8 1
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->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
        $rejected = false;
251
252
        foreach ($this->ballots as $i => $ballot)
253
        {
254
            if (count($ballot->getRanking()) > $this->election->getCandidateCount())
255
            {
256
                $rejected = true;
257
            }
258
            else
1 ignored issue
show
Coding Style introduced by
Expected 1 space after ELSE keyword; newline found
Loading history...
259
            {
260
                $candidateIds = $this->election->getCandidateIds();
261
262
                foreach ($ballot->getRanking() as $i => $candidate)
263
                {
264
                    $rejected = (in_array($candidate, $candidateIds)) ? $rejected : true;
265
                }
266
            }
267
268
            if ($rejected)
269
            {
270
                $this->rejectedBallots[] = clone $ballot;
271
                unset($this->ballots[$i]);
272
            }
273
        }
274
275
        return count($this->rejectedBallots);
276
    }
277
278
    /**
279
     * Get the quota to win
280
     *
281
     * @return int
282
     */
283 1
    public function getQuota(): int
284
    {
285 1
        return floor(
286 1
            ($this->election->getNumBallots() /
287 1
                ($this->election->getWinnersCount() + 1)
288
            )
289 1
            + 1);
290
    }
291
}
292