r53798 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r53797‎ | r53798 | r53799 >
Date:08:16, 27 July 2009
Author:tstarling
Status:deferred
Tags:
Comment:
* Improved Schulze tallier, which should match the method recommended by Schulze in the draft paper, before tie-breakers.
* Added script to cross-check the Schulze tallier against the Debian method as in the standalone tool by Jochen Voss. The methods do NOT match, for reasons I am looking into. The mismatch occurs in the case where an indirect defeat ties with a direct defeat.
* Added proper result formatting for pairwise talliers.
Modified paths:
  • /trunk/extensions/SecurePoll/SecurePoll.i18n.php (modified) (history)
  • /trunk/extensions/SecurePoll/SecurePoll.php (modified) (history)
  • /trunk/extensions/SecurePoll/cli/tallyDebian.php (added) (history)
  • /trunk/extensions/SecurePoll/includes/Tallier.php (modified) (history)
  • /trunk/extensions/SecurePoll/resources/SecurePoll.css (modified) (history)

Diff [purge]

Index: trunk/extensions/SecurePoll/SecurePoll.i18n.php
@@ -154,6 +154,9 @@
155155 'securepoll-no-upload' => 'No file was uploaded, cannot tally results.',
156156 'securepoll-dump-corrupt' => 'The dump file is corrupt and cannot be processed.',
157157 'securepoll-tally-upload-error' => 'Error tallying dump file: $1',
 158+ 'securepoll-pairwise-victories' => 'Pairwise victory matrix',
 159+ 'securepoll-strength-matrix' => 'Path strength matrix',
 160+ 'securepoll-ranks' => 'Final ranking',
158161 );
159162
160163 /** Message documentation (Message documentation)
Index: trunk/extensions/SecurePoll/SecurePoll.php
@@ -79,6 +79,7 @@
8080 'SecurePoll_XMLStore' => "$dir/includes/Store.php",
8181 'SecurePoll_Tallier' => "$dir/includes/Tallier.php",
8282 'SecurePoll_PluralityTallier' => "$dir/includes/Tallier.php",
 83+ 'SecurePoll_SchulzeTallier' => "$dir/includes/Tallier.php",
8384 'SecurePoll_TallyPage' => "$dir/includes/TallyPage.php",
8485 'SecurePoll_TranslatePage' => "$dir/includes/TranslatePage.php",
8586 'SecurePoll_Voter' => "$dir/includes/Voter.php",
Index: trunk/extensions/SecurePoll/includes/Tallier.php
@@ -1,7 +1,7 @@
22 <?php
33
44 abstract class SecurePoll_Tallier {
5 - var $context, $question;
 5+ var $context, $question, $optionsById;
66
77 abstract function addVote( $scores );
88 abstract function getHtmlResult();
@@ -23,7 +23,56 @@
2424 function __construct( $context, $question ) {
2525 $this->context = $context;
2626 $this->question = $question;
 27+ foreach ( $this->question->getOptions() as $option ) {
 28+ $this->optionsById[$option->getId()] = $option;
 29+ }
2730 }
 31+
 32+ function convertRanksToHtml( $ranks ) {
 33+ $s = "<table class=\"securepoll-results\">";
 34+ $ids = array_keys( $ranks );
 35+ foreach ( $ids as $i => $oid ) {
 36+ $rank = $ranks[$oid];
 37+ $prevRank = isset( $ids[$i-1] ) ? $ranks[$ids[$i-1]] : false;
 38+ $nextRank = isset( $ids[$i+1] ) ? $ranks[$ids[$i+1]] : false;
 39+ if ( $rank === $prevRank || $rank === $nextRank ) {
 40+ $rank .= '*';
 41+ }
 42+
 43+ $option = $this->optionsById[$oid];
 44+ $s .= "<tr>" .
 45+ Xml::element( 'td', array(), $rank ) .
 46+ Xml::element( 'td', array(), $option->parseMessage( 'text' ) ) .
 47+ "</tr>\n";
 48+ }
 49+ $s .= "</table>";
 50+ return $s;
 51+ }
 52+
 53+ function convertRanksToText( $ranks ) {
 54+ $s = '';
 55+ $ids = array_keys( $ranks );
 56+ $colWidth = 6;
 57+ foreach ( $this->optionsById as $option ) {
 58+ $colWidth = max( $colWidth, $option->getMessage( 'text' ) );
 59+ }
 60+
 61+ foreach ( $ids as $i => $oid ) {
 62+ $rank = $ranks[$oid];
 63+ $prevRank = isset( $ids[$i-1] ) ? $ranks[$ids[$i-1]] : false;
 64+ $nextRank = isset( $ids[$i+1] ) ? $ranks[$ids[$i+1]] : false;
 65+ if ( $rank === $prevRank || $rank === $nextRank ) {
 66+ $rank .= '*';
 67+ }
 68+
 69+ $option = $this->optionsById[$oid];
 70+ $s .= str_pad( $rank, 6 ) . ' | ' .
 71+ $option->getMessage( 'text' ) . "\n";
 72+ }
 73+ return $s;
 74+ }
 75+
 76+
2877 }
2978
3079 /**
@@ -59,9 +108,10 @@
60109 // Show the results
61110 $s = "<table class=\"securepoll-results\">\n";
62111
63 - foreach ( $this->question->getOptions() as $option ) {
 112+ foreach ( $this->tally as $oid => $rank ) {
 113+ $option = $this->optionsById[$oid];
64114 $s .= '<tr><td>' . $option->getMessage( 'text' ) . "</td>\n" .
65 - '<td>' . $this->tally[$option->getId()] . "</td>\n" .
 115+ '<td>' . $this->tally[$oid] . "</td>\n" .
66116 "</tr>\n";
67117 }
68118 $s .= "</table>\n";
@@ -71,7 +121,8 @@
72122 function getTextResult() {
73123 // Calculate column width
74124 $width = 10;
75 - foreach ( $this->question->getOptions() as $option ) {
 125+ foreach ( $this->tally as $oid => $rank ) {
 126+ $option = $this->optionsById[$oid];
76127 $width = max( $width, strlen( $option->getMessage( 'text' ) ) );
77128 }
78129 if ( $width > 57 ) {
@@ -84,7 +135,8 @@
85136 if ( $qtext !== '' ) {
86137 $s .= wordwrap( $qtext ) . "\n";
87138 }
88 - foreach ( $this->question->getOptions() as $option ) {
 139+ foreach ( $this->tally as $oid => $rank ) {
 140+ $option = $this->optionsById[$oid];
89141 $otext = $option->getMessage( 'text' );
90142 if ( strlen( $otext ) > $width ) {
91143 $otext = substr( $otext, 0, $width - 3 ) . '...';
@@ -115,6 +167,8 @@
116168 abstract class SecurePoll_PairwiseTallier extends SecurePoll_Tallier {
117169 var $optionIds = array();
118170 var $victories = array();
 171+ var $abbrevs;
 172+ var $rowLabels = array();
119173
120174 function __construct( $context, $question ) {
121175 parent::__construct( $context, $question );
@@ -146,32 +200,284 @@
147201 }
148202 return true;
149203 }
 204+
 205+ function getOptionAbbreviations() {
 206+ if ( is_null( $this->abbrevs ) ) {
 207+ $abbrevs = array();
 208+ foreach ( $this->question->getOptions() as $option ) {
 209+ $text = $option->getMessage( 'text' );
 210+ $parts = explode( ' ', $text );
 211+ $initials = '';
 212+ foreach ( $parts as $part ) {
 213+ if ( $part === '' || ctype_punct( $part[0] ) ) {
 214+ continue;
 215+ }
 216+ $initials .= $part[0];
 217+ }
 218+ if ( isset( $abbrevs[$initials] ) ) {
 219+ $index = 2;
 220+ while ( isset( $abbrevs[$initials . $index] ) ) {
 221+ $index++;
 222+ }
 223+ $initials .= $index;
 224+ }
 225+ $abbrevs[$initials] = $option->getId();
 226+ }
 227+ $this->abbrevs = array_flip( $abbrevs );
 228+ }
 229+ return $this->abbrevs;
 230+ }
 231+
 232+ function getRowLabels( $format = 'html' ) {
 233+ if ( !isset( $this->rowLabels[$format] ) ) {
 234+ $rowLabels = array();
 235+ $abbrevs = $this->getOptionAbbreviations();
 236+ foreach ( $this->question->getOptions() as $option ) {
 237+ if ( $format == 'html' ) {
 238+ $label = $option->parseMessage( 'text' );
 239+ } else {
 240+ $label = $option->getMessage( 'text' );
 241+ }
 242+ if ( $label !== $abbrevs[$option->getId()] ) {
 243+ $label .= ' (' . $abbrevs[$option->getId()] . ')';
 244+ }
 245+ $rowLabels[$option->getId()] = $label;
 246+ }
 247+ $this->rowLabels[$format] = $rowLabels;
 248+ }
 249+ return $this->rowLabels[$format];
 250+ }
 251+
 252+ function convertMatrixToHtml( $matrix, $rankedIds ) {
 253+ $abbrevs = $this->getOptionAbbreviations();
 254+ $rowLabels = $this->getRowLabels( 'html' );
 255+
 256+ $s = "<table class=\"securepoll-results\">";
 257+
 258+ # Corner box
 259+ $s .= "<tr>\n<th>&nbsp;</th>\n";
 260+
 261+ # Header row
 262+ foreach ( $rankedIds as $oid ) {
 263+ $s .= Xml::element( 'th', array(), $abbrevs[$oid] ) . "\n";
 264+ }
 265+ $s .= "</tr>\n";
 266+
 267+ foreach ( $rankedIds as $oid1 ) {
 268+ # Header column
 269+ $s .= "<tr>\n";
 270+ $s .= Xml::element( 'td', array( 'class' => 'securepoll-results-row-heading' ),
 271+ $rowLabels[$oid1] );
 272+ # Rest of the matrix
 273+ foreach ( $rankedIds as $oid2 ) {
 274+ if ( isset( $matrix[$oid1][$oid2] ) ) {
 275+ $value = $matrix[$oid1][$oid2];
 276+ } else {
 277+ $value = '';
 278+ }
 279+ if ( is_array( $value ) ) {
 280+ $value = '(' . implode( ', ', $value ) . ')';
 281+ }
 282+ $s .= Xml::element( 'td', array(), $value ) . "\n";
 283+ }
 284+ $s .= "</tr>\n";
 285+ }
 286+ $s .= "</table>";
 287+ }
 288+
 289+ function convertMatrixToText( $matrix, $rankedIds ) {
 290+ $abbrevs = $this->getOptionAbbreviations();
 291+ $minWidth = 15;
 292+ $rowLabels = $this->getRowLabels( 'text' );
 293+
 294+ # Calculate column widths
 295+ $colWidths = array();
 296+ foreach ( $abbrevs as $id => $abbrev ) {
 297+ if ( strlen( $abbrev ) < $minWidth ) {
 298+ $colWidths[$id] = $minWidth;
 299+ } else {
 300+ $colWidths[$id] = strlen( $abbrev );
 301+ }
 302+ }
 303+ $headerColumnWidth = $minWidth;
 304+ foreach ( $rowLabels as $label ) {
 305+ $headerColumnWidth = max( $headerColumnWidth, strlen( $label ) );
 306+ }
 307+
 308+ # Corner box
 309+ $s = str_repeat( ' ', $headerColumnWidth ) . ' | ';
 310+
 311+ # Header row
 312+ foreach ( $rankedIds as $oid ) {
 313+ $s .= str_pad( $abbrevs[$oid], $colWidths[$oid] ) . ' | ';
 314+ }
 315+ $s .= "\n";
 316+
 317+ # Divider
 318+ $s .= str_repeat( '-', $headerColumnWidth ) . '-+-';
 319+ foreach ( $rankedIds as $oid ) {
 320+ $s .= str_repeat( '-', $colWidths[$oid] ) . '-+-';
 321+ }
 322+ $s .= "\n";
 323+
 324+ foreach ( $rankedIds as $oid1 ) {
 325+ # Header column
 326+ $s .= str_pad( $rowLabels[$oid1], $headerColumnWidth ) . ' | ';
 327+
 328+ # Rest of the matrix
 329+ foreach ( $rankedIds as $oid2 ) {
 330+ if ( isset( $matrix[$oid1][$oid2] ) ) {
 331+ $value = $matrix[$oid1][$oid2];
 332+ } else {
 333+ $value = '';
 334+ }
 335+ if ( is_array( $value ) ) {
 336+ $value = '(' . implode( ', ', $value ) . ')';
 337+ }
 338+ $s .= str_pad( $value, $colWidths[$oid2] ) . ' | ';
 339+ }
 340+ $s .= "\n";
 341+ }
 342+ return $s;
 343+ }
 344+
150345 }
151346
 347+
 348+
152349 /**
 350+ * A tallier which gives a tie-breaking ranking of candidates (TBRC) by
 351+ * selecting random preferential votes
 352+ */
 353+abstract class SecurePoll_RandomPrefVoteTallier {
 354+ var $records, $random;
 355+
 356+ function addVote( $vote ) {
 357+ $this->records[] = $vote;
 358+ }
 359+
 360+ function getTBRCMatrix() {
 361+ $tbrc = array();
 362+ $marked = array();
 363+
 364+ $random = $this->context->getRandom();
 365+ $status = $random->open();
 366+ if ( !$status->isOK() ) {
 367+ throw new MWException( "Unable to open random device for TBRC ranking" );
 368+ }
 369+
 370+ # Random ballot round
 371+ $numCands = count( $this->optionIds );
 372+ $numPairsRanked = 0;
 373+ $numRecordsUsed = 0;
 374+ while ( $numRecordsUsed < count( $this->records )
 375+ && $numPairsRanked < $numCands * $numCands )
 376+ {
 377+ # Pick the record
 378+ $recordIndex = $random->getInt( $numCands - $numRecordsUsed );
 379+ $ranks = $this->records[$recordIndex];
 380+ $numRecordsUsed++;
 381+
 382+ # Swap it to the end
 383+ $destIndex = $numCands - $numRecordsUsed;
 384+ $this->records[$recordIndex] = $this->records[$destIndex];
 385+ $this->records[$destIndex] = $ranks;
 386+
 387+ # Apply its rankings
 388+ foreach ( $this->optionIds as $oid1 ) {
 389+ if ( !isset( $ranks[$oid1] ) ) {
 390+ throw new MWException( "Invalid vote record, missing option $oid1" );
 391+ }
 392+ foreach ( $this->optionIds as $oid2 ) {
 393+ if ( isset( $marked[$oid1][$oid2] ) ) {
 394+ // Already ranked
 395+ continue;
 396+ }
 397+
 398+ if ( $oid1 == $oid2 ) {
 399+ # Same candidate, no win
 400+ $tbrc[$oid1][$oid2] = false;
 401+ } elseif ( $ranks[$oid1] < $ranks[$oid2] ) {
 402+ # oid1 win
 403+ $tbrc[$oid1][$oid2] = true;
 404+ } elseif ( $ranks[$oid2] < $ranks[$oid1] ) {
 405+ # oid2 win
 406+ $tbrc[$oid1][$oid2] = false;
 407+ } else {
 408+ # Tie, don't mark
 409+ continue;
 410+ }
 411+ $marked[$oid1][$oid2] = true;
 412+ $numPairsRanked++;
 413+ }
 414+ }
 415+ }
 416+
 417+ # Random win round
 418+ if ( $numPairsRanked < $numCands * $numCands ) {
 419+ $randomRanks = $random->shuffle( $this->optionIds );
 420+ foreach ( $randomRanks as $oidWin ) {
 421+ if ( $numPairsRanked >= $numCands * $numCands ) {
 422+ # Done
 423+ break;
 424+ }
 425+ foreach ( $this->optionIds as $oidOther ) {
 426+ if ( !isset( $marked[$oidWin][$oidOther] ) ) {
 427+ $tbrc[$oidWin][$oidOther] = true;
 428+ $marked[$oidWin][$oidOther] = true;
 429+ $numPairsRanked++;
 430+ }
 431+ if ( !isset( $marked[$oidOther][$oidWin] ) ) {
 432+ $tbrc[$oidOther][$oidWin] = false;
 433+ $marked[$oidOther][$oidWin] = true;
 434+ $numPairsRanked++;
 435+ }
 436+ }
 437+ }
 438+ }
 439+
 440+ return $tbrc;
 441+ }
 442+}
 443+
 444+/**
153445 * This is the basic Schulze method with no tie-breaking.
154446 */
155447 class SecurePoll_SchulzeTallier extends SecurePoll_PairwiseTallier {
156 - var $strengths;
 448+ function getPathStrengths( $victories ) {
 449+ # This algorithm follows Markus Schulze, "A New Monotonic, Clone-Independent, Reversal
 450+ # Symmetric, and Condorcet-Consistent Single-Winner Election Method" and also
 451+ # http://en.wikipedia.org/w/index.php?title=User:MarkusSchulze/Statutory_Rules&oldid=303036893
 452+ #
 453+ # Path strengths in the Schulze method are given by pairs of integers notated (a, b)
 454+ # where a is the strength in one direction and b is the strength in the other. We make
 455+ # a matrix of path strength pairs "p", giving the path strength of the row index beating
 456+ # the column index.
157457
158 - function finishTally() {
159 - # This algorithm follows Markus Schulze, "A New Monotonic, Clone-Independent, Reversal
160 - # Symmetric, and Condorcet-Consistent Single-Winner Election Method"
161 -
162 - $this->strengths = array();
 458+ # First the path strength matrix is populated with the "direct" victory count in each
 459+ # direction, i.e. the number of people who preferenced A over B, and B over A.
 460+ $strengths = array();
163461 foreach ( $this->optionIds as $oid1 ) {
164462 foreach ( $this->optionIds as $oid2 ) {
165463 if ( $oid1 === $oid2 ) {
166464 continue;
167465 }
168 - if ( $this->victories[$oid1][$oid2] > $this->victories[$oid2][$oid1] ) {
169 - $this->strengths[$oid1][$oid2] = $this->victories[$oid1][$oid2];
170 - } else {
171 - $this->strengths[$oid1][$oid2] = 0;
172 - }
 466+ $v12 = $victories[$oid1][$oid2];
 467+ $v21 = $victories[$oid2][$oid1];
 468+ #if ( $v12 > $v21 ) {
 469+ # Direct victory
 470+ $strengths[$oid1][$oid2] = array( $v12, $v21 );
 471+ #} else {
 472+ # Direct loss
 473+ # $strengths[$oid1][$oid2] = array( 0, 0 );
 474+ #}
173475 }
174476 }
175477
 478+ echo $this->convertMatrixToText( $strengths, $this->optionIds ) . "\n";
 479+
 480+ # Next (continuing the Floyd-Warshall algorithm) we calculate the strongest indirect
 481+ # paths. This part dominates the O(N^3) time order.
176482 foreach ( $this->optionIds as $oid1 ) {
177483 foreach ( $this->optionIds as $oid2 ) {
178484 if ( $oid1 === $oid2 ) {
@@ -181,58 +487,106 @@
182488 if ( $oid1 === $oid3 || $oid2 === $oid3 ) {
183489 continue;
184490 }
185 - $this->strengths[$oid2][$oid3] = max(
186 - $this->strengths[$oid2][$oid3],
187 - min(
188 - $this->strengths[$oid2][$oid1],
189 - $this->strengths[$oid1][$oid3]
190 - )
191 - );
 491+ $s21 = $strengths[$oid2][$oid1];
 492+ $s13 = $strengths[$oid1][$oid3];
 493+ $s23 = $strengths[$oid2][$oid3];
 494+ if ( $this->isSchulzeWin( $s21, $s13 ) ) {
 495+ $temp = $s13;
 496+ } else {
 497+ $temp = $s21;
 498+ }
 499+ if ( $this->isSchulzeWin( $temp, $s23 ) ) {
 500+ $strengths[$oid2][$oid3] = $temp;
 501+ }
192502 }
193503 }
194504 }
195505
196 - # Calculate ranks
197 - $this->ranks = array();
198 - $rankedOptions = $this->optionIds;
199 - usort( $rankedOptions, array( $this, 'comparePair' ) );
200 - $rankedOptions = array_reverse( $rankedOptions );
 506+ return $strengths;
 507+ }
 508+
 509+ function convertStrengthMatrixToRanks( $strengths ) {
 510+ $unusedIds = $this->optionIds;
 511+ $ranks = array();
201512 $currentRank = 1;
202 - foreach ( $rankedOptions as $i => $oid ) {
203 - if ( $i > 0 && $this->comparePair( $rankedOptions[$i-1], $oid ) ) {
204 - $currentRank = $i + 1;
 513+
 514+ while ( count( $unusedIds ) ) {
 515+ $winners = array_flip( $unusedIds );
 516+ foreach ( $unusedIds as $oid1 ) {
 517+ foreach ( $unusedIds as $oid2 ) {
 518+ if ( $oid1 == $oid2 ) {
 519+ continue;
 520+ }
 521+ $s12 = $strengths[$oid1][$oid2];
 522+ $s21 = $strengths[$oid2][$oid1];
 523+ if ( $this->isSchulzeWin( $s21, $s12 ) ) {
 524+ # oid1 is defeated by someone, not a winner
 525+ unset( $winners[$oid1] );
 526+ break;
 527+ }
 528+ }
205529 }
206 - $this->ranks[$oid] = $currentRank;
 530+ if ( !count( $winners ) ) {
 531+ # No winners, everyone ties for this position
 532+ $winners = array_flip( $unusedIds );
 533+ }
 534+
 535+ # Now $winners is the list of candidates that tie for this position
 536+ foreach ( $winners as $oid => $unused ) {
 537+ $ranks[$oid] = $currentRank;
 538+ }
 539+ $currentRank += count( $winners );
 540+ $unusedIds = array_diff( $unusedIds, array_keys( $winners ) );
207541 }
 542+ return $ranks;
208543 }
209544
210 - function comparePair( $i, $j ) {
211 - if ( $i === $j ) {
212 - return 0;
213 - }
214 - $sij = $this->strengths[$i][$j];
215 - $sji = $this->strengths[$j][$i];
216 - if ( $sij > $sji ) {
217 - return 1;
218 - } elseif ( $sji > $sij ) {
219 - return -1;
220 - } else {
221 - return 0;
222 - }
 545+ /**
 546+ * Determine whether Schulze's win relation "s1 >win s2" for path strength
 547+ * pairs s1 and s2 is satisfied.
 548+ *
 549+ * When applied to final path strengths instead of intermediate results,
 550+ * the paper notates this relation >D (greater than subscript D).
 551+ *
 552+ * The inequality in the second part is reversed because the first part
 553+ * refers to wins, and the second part refers to losses.
 554+ */
 555+ function isSchulzeWin( $s1, $s2 ) {
 556+ return $s1[0] > $s2[0] || ( $s1[0] == $s2[0] && $s1[1] < $s2[1] );
223557 }
224558
 559+ function finishTally() {
 560+ $this->strengths = $this->getPathStrengths( $this->victories );
 561+ $this->ranks = $this->convertStrengthMatrixToRanks( $this->strengths );
 562+ }
 563+
 564+ function getRanks() {
 565+ return $this->ranks;
 566+ }
 567+
225568 function getHtmlResult() {
226 - return '<pre>' . $this->getTextResult() . '</pre>';
 569+ global $wgOut;
 570+ $s = $wgOut->parse( '<h2>' . wfMsgNoTrans( 'securepoll-ranks' ) . "</h2>\n" );
 571+ $s .= $this->convertRanksToHtml( $this->ranks );
 572+
 573+ $s = $wgOut->parse( '<h2>' . wfMsgNoTrans( 'securepoll-pairwise-victories' ) . "</h2>\n" );
 574+ $rankedIds = array_keys( $this->ranks );
 575+ $s .= $this->convertMatrixToHtml( $this->victories, $rankedIds );
 576+
 577+ $s .= $wgOut->parse( '<h2>' . wfMsgNoTrans( 'securepoll-strength-matrix' ) . "</h2>\n" );
 578+ $s .= $this->convertMatrixToHtml( $this->strengths, $rankedIds );
 579+ return $s;
227580 }
228581
229582 function getTextResult() {
230 - return
231 - "Victory matrix:\n" .
232 - var_export( $this->victories, true ) . "\n\n" .
233 - "Path strength matrix:\n" .
234 - var_export( $this->strengths, true ) . "\n\n" .
235 - "Ranks:\n" .
236 - var_export( $this->ranks, true ) . "\n";
 583+ $rankedIds = array_keys( $this->ranks );
 584+
 585+ return
 586+ wfMsg( 'securepoll-ranks' ) . "\n" .
 587+ $this->convertRanksToText( $this->ranks ) . "\n\n" .
 588+ wfMsg( 'securepoll-pairwise-victories' ). "\n" .
 589+ $this->convertMatrixToText( $this->victories, $rankedIds ) . "\n\n" .
 590+ wfMsg( 'securepoll-strength-matrix' ) . "\n" .
 591+ $this->convertMatrixToText( $this->strengths, $rankedIds );
237592 }
238593 }
239 -
Index: trunk/extensions/SecurePoll/cli/tallyDebian.php
@@ -0,0 +1,91 @@
 2+<?php
 3+
 4+require( dirname(__FILE__).'/cli.inc' );
 5+
 6+if ( !isset( $args[0] ) ) {
 7+ echo "Usage: php tallyDebian.php <file>\n";
 8+ exit( 1 );
 9+}
 10+
 11+$file = fopen( $args[0], 'r' );
 12+if ( !$file ) {
 13+ echo "Unable to open file \"$args[0]\" for input\n";
 14+}
 15+
 16+$votes = array();
 17+$numCands = 0;
 18+while ( !feof( $file ) ) {
 19+ $line = fgets( $file );
 20+ if ( $line === false ) {
 21+ break;
 22+ }
 23+ $line = trim( $line );
 24+ if ( !preg_match( '/^V: ([0-9-]*)$/', $line, $m ) ) {
 25+ echo "Skipping unrecognised line $line\n";
 26+ continue;
 27+ }
 28+
 29+ $record = array();
 30+ for ( $i = 0; $i < strlen( $m[1] ); $i++ ) {
 31+ $pref = substr( $m[1], $i, 1 );
 32+ if ( $pref === '-' ) {
 33+ $record[$i] = 1000;
 34+ } else {
 35+ $record[$i] = intval( $pref );
 36+ }
 37+ }
 38+ $votes[] = $record;
 39+ $numCands = max( $numCands, count( $record ) );
 40+}
 41+
 42+$options = array();
 43+for ( $i = 0; $i < $numCands - 1; $i++ ) {
 44+ $options[] = chr( ord( 'A' ) + $i );
 45+}
 46+$options[] = 'X';
 47+$question = new SecurePoll_FakeQuestion( $options );
 48+$tallier = new SecurePoll_SchulzeTallier( false, $question );
 49+foreach ( $votes as $vote ) {
 50+ $tallier->addVote( $vote );
 51+}
 52+$tallier->finishTally();
 53+echo $tallier->getTextResult();
 54+
 55+
 56+
 57+class SecurePoll_FakeQuestion {
 58+ var $options;
 59+
 60+ function __construct( $options ) {
 61+ $this->options = array();
 62+ foreach ( $options as $i => $option ) {
 63+ $this->options[] = new SecurePoll_FakeOption( $i, $option );
 64+ }
 65+ }
 66+
 67+ function getOptions() {
 68+ return $this->options;
 69+ }
 70+}
 71+
 72+class SecurePoll_FakeOption {
 73+ var $id, $name;
 74+
 75+ function __construct( $id, $name ) {
 76+ $this->id = $id;
 77+ $this->name = $name;
 78+ }
 79+
 80+ function getMessage( $key ) {
 81+ return $this->name;
 82+ }
 83+
 84+ function parseMessage( $key ) {
 85+ return htmlspecialchars( $this->name );
 86+ }
 87+
 88+ function getId() {
 89+ return $this->id;
 90+ }
 91+}
 92+
Property changes on: trunk/extensions/SecurePoll/cli/tallyDebian.php
___________________________________________________________________
Name: svn:eol-style
193 + native
Index: trunk/extensions/SecurePoll/resources/SecurePoll.css
@@ -62,4 +62,6 @@
6363 .securepoll-results caption {
6464 font-weight: bold;
6565 }
66 -
 66+.securepoll-results-row-heading {
 67+ background: #f2f2f2;
 68+}

Status & tagging log