r88962 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r88961‎ | r88962 | r88963 >
Date:09:42, 27 May 2011
Author:happy-melon
Status:deferred (Comments)
Tags:
Comment:
Alternative Vote tallier for SecurePoll.
Modified paths:
  • /trunk/extensions/SecurePoll/SecurePoll.i18n.php (modified) (history)
  • /trunk/extensions/SecurePoll/SecurePoll.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/talliers/AlternativeVoteTallier.php (added) (history)
  • /trunk/extensions/SecurePoll/includes/talliers/Tallier.php (modified) (history)
  • /trunk/extensions/SecurePoll/resources/SecurePoll.css (modified) (history)

Diff [purge]

Index: trunk/extensions/SecurePoll/SecurePoll.i18n.php
@@ -163,6 +163,9 @@
164164 'securepoll-strength-matrix' => 'Path strength matrix',
165165 'securepoll-ranks' => 'Final ranking',
166166 'securepoll-average-score' => 'Average score',
 167+ 'securepoll-round' => 'Round $1',
 168+ 'securepoll-spoilt' => '(Spoilt)',
 169+ 'securepoll-exhausted' => '(Exhausted)',
167170 );
168171
169172 /** Message documentation (Message documentation)
@@ -238,6 +241,9 @@
239242 'securepoll-subpage-list' => 'Link text to a sub page in the SecurePoll extension where users can list poll information.',
240243 'securepoll-subpage-dump' => 'Link text to a sub page in the SecurePoll extension where users can dump results.',
241244 'securepoll-subpage-tally' => 'Link text to a sub page in the SecurePoll extension where users can tally.',
 245+ 'securepoll-round' => 'Column header for tables on tallies which take place over multiple rounds; parameter is a roman numeral.',
 246+ 'securepoll-spoilt' => 'Row label for counting ballots which were spoilt (not correctly filled in or indecipherable',
 247+ 'securepoll-exhausted' => 'Row label for counting ballots which have been exhausted in a multi-round counting system',
242248 );
243249
244250 /** Afrikaans (Afrikaans)
Index: trunk/extensions/SecurePoll/SecurePoll.php
@@ -92,6 +92,7 @@
9393 'SecurePoll_PairwiseTallier' => "$dir/includes/talliers/PairwiseTallier.php",
9494 'SecurePoll_PluralityTallier' => "$dir/includes/talliers/PluralityTallier.php",
9595 'SecurePoll_SchulzeTallier' => "$dir/includes/talliers/SchulzeTallier.php",
 96+ 'SecurePoll_AlternativeVoteTallier' => "$dir/includes/talliers/AlternativeVoteTallier.php",
9697 'SecurePoll_Tallier' => "$dir/includes/talliers/Tallier.php",
9798
9899 # user
Index: trunk/extensions/SecurePoll/includes/talliers/AlternativeVoteTallier.php
@@ -0,0 +1,205 @@
 2+<?php
 3+
 4+/**
 5+ * Tallier for AlternativeVote system.
 6+ */
 7+class SecurePoll_AlternativeVoteTallier extends SecurePoll_Tallier {
 8+
 9+ /**
 10+ * Array of ballot bins for each candidate
 11+ * @var array
 12+ */
 13+ var $ballots = array();
 14+
 15+ /**
 16+ * Votes which give the same preference to more than one candidate are considered
 17+ * spoilt; keep a count of these so scrutineers can check that the numbers add up
 18+ * array( <round number> => <number>
 19+ */
 20+ var $spoilt = array( 1 => 0);
 21+ var $exhausted = array( 1 => 0 );
 22+
 23+ // Total number of counting rounds
 24+ var $rounds = 0;
 25+
 26+ // Total number of candidates who received any votes at all
 27+ var $numCandidates = 0;
 28+
 29+ /**
 30+ * The results of the counting process
 31+ * array( <option id> => array( <round number> => <votes> ) )
 32+ */
 33+ var $results = array();
 34+
 35+
 36+ /**
 37+ * @param $context SecurePoll_Context
 38+ * @param $electionTallier SecurePoll_ElectionTallier
 39+ * @param $question SecurePoll_Question
 40+ */
 41+ function __construct( $context, $electionTallier, $question ) {
 42+ parent::__construct( $context, $electionTallier, $question );
 43+
 44+ foreach ( $question->getOptions() as $option ) {
 45+ $this->results[$option->getId()] = array();
 46+ }
 47+ }
 48+
 49+ /**
 50+ * Add a voter's preferences to the ballot bin
 51+ *
 52+ * @param $scores array of <option_id> => <preference>
 53+ * @return bool Whether the vote was parsed correctly
 54+ */
 55+ function addVote( $scores ) {
 56+
 57+ foreach ( $scores as $oid => $score ) {
 58+ if ( !isset( $this->results[$oid] ) ) {
 59+ wfDebug( __METHOD__.": unknown OID $oid\n" );
 60+ return false;
 61+ }
 62+ // Score of zero = no preference
 63+ if( $score == 0 ){
 64+ unset( $scores[$oid] );
 65+ }
 66+ }
 67+
 68+ $this->numCandidates = max( $this->numCandidates, count( $scores ) );
 69+
 70+ // Simple way to check for duplicate preferences: flip the array, and the
 71+ // preferences will become duplicate keys.
 72+ $rscores = array_flip( $scores );
 73+ if( count( $rscores ) < count( $scores ) ){
 74+ wfDebug( __METHOD__.": vote has duplicate preferences, spoilt\n" );
 75+ $this->spoilt[1] ++;
 76+ return true;
 77+ } elseif ( count( $rscores ) == 0 ) {
 78+ wfDebug( __METHOD__.": vote is empty\n" );
 79+ $this->exhausted[1]++;
 80+ return true;
 81+ }
 82+
 83+ // Sorting also avoids any problem with voters skipping preferences (1, 2, 4, etc)
 84+ ksort( $rscores );
 85+
 86+ // Slightly ugly way to get the first element of the array when that might not
 87+ // have index zero
 88+ $this->ballots[reset($rscores)][] = $rscores;
 89+
 90+ return true;
 91+ }
 92+
 93+ function finishTally(){
 94+ while ( $this->rounds++ < $this->numCandidates ){
 95+ // Record the number of ballots in each bin
 96+ foreach( $this->ballots as $oid => $bin ){
 97+ $this->results[$oid][$this->rounds] = count( $bin );
 98+ }
 99+
 100+ // Carry over exhausted ballot count from previous round
 101+ $this->exhausted[$this->rounds + 1] = $this->exhausted[$this->rounds];
 102+ $this->spoilt[$this->rounds + 1] = $this->spoilt[$this->rounds];
 103+
 104+ // Sort the ballot bins by the number of ballots they contain
 105+ uasort( $this->ballots, array( __CLASS__, 'sortByArraySize' ) );
 106+
 107+ // The smallest bin is now at the end of the list; kill it
 108+ $loser = array_pop( $this->ballots );
 109+
 110+ // And redistribute its ballots to the other bins
 111+ foreach( $loser as &$ballot ){
 112+ $reused = false;
 113+ foreach( $ballot as $pref => $oid ){
 114+ if( !array_key_exists( $oid, $this->ballots ) ){
 115+ unset( $ballot[$pref] );
 116+ } else {
 117+ $this->ballots[$oid][] = $ballot;
 118+ $reused = true;
 119+ break;
 120+ }
 121+ }
 122+ if( !$reused ){
 123+ $this->exhausted[$this->rounds + 1] ++;
 124+ }
 125+ }
 126+ }
 127+
 128+ // We marked every ballot as exhausted after the final round, which is a bit silly
 129+ array_pop( $this->exhausted );
 130+ array_pop( $this->spoilt );
 131+
 132+ // Sort the results so the winner is on top
 133+ uasort( $this->results, array( __CLASS__, 'sortByArraySize' ) );
 134+ }
 135+
 136+ public static function sortByArraySize( $a, $b ){
 137+ if( !is_array( $a ) || !is_array( $b ) || count( $a ) == count( $b ) ){
 138+ return 0;
 139+ } else {
 140+ return count( $a ) < count( $b );
 141+ }
 142+ }
 143+
 144+ function getHtmlResult() {
 145+ global $wgLang;
 146+
 147+ $s = "<table class=\"securepoll-results\">\n";
 148+
 149+ $lines = array();
 150+ foreach( $this->results as $oid => $data ){
 151+ $option = $this->optionsById[$oid];
 152+ $res = implode( $data, '</td><td>' );
 153+ $name = $option->parseMessageInline( 'text' );
 154+ $lines[] = "<tr><th>$name</th><td>$res</td></tr>";
 155+ }
 156+
 157+ $t = '<th></th>';
 158+ for( $i = 1; $i < $this->rounds; $i++ ){
 159+ $ordinal = wfMsg( 'securepoll-round', $wgLang->romanNumeral( $i ) );
 160+ $t .= "<th>$ordinal</th>";
 161+ }
 162+
 163+ $s .= "<tr>$t</tr>\n<tr>" . implode( $lines, "\n" ) . "</tr>\n";
 164+
 165+ $exhausted = wfMsg( 'securepoll-exhausted' );
 166+ $s .= "<tr class='securepoll-exhausted'><th>$exhausted</th>";
 167+ $s .= "<td>" . implode( array_values( $this->exhausted ), "</td><td>" ) . "</td></tr>\n";
 168+
 169+ $spoilt = wfMsg( 'securepoll-spoilt' );
 170+ $s .= "<tr class='securepoll-spoilt'><th>$spoilt</th>";
 171+ $s .= "<td>" . implode( array_values( $this->spoilt ), "</td><td>" ) . "</td></tr>\n";
 172+
 173+ $s .= "</table>\n";
 174+ return $s;
 175+ }
 176+
 177+ function getTextResult() {
 178+ // Calculate column width
 179+ $width = 10;
 180+ foreach ( $this->results as $oid => $rank ) {
 181+ $option = $this->optionsById[$oid];
 182+ $width = min( 57, max( $width, strlen( $option->getMessage( 'text' ) ) ) );
 183+ }
 184+
 185+ // Show the results
 186+ $qtext = $this->question->getMessage( 'text' );
 187+ $s = '';
 188+ if ( $qtext !== '' ) {
 189+ $s .= wordwrap( $qtext ) . "\n";
 190+ }
 191+
 192+ foreach ( $this->results as $oid => $data ) {
 193+ $option = $this->optionsById[$oid];
 194+ $otext = $option->getMessage( 'text' );
 195+ if ( strlen( $otext ) > $width ) {
 196+ $otext = substr( $otext, 0, $width - 3 ) . '...';
 197+ } else {
 198+ $otext = str_pad( $otext, $width );
 199+ }
 200+ $s .= $otext . ' | ';
 201+ $s .= implode( array_values( $data ), ' | ' ) . "\n";
 202+ }
 203+ return $s;
 204+ }
 205+}
 206+
Property changes on: trunk/extensions/SecurePoll/includes/talliers/AlternativeVoteTallier.php
___________________________________________________________________
Added: svn:eol-style
1207 + native
Index: trunk/extensions/SecurePoll/includes/talliers/Tallier.php
@@ -22,6 +22,8 @@
2323 return new SecurePoll_SchulzeTallier( $context, $electionTallier, $question );
2424 case 'histogram-range':
2525 return new SecurePoll_HistogramRangeTallier( $context, $electionTallier, $question );
 26+ case 'alternative-vote':
 27+ return new SecurePoll_AlternativeVoteTallier( $context, $electionTallier, $question );
2628 default:
2729 throw new MWException( "Invalid tallier type: $type" );
2830 }
Index: trunk/extensions/SecurePoll/resources/SecurePoll.css
@@ -97,3 +97,17 @@
9898 .securepoll-election-closed {
9999 color: #aaa;
100100 }
 101+
 102+.securepoll-results th {
 103+ padding-right:5px;
 104+ text-align: left;
 105+}
 106+
 107+tr.securepoll-exhausted th, tr.securepoll-exhausted td {
 108+ font-style: italic;
 109+ border-top: 1px solid #aaa;
 110+}
 111+
 112+tr.securepoll-spoilt {
 113+ font-style: italic;
 114+}
\ No newline at end of file

Sign-offs

UserFlagDate
Werdnainspected07:09, 28 May 2011

Comments

#Comment by Reedy (talk | contribs)   17:21, 27 May 2011

So after the British public didn't accept it, you thought WMF might want it? :P

#Comment by Happy-melon (talk | contribs)   19:11, 27 May 2011

I couldn't care less whether WMF want it, I want it to stop causing merge conflicts on my working copy :P

Status & tagging log