Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.97% covered (success)
92.97%
119 / 128
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikitextFormatter
92.97% covered (success)
92.97%
119 / 128
33.33% covered (danger)
33.33%
1 / 3
23.18
0.00% covered (danger)
0.00%
0 / 1
 formatPreamble
97.22% covered (success)
97.22%
35 / 36
0.00% covered (danger)
0.00%
0 / 1
5
 formatRoundsPreamble
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 formatRound
90.36% covered (success)
90.36%
75 / 83
0.00% covered (danger)
0.00%
0 / 1
17.26
1<?php
2
3namespace MediaWiki\Extension\SecurePoll\Talliers\STVFormatter;
4
5class 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}