Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
92.97% |
119 / 128 |
|
33.33% |
1 / 3 |
CRAP | |
0.00% |
0 / 1 |
WikitextFormatter | |
92.97% |
119 / 128 |
|
33.33% |
1 / 3 |
23.18 | |
0.00% |
0 / 1 |
formatPreamble | |
97.22% |
35 / 36 |
|
0.00% |
0 / 1 |
5 | |||
formatRoundsPreamble | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
formatRound | |
90.36% |
75 / 83 |
|
0.00% |
0 / 1 |
17.26 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\SecurePoll\Talliers\STVFormatter; |
4 | |
5 | class WikitextFormatter extends HtmlFormatter { |
6 | |
7 | public function formatPreamble( array $elected, array $eliminated ) { |
8 | // Generate overview of elected candidates |
9 | $electionSummary = "==" . |
10 | wfMessage( 'securepoll-stv-result-election-elected-header' ) . "==\n"; |
11 | |
12 | $totalVotes = array_reduce( $this->rankedVotes, static function ( $count, $ballot ) { |
13 | return $count + $ballot['count']; |
14 | }, 0 ); |
15 | $electionSummary .= wfMessage( 'securepoll-stv-election-paramters' ) |
16 | ->numParams( |
17 | $this->seats, |
18 | count( $this->candidates ), |
19 | $totalVotes |
20 | ); |
21 | |
22 | $electedList = ""; |
23 | for ( $i = 0; $i < $this->seats; $i++ ) { |
24 | $electedList .= "\n" . "* "; |
25 | if ( isset( $elected[$i] ) ) { |
26 | $currentCandidate = $elected[$i]; |
27 | $electedList .= $this->getCandidateName( $currentCandidate ); |
28 | } else { |
29 | $electedList .= "''" . |
30 | wfMessage( 'securepoll-stv-result-elected-list-unfilled-seat', |
31 | implode( |
32 | ', ', |
33 | array_map( [ $this, 'getCandidateName' ], $eliminated ) |
34 | ) |
35 | ) . "''"; |
36 | } |
37 | } |
38 | $electionSummary .= $electedList; |
39 | |
40 | // Generate overview of eliminated candidates |
41 | $electionSummary .= "\n" . "==" . wfMessage( 'securepoll-stv-result-election-eliminated-header' ) . "=="; |
42 | $eliminatedList = ""; |
43 | foreach ( $eliminated as $eliminatedCandidate ) { |
44 | $eliminatedList .= "\n" . "* " . $this->getCandidateName( $eliminatedCandidate ); |
45 | } |
46 | |
47 | // If any candidates weren't eliminated from the rounds (as a result of seats filling) |
48 | // Output them to the eliminated list after all of the round-eliminated candidates |
49 | foreach ( |
50 | array_diff( |
51 | array_keys( $this->candidates ), |
52 | array_merge( $elected, $eliminated ) |
53 | ) as $remainingCandidate ) { |
54 | $eliminatedList .= "\n" . "* " . $this->getCandidateName( $remainingCandidate ); |
55 | } |
56 | |
57 | $electionSummary .= $eliminatedList; |
58 | return $electionSummary; |
59 | } |
60 | |
61 | public function formatRoundsPreamble(): string { |
62 | $electionRounds = "\n" . "==" . wfMessage( 'securepoll-stv-result-election-rounds-header' ) . "==" . "\n"; |
63 | |
64 | // Generate rounds table |
65 | $table = '{| class="wikitable"' . "\n"; |
66 | // thead |
67 | $table .= '!' . |
68 | wfMessage( 'securepoll-stv-result-election-round-number-table-heading' ) |
69 | . "\n" . "!" . |
70 | wfMessage( 'securepoll-stv-result-election-tally-table-heading' ) |
71 | . "\n" . "!" . |
72 | wfMessage( 'securepoll-stv-result-election-result-table-heading' ); |
73 | return $electionRounds . $table; |
74 | } |
75 | |
76 | public function formatRound(): string { |
77 | // tbody |
78 | $previouslyElected = []; |
79 | $previouslyEliminated = []; |
80 | $tbody = ""; |
81 | foreach ( $this->resultsLog['rounds'] as $round ) { |
82 | $tr = "\n" . "|-"; |
83 | // Round number |
84 | $tr .= "\n" . "|" . $round['round']; |
85 | |
86 | // Sort rankings before listing them |
87 | uksort( $round['rankings'], static function ( $aKey, $bKey ) use ( $round ) { |
88 | $a = $round['rankings'][$aKey]; |
89 | $b = $round['rankings'][$bKey]; |
90 | if ( $a['total'] === $b['total'] ) { |
91 | // ascending sort |
92 | return $aKey <=> $bKey; |
93 | } |
94 | // descending sort |
95 | return $b['total'] <=> $a['total']; |
96 | } ); |
97 | |
98 | $tally = ""; |
99 | $votesTransferred = false; |
100 | foreach ( $round['rankings'] as $currentCandidate => $rank ) { |
101 | $content = "\n" . "*"; |
102 | // Was the candidate eliminated this round? |
103 | $candidateEliminatedThisRound = in_array( $currentCandidate, $round['eliminated'] ); |
104 | if ( $candidateEliminatedThisRound ) { |
105 | $content .= "<s>"; |
106 | } |
107 | $name = $this->getCandidateName( $currentCandidate ); |
108 | $nameContent = wfMessage( 'securepoll-stv-result-candidate', $name ); |
109 | $nameContent .= ' '; |
110 | $content .= $nameContent; |
111 | $candidateState = ""; |
112 | |
113 | // Only show candidates who haven't been eliminated by this round |
114 | if ( in_array( $currentCandidate, $previouslyEliminated ) ) { |
115 | continue; |
116 | } |
117 | $roundedVotes = round( $rank['votes'], self::DISPLAY_PRECISION ); |
118 | $roundedTotal = round( $rank['total'], self::DISPLAY_PRECISION ); |
119 | |
120 | // Rounding doesn't guarantee accurate display. One value may be rounded up/down and another one |
121 | // left as-is, resulting in a discrepency of 1E-6 |
122 | // Calculating the earned votes post-rounding simulates how earned votes are calculated by |
123 | // the algorithm and ensures that our display shows accurate math |
124 | $roundedEarned = round( $roundedTotal - $roundedVotes, self::DISPLAY_PRECISION ); |
125 | $formattedVotes = $this->formatForNumParams( $roundedVotes ); |
126 | $formattedTotal = $this->formatForNumParams( $roundedTotal ); |
127 | |
128 | // We select the votes-gain/-votes-surplus message based on the sign of |
129 | // $roundedEarned. However, those messages expect its absolute value. |
130 | $formattedEarned = $this->formatForNumParams( abs( $roundedEarned ) ); |
131 | |
132 | // Round 1 should just show the initial votes and is guaranteed to neither elect nor eliminate |
133 | if ( $round['round'] === 1 ) { |
134 | $candidateState .= wfMessage( 'securepoll-stv-result-votes-no-change' ) |
135 | ->numParams( $formattedTotal ); |
136 | } elseif ( $roundedEarned > 0 ) { |
137 | $candidateState .= wfMessage( 'securepoll-stv-result-votes-gain' ) |
138 | ->numParams( |
139 | $formattedVotes, |
140 | $formattedEarned, |
141 | $formattedTotal |
142 | ); |
143 | $votesTransferred = true; |
144 | } elseif ( $roundedEarned < 0 ) { |
145 | $candidateState .= wfMessage( 'securepoll-stv-result-votes-surplus' ) |
146 | ->numParams( |
147 | $formattedVotes, |
148 | $formattedEarned, |
149 | $formattedTotal |
150 | ); |
151 | $votesTransferred = true; |
152 | } else { |
153 | $candidateState .= wfMessage( 'securepoll-stv-result-votes-no-change' ) |
154 | ->numParams( $formattedTotal ); |
155 | } |
156 | |
157 | if ( in_array( $currentCandidate, $round['elected'] ) ) { |
158 | // Mark the candidate as having been previously elected (for display purposes only). |
159 | $previouslyElected[] = $currentCandidate; |
160 | } elseif ( in_array( $currentCandidate, $previouslyElected ) ) { |
161 | $formattedParams = $this->formatForNumParams( $round['keepFactors'][$currentCandidate] ); |
162 | $candidateState .= ' ' . wfMessage( 'securepoll-stv-result-round-keep-factor' ) |
163 | ->numParams( $formattedParams ); |
164 | } elseif ( $candidateEliminatedThisRound ) { |
165 | // Mark the candidate as having been previously eliminated (for display purposes only). |
166 | $previouslyEliminated[] = $currentCandidate; |
167 | } |
168 | |
169 | $content .= $candidateState; |
170 | |
171 | if ( $candidateEliminatedThisRound ) { |
172 | $content .= "</s>"; |
173 | } |
174 | |
175 | $tally .= $content; |
176 | } |
177 | $tr .= "\n" . "|" . $tally; |
178 | |
179 | // Result |
180 | $roundResults = "\n" . "|"; |
181 | |
182 | // Quota |
183 | $roundResults .= wfMessage( 'securepoll-stv-result-round-quota' ) |
184 | ->numParams( $this->formatForNumParams( $round['quota'] ) ) . "\n"; |
185 | |
186 | // Elected |
187 | if ( count( $round['elected'] ) ) { |
188 | $candidatesElected = array_map( [ $this, 'getCandidateName' ], $round['elected'] ); |
189 | $formattedElected = implode( ', ', $candidatesElected ); |
190 | $roundResults .= wfMessage( 'securepoll-stv-result-round-elected', $formattedElected ) . "\n"; |
191 | } |
192 | |
193 | // Eliminated |
194 | if ( $round['eliminated'] !== null && count( $round['eliminated'] ) > 0 ) { |
195 | $candidatesEliminated = array_map( [ $this, 'getCandidateName' ], $round['eliminated'] ); |
196 | $formattedEliminated = implode( ', ', $candidatesEliminated ); |
197 | $roundResults .= wfMessage( 'securepoll-stv-result-round-eliminated', $formattedEliminated ); |
198 | } |
199 | |
200 | // Votes transferred |
201 | if ( $votesTransferred ) { |
202 | $roundResults .= "\n" . wfMessage( 'securepoll-stv-votes-transferred' ); |
203 | } |
204 | $tr .= $roundResults; |
205 | $tbody .= $tr; |
206 | } |
207 | return $tbody; |
208 | } |
209 | } |