Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
63.41% |
52 / 82 |
|
33.33% |
3 / 9 |
CRAP | |
0.00% |
0 / 1 |
SchulzeTallier | |
63.41% |
52 / 82 |
|
33.33% |
3 / 9 |
74.07 | |
0.00% |
0 / 1 |
getPathStrengths | |
75.00% |
24 / 32 |
|
0.00% |
0 / 1 |
15.64 | |||
convertStrengthMatrixToRanks | |
95.24% |
20 / 21 |
|
0.00% |
0 / 1 |
8 | |||
isSchulzeWin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
finishTally | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getRanks | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
loadJSONResult | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getJSONResult | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getHtmlResult | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getTextResult | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\SecurePoll\Talliers; |
4 | |
5 | /** |
6 | * This is the basic Schulze method with no tie-breaking. The algorithm in this |
7 | * class is believed to be equivalent to the method in the Debian constitution, |
8 | * assuming no quorum and no default option. It has been cross-tested against |
9 | * debian-vote (but not devotee). |
10 | */ |
11 | class SchulzeTallier extends PairwiseTallier { |
12 | /** @var array|null */ |
13 | public $ranks; |
14 | /** @var array|null */ |
15 | public $strengths; |
16 | |
17 | /** |
18 | * @param array $victories |
19 | * @return array |
20 | */ |
21 | private function getPathStrengths( $victories ) { |
22 | # This algorithm follows Markus Schulze, "A New Monotonic, Clone-Independent, Reversal |
23 | # Symmetric, and Condorcet-Consistent Single-Winner Election Method" and also |
24 | # http://en.wikipedia.org/w/index.php?title=User:MarkusSchulze/Statutory_Rules&oldid=303036893 |
25 | # Path strengths in the Schulze method are given by pairs of integers notated (a, b) |
26 | # where a is the strength in one direction and b is the strength in the other. We make |
27 | # a matrix of path strength pairs "p", giving the path strength of the row index beating |
28 | # the column index. |
29 | |
30 | # First the path strength matrix is populated with the "direct" victory count in each |
31 | # direction, i.e. the number of people who preferenced A over B, and B over A. |
32 | $strengths = []; |
33 | foreach ( $this->optionIds as $oid1 ) { |
34 | foreach ( $this->optionIds as $oid2 ) { |
35 | if ( $oid1 === $oid2 ) { |
36 | continue; |
37 | } |
38 | $v12 = $victories[$oid1][$oid2]; |
39 | $v21 = $victories[$oid2][$oid1]; |
40 | if ( $v12 > $v21 ) { |
41 | # Direct victory |
42 | $strengths[$oid1][$oid2] = [ |
43 | $v12, |
44 | $v21 |
45 | ]; |
46 | } else { |
47 | # Direct loss |
48 | $strengths[$oid1][$oid2] = [ |
49 | 0, |
50 | 0 |
51 | ]; |
52 | } |
53 | } |
54 | } |
55 | |
56 | # Next (continuing the Floyd-Warshall algorithm) we calculate the strongest indirect |
57 | # paths. This part dominates the O(N^3) time order. |
58 | foreach ( $this->optionIds as $oid1 ) { |
59 | foreach ( $this->optionIds as $oid2 ) { |
60 | if ( $oid1 === $oid2 ) { |
61 | continue; |
62 | } |
63 | foreach ( $this->optionIds as $oid3 ) { |
64 | if ( $oid1 === $oid3 || $oid2 === $oid3 ) { |
65 | continue; |
66 | } |
67 | $s21 = $strengths[$oid2][$oid1]; |
68 | $s13 = $strengths[$oid1][$oid3]; |
69 | $s23 = $strengths[$oid2][$oid3]; |
70 | if ( $this->isSchulzeWin( $s21, $s13 ) ) { |
71 | $temp = $s13; |
72 | } else { |
73 | $temp = $s21; |
74 | } |
75 | if ( $this->isSchulzeWin( $temp, $s23 ) ) { |
76 | $strengths[$oid2][$oid3] = $temp; |
77 | } |
78 | } |
79 | } |
80 | } |
81 | |
82 | return $strengths; |
83 | } |
84 | |
85 | private function convertStrengthMatrixToRanks( $strengths ) { |
86 | $unusedIds = $this->optionIds; |
87 | $ranks = []; |
88 | $currentRank = 1; |
89 | |
90 | while ( count( $unusedIds ) ) { |
91 | $winners = array_flip( $unusedIds ); |
92 | foreach ( $unusedIds as $oid1 ) { |
93 | foreach ( $unusedIds as $oid2 ) { |
94 | if ( $oid1 == $oid2 ) { |
95 | continue; |
96 | } |
97 | $s12 = $strengths[$oid1][$oid2]; |
98 | $s21 = $strengths[$oid2][$oid1]; |
99 | if ( $this->isSchulzeWin( $s21, $s12 ) ) { |
100 | # oid1 is defeated by someone, not a winner |
101 | unset( $winners[$oid1] ); |
102 | break; |
103 | } |
104 | } |
105 | } |
106 | if ( !count( $winners ) ) { |
107 | # No winners, everyone ties for this position |
108 | $winners = array_flip( $unusedIds ); |
109 | } |
110 | |
111 | # Now $winners is the list of candidates that tie for this position |
112 | foreach ( $winners as $oid => $unused ) { |
113 | $ranks[$oid] = $currentRank; |
114 | } |
115 | $currentRank += count( $winners ); |
116 | $unusedIds = array_diff( $unusedIds, array_keys( $winners ) ); |
117 | } |
118 | |
119 | return $ranks; |
120 | } |
121 | |
122 | /** |
123 | * Determine whether Schulze's win relation "s1 >win s2" for path strength |
124 | * pairs s1 and s2 is satisfied. |
125 | * |
126 | * When applied to final path strengths instead of intermediate results, |
127 | * the paper notates this relation >D (greater than subscript D). |
128 | * |
129 | * The inequality in the second part is reversed because the first part |
130 | * refers to wins, and the second part refers to losses. |
131 | * @param array $s1 |
132 | * @param array $s2 |
133 | * @return bool |
134 | */ |
135 | private function isSchulzeWin( $s1, $s2 ) { |
136 | return $s1[0] > $s2[0] || ( $s1[0] == $s2[0] && $s1[1] < $s2[1] ); |
137 | } |
138 | |
139 | /** |
140 | * @inheritDoc |
141 | * |
142 | */ |
143 | public function finishTally() { |
144 | $this->strengths = $this->getPathStrengths( $this->victories ); |
145 | $this->ranks = $this->convertStrengthMatrixToRanks( $this->strengths ); |
146 | } |
147 | |
148 | public function getRanks() { |
149 | return $this->ranks; |
150 | } |
151 | |
152 | /** |
153 | * @inheritDoc |
154 | * |
155 | */ |
156 | public function loadJSONResult( $data ) { |
157 | $this->ranks = $data['ranks']; |
158 | $this->victories = $data['victories']; |
159 | $this->strengths = $data['strengths']; |
160 | } |
161 | |
162 | /** |
163 | * @inheritDoc |
164 | * |
165 | */ |
166 | public function getJSONResult() { |
167 | return [ |
168 | 'ranks' => $this->ranks, |
169 | 'victories' => $this->victories, |
170 | 'strengths' => $this->strengths, |
171 | ]; |
172 | } |
173 | |
174 | /** |
175 | * @inheritDoc |
176 | * |
177 | */ |
178 | public function getHtmlResult() { |
179 | $s = '<h2>' . wfMessage( 'securepoll-ranks' )->parse() . "</h2>\n"; |
180 | $s .= $this->convertRanksToHtml( $this->ranks ); |
181 | |
182 | $s .= '<h2>' . wfMessage( 'securepoll-pairwise-victories' )->parse() . "</h2>\n"; |
183 | $rankedIds = array_keys( $this->ranks ); |
184 | $s .= $this->convertMatrixToHtml( $this->victories, $rankedIds ); |
185 | |
186 | $s .= '<h2>' . wfMessage( 'securepoll-strength-matrix' )->parse() . "</h2>\n"; |
187 | $s .= $this->convertMatrixToHtml( $this->strengths, $rankedIds ); |
188 | |
189 | return $s; |
190 | } |
191 | |
192 | /** |
193 | * @inheritDoc |
194 | * |
195 | */ |
196 | public function getTextResult() { |
197 | $rankedIds = array_keys( $this->ranks ); |
198 | |
199 | return wfMessage( 'securepoll-ranks' )->text() . "\n" . $this->convertRanksToText( |
200 | $this->ranks |
201 | ) . "\n\n" . wfMessage( 'securepoll-pairwise-victories' )->text( |
202 | ) . "\n" . $this->convertMatrixToText( |
203 | $this->victories, |
204 | $rankedIds |
205 | ) . "\n\n" . wfMessage( 'securepoll-strength-matrix' )->text( |
206 | ) . "\n" . $this->convertMatrixToText( $this->strengths, $rankedIds ); |
207 | } |
208 | } |