Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
63.41% covered (warning)
63.41%
52 / 82
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
SchulzeTallier
63.41% covered (warning)
63.41%
52 / 82
33.33% covered (danger)
33.33%
3 / 9
74.07
0.00% covered (danger)
0.00%
0 / 1
 getPathStrengths
75.00% covered (warning)
75.00%
24 / 32
0.00% covered (danger)
0.00%
0 / 1
15.64
 convertStrengthMatrixToRanks
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
8
 isSchulzeWin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 finishTally
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRanks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loadJSONResult
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getJSONResult
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getHtmlResult
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getTextResult
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace 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 */
11class 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}