Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.51% covered (warning)
83.51%
81 / 97
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
PairwiseTallier
83.51% covered (warning)
83.51%
81 / 97
33.33% covered (danger)
33.33%
2 / 6
41.82
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 addVote
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 getOptionAbbreviations
73.68% covered (warning)
73.68%
14 / 19
0.00% covered (danger)
0.00%
0 / 1
9.17
 getRowLabels
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 convertMatrixToHtml
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
5
 convertMatrixToText
92.86% covered (success)
92.86%
26 / 28
0.00% covered (danger)
0.00%
0 / 1
9.03
1<?php
2
3namespace MediaWiki\Extension\SecurePoll\Talliers;
4
5use MediaWiki\Xml\Xml;
6
7/**
8 * Generic functionality for Condorcet-style pairwise methods.
9 * Tested via SchulzeTallier.
10 */
11abstract class PairwiseTallier extends Tallier {
12    /** @var array */
13    public $optionIds = [];
14    /** @var array */
15    public $victories = [];
16    /** @var array|null */
17    public $abbrevs;
18    /** @var array */
19    public $rowLabels = [];
20
21    /** @inheritDoc */
22    public function __construct( $context, $electionTallier, $question ) {
23        parent::__construct( $context, $electionTallier, $question );
24        $this->optionIds = [];
25        foreach ( $question->getOptions() as $option ) {
26            $this->optionIds[] = $option->getId();
27        }
28
29        $this->victories = [];
30        foreach ( $this->optionIds as $i ) {
31            foreach ( $this->optionIds as $j ) {
32                $this->victories[$i][$j] = 0;
33            }
34        }
35    }
36
37    /**
38     * @inheritDoc
39     *
40     */
41    public function addVote( $ranks ) {
42        foreach ( $this->optionIds as $oid1 ) {
43            if ( !isset( $ranks[$oid1] ) ) {
44                wfDebug( "Invalid vote record, missing option $oid1\n" );
45
46                return false;
47            }
48            foreach ( $this->optionIds as $oid2 ) {
49                # Lower = better
50                if ( $ranks[$oid1] < $ranks[$oid2] ) {
51                    $this->victories[$oid1][$oid2]++;
52                }
53            }
54        }
55
56        return true;
57    }
58
59    /**
60     * @return array
61     */
62    public function getOptionAbbreviations() {
63        if ( $this->abbrevs === null ) {
64            $abbrevs = [];
65            foreach ( $this->question->getOptions() as $option ) {
66                $text = $option->getMessage( 'text' );
67                $parts = explode( ' ', $text );
68                $initials = '';
69                foreach ( $parts as $part ) {
70                    $firstLetter = mb_substr( $part, 0, 1 );
71                    if ( $part === '' || ctype_punct( $firstLetter ) ) {
72                        continue;
73                    }
74                    $initials .= $firstLetter;
75                }
76                if ( isset( $abbrevs[$initials] ) ) {
77                    $index = 2;
78                    while ( isset( $abbrevs[$initials . $index] ) ) {
79                        $index++;
80                    }
81                    $initials .= $index;
82                }
83                $abbrevs[$initials] = $option->getId();
84            }
85            $this->abbrevs = array_flip( $abbrevs );
86        }
87
88        return $this->abbrevs;
89    }
90
91    /**
92     * @param string $format
93     * @return string[]
94     */
95    public function getRowLabels( $format = 'html' ) {
96        if ( !isset( $this->rowLabels[$format] ) ) {
97            $rowLabels = [];
98            $abbrevs = $this->getOptionAbbreviations();
99            foreach ( $this->question->getOptions() as $option ) {
100                if ( $format == 'html' ) {
101                    $label = $option->parseMessage( 'text' );
102                } else {
103                    $label = $option->getMessage( 'text' );
104                }
105                if ( $label !== $abbrevs[$option->getId()] ) {
106                    $label .= ' (' . $abbrevs[$option->getId()] . ')';
107                }
108                $rowLabels[$option->getId()] = $label;
109            }
110            $this->rowLabels[$format] = $rowLabels;
111        }
112
113        return $this->rowLabels[$format];
114    }
115
116    /**
117     * @param array $matrix
118     * @param int[] $rankedIds
119     * @return string
120     */
121    public function convertMatrixToHtml( $matrix, $rankedIds ) {
122        $abbrevs = $this->getOptionAbbreviations();
123        $rowLabels = $this->getRowLabels( 'html' );
124
125        $s = "<table class=\"securepoll-results\">";
126
127        # Corner box
128        $s .= "<tr>\n<th>&#160;</th>\n";
129
130        # Header row
131        foreach ( $rankedIds as $oid ) {
132            $s .= Xml::tags( 'th', [], $abbrevs[$oid] ) . "\n";
133        }
134        $s .= "</tr>\n";
135
136        foreach ( $rankedIds as $oid1 ) {
137            # Header column
138            $s .= "<tr>\n";
139            $s .= Xml::tags(
140                'td',
141                [ 'class' => 'securepoll-results-row-heading' ],
142                $rowLabels[$oid1]
143            );
144            # Rest of the matrix
145            foreach ( $rankedIds as $oid2 ) {
146                $value = $matrix[$oid1][$oid2] ?? '';
147                if ( is_array( $value ) ) {
148                    $value = '(' . implode( ', ', $value ) . ')';
149                }
150                $s .= Xml::element( 'td', [], $value ) . "\n";
151            }
152            $s .= "</tr>\n";
153        }
154        $s .= "</table>";
155
156        return $s;
157    }
158
159    /**
160     * @param array $matrix
161     * @param int[] $rankedIds
162     * @return string
163     */
164    public function convertMatrixToText( $matrix, $rankedIds ) {
165        $abbrevs = $this->getOptionAbbreviations();
166        $minWidth = 15;
167        $rowLabels = $this->getRowLabels( 'text' );
168
169        # Calculate column widths
170        $colWidths = [];
171        foreach ( $abbrevs as $id => $abbrev ) {
172            if ( strlen( $abbrev ) < $minWidth ) {
173                $colWidths[$id] = $minWidth;
174            } else {
175                $colWidths[$id] = strlen( $abbrev );
176            }
177        }
178        $headerColumnWidth = $minWidth;
179        foreach ( $rowLabels as $label ) {
180            $headerColumnWidth = max( $headerColumnWidth, strlen( $label ) );
181        }
182
183        # Corner box
184        $s = str_repeat( ' ', $headerColumnWidth ) . ' | ';
185
186        # Header row
187        foreach ( $rankedIds as $oid ) {
188            $s .= str_pad( $abbrevs[$oid], $colWidths[$oid] ) . ' | ';
189        }
190        $s .= "\n";
191
192        # Divider
193        $s .= str_repeat( '-', $headerColumnWidth ) . '-+-';
194        foreach ( $rankedIds as $oid ) {
195            $s .= str_repeat( '-', $colWidths[$oid] ) . '-+-';
196        }
197        $s .= "\n";
198
199        foreach ( $rankedIds as $oid1 ) {
200            # Header column
201            $s .= str_pad( $rowLabels[$oid1], $headerColumnWidth ) . ' | ';
202
203            # Rest of the matrix
204            foreach ( $rankedIds as $oid2 ) {
205                $value = $matrix[$oid1][$oid2] ?? '';
206                if ( is_array( $value ) ) {
207                    $value = '(' . implode( ', ', $value ) . ')';
208                }
209                $s .= str_pad( $value, $colWidths[$oid2] ) . ' | ';
210            }
211            $s .= "\n";
212        }
213
214        return $s;
215    }
216}