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 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    public function __construct( $context, $electionTallier, $question ) {
22        parent::__construct( $context, $electionTallier, $question );
23        $this->optionIds = [];
24        foreach ( $question->getOptions() as $option ) {
25            $this->optionIds[] = $option->getId();
26        }
27
28        $this->victories = [];
29        foreach ( $this->optionIds as $i ) {
30            foreach ( $this->optionIds as $j ) {
31                $this->victories[$i][$j] = 0;
32            }
33        }
34    }
35
36    /**
37     * @inheritDoc
38     *
39     */
40    public function addVote( $ranks ) {
41        foreach ( $this->optionIds as $oid1 ) {
42            if ( !isset( $ranks[$oid1] ) ) {
43                wfDebug( "Invalid vote record, missing option $oid1\n" );
44
45                return false;
46            }
47            foreach ( $this->optionIds as $oid2 ) {
48                # Lower = better
49                if ( $ranks[$oid1] < $ranks[$oid2] ) {
50                    $this->victories[$oid1][$oid2]++;
51                }
52            }
53        }
54
55        return true;
56    }
57
58    public function getOptionAbbreviations() {
59        if ( $this->abbrevs === null ) {
60            $abbrevs = [];
61            foreach ( $this->question->getOptions() as $option ) {
62                $text = $option->getMessage( 'text' );
63                $parts = explode( ' ', $text );
64                $initials = '';
65                foreach ( $parts as $part ) {
66                    $firstLetter = mb_substr( $part, 0, 1 );
67                    if ( $part === '' || ctype_punct( $firstLetter ) ) {
68                        continue;
69                    }
70                    $initials .= $firstLetter;
71                }
72                if ( isset( $abbrevs[$initials] ) ) {
73                    $index = 2;
74                    while ( isset( $abbrevs[$initials . $index] ) ) {
75                        $index++;
76                    }
77                    $initials .= $index;
78                }
79                $abbrevs[$initials] = $option->getId();
80            }
81            $this->abbrevs = array_flip( $abbrevs );
82        }
83
84        return $this->abbrevs;
85    }
86
87    public function getRowLabels( $format = 'html' ) {
88        if ( !isset( $this->rowLabels[$format] ) ) {
89            $rowLabels = [];
90            $abbrevs = $this->getOptionAbbreviations();
91            foreach ( $this->question->getOptions() as $option ) {
92                if ( $format == 'html' ) {
93                    $label = $option->parseMessage( 'text' );
94                } else {
95                    $label = $option->getMessage( 'text' );
96                }
97                if ( $label !== $abbrevs[$option->getId()] ) {
98                    $label .= ' (' . $abbrevs[$option->getId()] . ')';
99                }
100                $rowLabels[$option->getId()] = $label;
101            }
102            $this->rowLabels[$format] = $rowLabels;
103        }
104
105        return $this->rowLabels[$format];
106    }
107
108    public function convertMatrixToHtml( $matrix, $rankedIds ) {
109        $abbrevs = $this->getOptionAbbreviations();
110        $rowLabels = $this->getRowLabels( 'html' );
111
112        $s = "<table class=\"securepoll-results\">";
113
114        # Corner box
115        $s .= "<tr>\n<th>&#160;</th>\n";
116
117        # Header row
118        foreach ( $rankedIds as $oid ) {
119            $s .= Xml::tags( 'th', [], $abbrevs[$oid] ) . "\n";
120        }
121        $s .= "</tr>\n";
122
123        foreach ( $rankedIds as $oid1 ) {
124            # Header column
125            $s .= "<tr>\n";
126            $s .= Xml::tags(
127                'td',
128                [ 'class' => 'securepoll-results-row-heading' ],
129                $rowLabels[$oid1]
130            );
131            # Rest of the matrix
132            foreach ( $rankedIds as $oid2 ) {
133                $value = $matrix[$oid1][$oid2] ?? '';
134                if ( is_array( $value ) ) {
135                    $value = '(' . implode( ', ', $value ) . ')';
136                }
137                $s .= Xml::element( 'td', [], $value ) . "\n";
138            }
139            $s .= "</tr>\n";
140        }
141        $s .= "</table>";
142
143        return $s;
144    }
145
146    public function convertMatrixToText( $matrix, $rankedIds ) {
147        $abbrevs = $this->getOptionAbbreviations();
148        $minWidth = 15;
149        $rowLabels = $this->getRowLabels( 'text' );
150
151        # Calculate column widths
152        $colWidths = [];
153        foreach ( $abbrevs as $id => $abbrev ) {
154            if ( strlen( $abbrev ) < $minWidth ) {
155                $colWidths[$id] = $minWidth;
156            } else {
157                $colWidths[$id] = strlen( $abbrev );
158            }
159        }
160        $headerColumnWidth = $minWidth;
161        foreach ( $rowLabels as $label ) {
162            $headerColumnWidth = max( $headerColumnWidth, strlen( $label ) );
163        }
164
165        # Corner box
166        $s = str_repeat( ' ', $headerColumnWidth ) . ' | ';
167
168        # Header row
169        foreach ( $rankedIds as $oid ) {
170            $s .= str_pad( $abbrevs[$oid], $colWidths[$oid] ) . ' | ';
171        }
172        $s .= "\n";
173
174        # Divider
175        $s .= str_repeat( '-', $headerColumnWidth ) . '-+-';
176        foreach ( $rankedIds as $oid ) {
177            $s .= str_repeat( '-', $colWidths[$oid] ) . '-+-';
178        }
179        $s .= "\n";
180
181        foreach ( $rankedIds as $oid1 ) {
182            # Header column
183            $s .= str_pad( $rowLabels[$oid1], $headerColumnWidth ) . ' | ';
184
185            # Rest of the matrix
186            foreach ( $rankedIds as $oid2 ) {
187                $value = $matrix[$oid1][$oid2] ?? '';
188                if ( is_array( $value ) ) {
189                    $value = '(' . implode( ', ', $value ) . ')';
190                }
191                $s .= str_pad( $value, $colWidths[$oid2] ) . ' | ';
192            }
193            $s .= "\n";
194        }
195
196        return $s;
197    }
198}