|
1
|
|
|
<?php |
|
2
|
|
|
/* |
|
3
|
|
|
Ranked Pairs part of the Condorcet PHP Class |
|
4
|
|
|
|
|
5
|
|
|
By Julien Boudry - MIT LICENSE (Please read LICENSE.txt) |
|
6
|
|
|
https://github.com/julien-boudry/Condorcet |
|
7
|
|
|
*/ |
|
8
|
|
|
declare(strict_types=1); |
|
9
|
|
|
|
|
10
|
|
|
namespace Condorcet\Algo\Methods; |
|
11
|
|
|
|
|
12
|
|
|
use Condorcet\Algo\Method; |
|
13
|
|
|
use Condorcet\Algo\MethodInterface; |
|
14
|
|
|
use Condorcet\Algo\Tools\PairwiseStats; |
|
15
|
|
|
use Condorcet\Election; |
|
16
|
|
|
use Condorcet\Result; |
|
17
|
|
|
|
|
18
|
|
|
// Ranker Pairs is a Condorcet Algorithm | http://en.wikipedia.org/wiki/Ranked_Pairs |
|
19
|
|
|
class RankedPairs_Core extends Method implements MethodInterface |
|
20
|
|
|
{ |
|
21
|
|
|
// Limits |
|
22
|
|
|
public static $_maxCandidates = 40; |
|
23
|
|
|
|
|
24
|
|
|
// Ranked Pairs |
|
25
|
|
|
protected $_PairwiseSort; |
|
26
|
|
|
protected $_Arcs; |
|
27
|
|
|
protected $_Stats; |
|
28
|
|
|
protected $_StatsDone = false; |
|
29
|
|
|
|
|
30
|
|
|
|
|
31
|
|
|
/////////// PUBLIC /////////// |
|
32
|
|
|
|
|
33
|
|
|
|
|
34
|
|
|
// Get the Ranked Pairs ranking |
|
35
|
1 |
|
public function getResult () : Result |
|
36
|
|
|
{ |
|
37
|
|
|
// Cache |
|
38
|
1 |
|
if ( $this->_Result !== null ) : |
|
39
|
1 |
|
return $this->_Result; |
|
40
|
|
|
endif; |
|
41
|
|
|
|
|
42
|
|
|
////// |
|
43
|
|
|
|
|
44
|
|
|
// Sort pairwise |
|
45
|
1 |
|
$this->_PairwiseSort = $this->pairwiseSort(); |
|
46
|
|
|
|
|
47
|
|
|
// Ranking calculation |
|
48
|
1 |
|
$this->makeArcs(); |
|
49
|
|
|
|
|
50
|
|
|
// Make Stats |
|
51
|
1 |
|
$this->_Stats['tally'] = $this->_PairwiseSort; |
|
52
|
1 |
|
$this->_Stats['arcs'] = $this->_Arcs; |
|
53
|
|
|
|
|
54
|
|
|
// Make Result |
|
55
|
1 |
|
return $this->_Result = $this->createResult($this->makeResult()); |
|
56
|
|
|
} |
|
57
|
|
|
|
|
58
|
|
|
// Get the Ranked Pair ranking |
|
59
|
1 |
|
protected function getStats () : array |
|
60
|
|
|
{ |
|
61
|
1 |
|
if (!$this->_StatsDone) : |
|
62
|
1 |
View Code Duplication |
foreach ($this->_Stats['tally'] as &$Roundvalue) : |
|
|
|
|
|
|
63
|
1 |
|
foreach ($Roundvalue as $ArcKey => &$Arcvalue) : |
|
64
|
1 |
|
foreach ($Arcvalue as $key => &$value) : |
|
65
|
1 |
|
if ($key === 'from' || $key === 'to') : |
|
66
|
1 |
|
$value = $this->_selfElection->getCandidateId($value, true); |
|
67
|
|
|
endif; |
|
68
|
|
|
endforeach; |
|
69
|
|
|
endforeach; |
|
70
|
|
|
endforeach; |
|
71
|
|
|
|
|
72
|
1 |
View Code Duplication |
foreach ($this->_Stats['arcs'] as $ArcKey => &$Arcvalue) : |
|
|
|
|
|
|
73
|
1 |
|
foreach ($Arcvalue as $key => &$value) : |
|
74
|
1 |
|
if ($key === 'from' || $key === 'to') : |
|
75
|
1 |
|
$value = $this->_selfElection->getCandidateId($value, true); |
|
76
|
|
|
endif; |
|
77
|
|
|
endforeach; |
|
78
|
|
|
endforeach; |
|
79
|
|
|
|
|
80
|
1 |
|
$this->_StatsDone = true; |
|
81
|
|
|
endif; |
|
82
|
|
|
|
|
83
|
1 |
|
return $this->_Stats; |
|
84
|
|
|
} |
|
85
|
|
|
|
|
86
|
|
|
|
|
87
|
|
|
/////////// COMPUTE /////////// |
|
88
|
|
|
|
|
89
|
|
|
|
|
90
|
|
|
//:: RANKED PAIRS ALGORITHM. ::// |
|
91
|
|
|
|
|
92
|
1 |
|
protected function makeResult () : array |
|
93
|
|
|
{ |
|
94
|
1 |
|
$result = []; |
|
95
|
1 |
|
$alreadyDone = []; |
|
96
|
1 |
|
$lastWinner = null; |
|
|
|
|
|
|
97
|
|
|
|
|
98
|
1 |
|
$rang = 1; |
|
99
|
1 |
|
while (count($alreadyDone) < $this->_selfElection->countCandidates()) : |
|
100
|
1 |
|
$winners = $this->getWinners($alreadyDone); |
|
101
|
|
|
|
|
102
|
1 |
|
foreach ($this->_Arcs as $ArcKey => $Arcvalue) : |
|
103
|
1 |
|
foreach ($winners as $oneWinner) : |
|
104
|
1 |
|
if ($Arcvalue['from'] === $oneWinner || $Arcvalue['to'] === $oneWinner ) : |
|
105
|
1 |
|
unset($this->_Arcs[$ArcKey]); |
|
106
|
|
|
endif; |
|
107
|
|
|
endforeach; |
|
108
|
|
|
endforeach; |
|
109
|
|
|
|
|
110
|
1 |
|
$result[$rang++] = $winners; |
|
111
|
1 |
|
$alreadyDone = array_merge($alreadyDone,$winners); |
|
112
|
|
|
endwhile; |
|
113
|
|
|
|
|
114
|
1 |
|
return $result; |
|
115
|
|
|
} |
|
116
|
|
|
|
|
117
|
1 |
|
protected function getWinners (array $alreadyDone) : array |
|
118
|
|
|
{ |
|
119
|
1 |
|
$winners = []; |
|
120
|
|
|
|
|
121
|
1 |
|
foreach ($this->_selfElection->getCandidatesList() as $candidateKey => $candidateId) : |
|
122
|
1 |
|
if (!in_array($candidateKey, $alreadyDone, true)) : |
|
123
|
1 |
|
$win = true; |
|
124
|
1 |
|
foreach ($this->_Arcs as $ArcKey => $ArcValue) : |
|
125
|
1 |
|
if ($ArcValue['to'] === $candidateKey) : |
|
126
|
1 |
|
$win = false; |
|
127
|
|
|
endif; |
|
128
|
|
|
endforeach; |
|
129
|
|
|
|
|
130
|
1 |
|
if ($win) : |
|
131
|
1 |
|
$winners[] = $candidateKey; |
|
132
|
|
|
endif; |
|
133
|
|
|
endif; |
|
134
|
|
|
endforeach; |
|
135
|
|
|
|
|
136
|
1 |
|
return $winners; |
|
137
|
|
|
} |
|
138
|
|
|
|
|
139
|
|
|
|
|
140
|
1 |
|
protected function makeArcs () : void |
|
141
|
|
|
{ |
|
142
|
1 |
|
$this->_Arcs = []; |
|
143
|
|
|
|
|
144
|
1 |
|
foreach ($this->_PairwiseSort as $newArcsRound) : |
|
145
|
1 |
|
$virtualArcs = $this->_Arcs; |
|
146
|
1 |
|
$testNewsArcs = []; |
|
147
|
1 |
|
$candidatesToCheck = []; |
|
|
|
|
|
|
148
|
|
|
|
|
149
|
1 |
|
$newKey = max((empty($highKey = array_keys($virtualArcs)) ? [-1] : $highKey)) + 1; |
|
150
|
1 |
|
foreach ($newArcsRound as $newArc) : |
|
151
|
1 |
|
$virtualArcs[$newKey] = [ 'from' => $newArc['from'], 'to' => $newArc['to'] ]; |
|
152
|
1 |
|
$testNewsArcs[$newKey] = $virtualArcs[$newKey]; |
|
153
|
1 |
|
$newKey++; |
|
154
|
|
|
endforeach; |
|
155
|
|
|
|
|
156
|
1 |
|
foreach ($this->getArcsInCycle($virtualArcs) as $cycleArcKey) : |
|
157
|
1 |
|
if (array_key_exists($cycleArcKey, $testNewsArcs)) : |
|
158
|
1 |
|
unset($testNewsArcs[$cycleArcKey]); |
|
159
|
|
|
endif; |
|
160
|
|
|
endforeach; |
|
161
|
|
|
|
|
162
|
1 |
|
foreach ($testNewsArcs as $newArc) : |
|
163
|
1 |
|
$this->_Arcs[] = $newArc; |
|
164
|
|
|
endforeach; |
|
165
|
|
|
|
|
166
|
|
|
endforeach; |
|
167
|
1 |
|
} |
|
168
|
|
|
|
|
169
|
1 |
|
protected function getArcsInCycle (array $virtualArcs) : array |
|
170
|
|
|
{ |
|
171
|
1 |
|
$cycles = []; |
|
172
|
|
|
|
|
173
|
1 |
|
foreach ($this->_selfElection->getCandidatesList() as $candidateKey => $candidateId) : |
|
174
|
1 |
|
$cycles = array_merge($cycles,$this->followCycle($virtualArcs,$candidateKey,$candidateKey)); |
|
175
|
|
|
endforeach; |
|
176
|
|
|
|
|
177
|
1 |
|
return $cycles; |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
1 |
|
protected function followCycle (array $virtualArcs, int $startCandidateKey, int $searchCandidateKey, array &$done = []) |
|
181
|
|
|
{ |
|
182
|
1 |
|
$arcsInCycle = []; |
|
183
|
|
|
|
|
184
|
1 |
|
foreach ($virtualArcs as $ArcKey => $ArcValue) : |
|
185
|
1 |
|
if ($ArcValue['from'] === $startCandidateKey) : |
|
186
|
1 |
|
if (in_array($ArcKey, $done, true)) : |
|
187
|
1 |
|
continue; |
|
188
|
1 |
|
elseif ($ArcValue['to'] === $searchCandidateKey) : |
|
189
|
1 |
|
$done[] = $ArcKey; |
|
190
|
1 |
|
$arcsInCycle[] = $ArcKey; |
|
191
|
|
|
else : |
|
192
|
1 |
|
$done[] = $ArcKey; |
|
193
|
1 |
|
$arcsInCycle = array_merge($arcsInCycle,$this->followCycle($virtualArcs,$ArcValue['to'],$searchCandidateKey, $done)); |
|
194
|
|
|
endif; |
|
195
|
|
|
endif; |
|
196
|
|
|
endforeach; |
|
197
|
|
|
|
|
198
|
1 |
|
return $arcsInCycle; |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
1 |
|
protected function pairwiseSort () : array |
|
202
|
|
|
{ |
|
203
|
1 |
|
$pairs = []; |
|
204
|
|
|
|
|
205
|
1 |
|
$i = 0; |
|
206
|
1 |
|
foreach ($this->_selfElection->getPairwise(false) as $candidate_key => $candidate_value) : |
|
207
|
1 |
|
foreach ($candidate_value['win'] as $challenger_key => $challenger_value) : |
|
208
|
|
|
|
|
209
|
1 |
|
if ($challenger_value > $candidate_value['lose'][$challenger_key]) : |
|
210
|
|
|
|
|
211
|
|
|
// Victory |
|
212
|
1 |
|
$pairs[$i]['from'] = $candidate_key; |
|
213
|
|
|
// Defeat |
|
214
|
1 |
|
$pairs[$i]['to'] = $challenger_key; |
|
215
|
|
|
|
|
216
|
1 |
|
$pairs[$i]['win'] = $challenger_value; |
|
217
|
1 |
|
$pairs[$i]['minority'] = $candidate_value['lose'][$challenger_key]; |
|
218
|
1 |
|
$pairs[$i]['margin'] = $candidate_value['win'][$challenger_key] - $candidate_value['lose'][$challenger_key]; |
|
219
|
|
|
|
|
220
|
1 |
|
$i++; |
|
221
|
|
|
|
|
222
|
|
|
endif; |
|
223
|
|
|
|
|
224
|
|
|
endforeach; |
|
225
|
|
|
endforeach; |
|
226
|
|
|
|
|
227
|
1 |
|
usort($pairs, function ($a, $b) : int { |
|
228
|
1 |
|
if ($a[static::RP_VARIANT_1] < $b[static::RP_VARIANT_1]) : |
|
229
|
1 |
|
return 1; |
|
230
|
1 |
|
elseif ($a[static::RP_VARIANT_1] > $b[static::RP_VARIANT_1]) : |
|
231
|
1 |
|
return -1; |
|
232
|
|
|
else : // Equal |
|
233
|
|
|
if ($a['minority'] > $b['minority']) : |
|
234
|
|
|
return 1; |
|
235
|
|
|
elseif ($a['minority'] < $b['minority']) : |
|
236
|
|
|
return -1; |
|
237
|
|
|
else : // Equal |
|
238
|
|
|
return 0; |
|
239
|
|
|
endif; |
|
240
|
|
|
endif; |
|
241
|
1 |
|
}); |
|
242
|
|
|
|
|
243
|
1 |
|
$newArcs = []; |
|
244
|
1 |
|
$i = 0; |
|
245
|
1 |
|
$f = true; |
|
246
|
1 |
|
foreach ($pairs as $pairsKey => $pairsValue) : |
|
247
|
1 |
|
if ($f === true) : |
|
248
|
1 |
|
$newArcs[$i][] = $pairs[$pairsKey]; |
|
249
|
1 |
|
$f = false; |
|
250
|
1 |
|
elseif ($pairs[$pairsKey][static::RP_VARIANT_1] === $pairs[$pairsKey - 1][static::RP_VARIANT_1] && $pairs[$pairsKey]['minority'] === $pairs[$pairsKey - 1]['minority']) : |
|
251
|
|
|
$newArcs[$i][] = $pairs[$pairsKey]; |
|
252
|
|
|
else : |
|
253
|
1 |
|
$newArcs[++$i][] = $pairs[$pairsKey]; |
|
254
|
|
|
endif; |
|
255
|
|
|
endforeach; |
|
256
|
|
|
|
|
257
|
1 |
|
return $newArcs; |
|
258
|
|
|
} |
|
259
|
|
|
|
|
260
|
|
|
} |
|
261
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.