File Coverage

blib/lib/Vote/Count/TieBreaker.pm
Criterion Covered Total %
statement 231 234 98.7
branch 60 64 93.7
condition 8 8 100.0
subroutine 29 29 100.0
pod 5 8 62.5
total 333 343 97.0


line stmt bran cond sub pod time code
1 39     39   21278 use strict;
  39         114  
  39         1205  
2 39     39   230 use warnings;
  39         81  
  39         972  
3 39     39   669 use 5.024;
  39         143  
4              
5 39     39   210 use feature qw /postderef signatures switch/;
  39         88  
  39         3416  
6              
7             use Moose::Role;
8 39     39   249  
  39         92  
  39         293  
9             no warnings 'experimental';
10 39     39   205115 use List::Util qw( min max sum );
  39         101  
  39         1697  
11 39     39   251 use Path::Tiny;
  39         90  
  39         3084  
12 39     39   332 # use Data::Dumper;
  39         98  
  39         1986  
13             # use Data::Printer;
14             use Vote::Count::RankCount;
15 39     39   319 use List::Util qw( min max sum);
  39         113  
  39         1231  
16 39     39   247 use Carp;
  39         115  
  39         2015  
17 39     39   259 use Try::Tiny;
  39         113  
  39         2210  
18 39     39   260  
  39         81  
  39         61287  
19             our $VERSION='2.02';
20              
21             =head1 NAME
22              
23             Vote::Count::TieBreaker
24              
25             =head1 VERSION 2.02
26              
27             =head1 Synopsis
28              
29             my $Election = Vote::Count->new(
30             'BallotSet' => $ballotsirvtie2,
31             'TieBreakMethod' => 'approval',
32             'TieBreakerFallBackPrecedence' => 0,
33             );
34              
35             =cut
36              
37             # ABSTRACT: TieBreaker object for Vote::Count. Toolkit for vote counting.
38              
39             =head1 TieBreakMethods
40              
41             =head2 TieBreakMethod argement to new
42              
43             'approval'
44             'topcount' [ of just tied choices ]
45             'topcount_active' [ currently active choices ]
46             'all' [ eliminate all tied choices ]
47             'borda' [ Borda Count to current Active set ]
48             'borda_all' [ includes all choices in Borda Count ]
49             'grandjunction' [ more resolveable than simple TopCount would be ]
50             'none' [ eliminate no choices ]
51             'precedence' [ requires also setting PrecedenceFile ]
52              
53             Approval, TopCount, and Borda may be passed in either lower case or in the CamelCase form of the method name. borda_all calculates the Borda Count with all choices which can yield a different result than just the current choices. If you want TopCount to use all of the choices, or a snapshot such as after a floor rule, generate a Precedence File and then use that with Precedence as the Tie Breaker.
54              
55             =head2 (Modified) Grand Junction
56              
57             The Grand Junction (also known as Bucklin) method is one of the simplest and easiest to Hand Count RCV resolution methods. Other than that, it is generally not considered a good method.
58              
59             Because it is simple, and nearly always resolves, except when ballots are perfectly matched up, it is a great TieBreaker. It is not Later Harm Safe, but heavily favors higher rankings.
60              
61             =head3 The (Standard) Grand Junction Method
62              
63             Only the Tie-Breaker variant is currently implemented in Vote::Count.
64              
65             =over
66              
67             =item 1
68              
69             Count the Ballots to determine the quota for a majority.
70              
71             =item 2
72              
73             Count the first choices and elect a choice which has a majority.
74              
75             =item 3
76              
77             If there is no winner add the second choices to the totals and elect the choice which has a majority (or the most votes if more than one choice reaches a majority).
78              
79             =item 4
80              
81             Keep adding the next rank to the totals until either there is a winner or all ballots are exhausted.
82              
83             =item 5
84              
85             When all ballots are exhausted the choice with the highest total wins.
86              
87             =back
88              
89             =head3 As a Tie Breaker
90              
91             The Tie Breaker Method is modified.
92              
93             Instead of Majority, any choice with a current total less than another is eliminated. This allows resolution of any number of choices in a tie.
94              
95             The winner is the last choice remaining.
96              
97             =head3 TieBreakerGrandJunction
98              
99             my $resolve = $Election->TieBreakerGrandJunction( $choice1, $choice2 [ $choice3 ... ] );
100             if ( $resolve->{'winner'}) { say "Tie Winner is $resolve->{'winner'}"}
101             elsif ( $resolve->{'tie'}) {
102             my @tied = $resolve->{'tied'}->@*;
103             say "Still tied between @tied."
104             }
105              
106             The Tie Breaking will be logged to the verbose log, any number of tied choices may be provided.
107              
108             =head2 Changing Tie Breakers
109              
110             When Changing Tie Breakers or Precedence Files, the PairMatrix is not automatically updated. To update the PairMatrix it is necessary to call the UpdatePairMatrix Method.
111              
112             =cut
113              
114             has 'TieBreakMethod' => (
115             is => 'rw',
116             isa => 'Str',
117             required => 0,
118             );
119              
120             # This is only used for the precedence tiebreaker and fallback!
121             has 'PrecedenceFile' => (
122             is => 'rw',
123             isa => 'Str',
124             required => 0,
125             trigger => \&_triggercheckprecedence,
126             );
127              
128             has 'TieBreakerFallBackPrecedence' => (
129             is => 'rw',
130             isa => 'Bool',
131             default => 0,
132             lazy => 0,
133             trigger => \&_triggercheckprecedence,
134             );
135              
136             unless ( $I->PrecedenceFile() ) {
137 289     289   5393 $I->PrecedenceFile('/tmp/precedence.txt');
  289         454  
  289         471  
  289         465  
  289         506  
138 289 100       8604 $I->logt( "Generated FallBack TieBreaker Precedence Order: \n"
139 53         1456 . join( ', ', $I->CreatePrecedenceRandom() ) );
140 53         295 }
141             $I->{'PRECEDENCEORDER'} = undef; # clear cached if the file changes.
142             }
143 289         7480  
144             my $ballots = $self->BallotSet()->{'ballots'};
145             my %current = ( map { $_ => 0 } @tiedchoices );
146 70     70 1 10826 my $deepest = 0;
  70         95  
  70         134  
  70         92  
147 70         1834 for my $b ( keys $ballots->%* ) {
148 70         141 my $depth = scalar $ballots->{$b}{'votes'}->@*;
  169         375  
149 70         134 $deepest = $depth if $depth > $deepest;
150 70         282 }
151 756         1147 my $round = 1;
152 756 100       1364 while ( $round <= $deepest ) {
153             $self->logv("Tie Breaker Round: $round");
154 70         217 for my $b ( keys $ballots->%* ) {
155 70         175 my $pick = $ballots->{$b}{'votes'}[ $round - 1 ] or next;
156 134         531 if ( defined $current{$pick} ) {
157 134         475 $current{$pick} += $ballots->{$b}{'count'};
158 1416 100       2785 }
159 1024 100       1952 }
160 227         430 my $max = max( values %current );
161             for my $c ( sort @tiedchoices ) {
162             $self->logv("\t$c: $current{$c}");
163 134         506 }
164 134         358 for my $c ( sort @tiedchoices ) {
165 308         1024 if ( $current{$c} < $max ) {
166             delete $current{$c};
167 134         392 $self->logv("Tie Breaker $c eliminated");
168 308 100       665 }
169 76         149 }
170 76         257 @tiedchoices = ( sort keys %current );
171             if ( 1 == @tiedchoices ) {
172             $self->logv("Tie Breaker Won By: $tiedchoices[0]");
173 134         422 return { 'winner' => $tiedchoices[0], 'tie' => 0, 'tied' => [] };
174 134 100       361 }
175 53         225 $round++;
176 53         397 }
177             if ( $self->TieBreakerFallBackPrecedence() ) {
178 81         196 $self->logv('Applying Precedence fallback');
179             return $self->TieBreakerPrecedence(@tiedchoices);
180 17 100       541 }
181 4         16 else {
182 4         18 return { 'winner' => 0, 'tie' => 1, 'tied' => \@tiedchoices };
183             }
184             }
185 13         101  
186             =head1 TieBreaker
187              
188             Implements some basic methods for resolving ties. The default value for IRV is eliminate 'all', and the default value for Matrix is eliminate 'none'. 'all' is inappropriate for Matrix, and 'none' is inappropriate for IRV.
189              
190             my @keep = $Election->TieBreaker( $tiebreaker, $active, @tiedchoices );
191              
192             TieBreaker returns a list containing the winner, if the method is 'all' the list is empty, if 'none' the original @tiedchoices list is returned. If the TieBreaker is a tie there will be multiple elements.
193              
194             =head1 Breaking Ties With Precedence
195              
196             Since many existing Elections Rules call for Random, and Vote::Count does not accept Random as the result will be different bewtween runs, Precedence allows the Administrators of an election to randomly or arbitrarily determine who will win ties before running Vote::Count.
197              
198             The Precedence list takes the choices of the election one per line. Choices defeat any choice later than them in the list. When Precedence is used an additional attribute must be specified for the Precedence List.
199              
200             my $Election = Vote::Count->new(
201             BallotSet => read_ballots('somefile'),
202             TieBreakMethod => 'precedence',
203             PrecedenceFile => '/path/to/precedencefile');
204              
205             =head2 Precedence (Method)
206              
207             Returns a Vote::Count::RankCount object from the Precedence List. Takes a HashRef of an Active set as an optional argument, defaults to the Current Active Set.
208              
209             my $RankCountByPrecedence = $Election->Precedence();
210             my $RankCountByPrecedence = $Election->Precedence( $active );
211              
212             =head2 CreatePrecedenceRandom
213              
214             Creates a Predictable Psuedo Random Precedence file, and returns the list. Randomizes the choices using the number of ballots as the Random Seed for Perl's built in rand() function. For any given Ballot File, it will always return the same list. If the precedence filename argument is not given it defaults to '/tmp/precedence.txt'. This is the best solution to use where the Rules call for Random, in a large election the number of ballots cast will be sufficiently random, while anyone with access to Perl can reproduce the Precedence file.
215              
216             # Generate a random precedence file
217             my @precedence = Vote::Count->new( BallotSet => read_ballots('somefile') )
218             ->CreatePrecedenceRandom( '/tmp/precedence.txt');
219             # Create a new Election with it.
220             my $Election = Vote::Count->new( BallotSet => read_ballots('somefile'),
221             PrecedenceFile => '/tmp/precedence.txt', TieBreakMethod => 'Precedence' );
222              
223             =head2 TieBreakerFallBackPrecedence
224              
225             This optional argument enables or disables using precedence as a fallback if the primary tiebreaker cannot break the tie. Generates /tmp/precedence.txt using CreatePrecedenceRandom if no PrecedenceFile is specified. Default is off (0).
226              
227             TieBreakMethod must be defined and may not be all or none.
228              
229             =head2 UnTieList
230              
231             Sort a list in an order determined by a ranking method, sorted in Descending Order. The ranking must be a method that returns a RankCount object: Borda, TopCount, Precedence and Approval. If the tie is not resolved it will fall back to Precedence.
232              
233             my @orderedlosers = $Election->UnTieList(
234             'ranking1' => $Election->TieBreakMethod(), 'tied' => \@unorderedlosers );
235              
236             A second method may be provided.
237              
238             my @orderedlosers = $Election->UnTieList(
239             'ranking1' => 'TopCount', 'ranking2' => 'Borda', 'tied' => \@unorderedlosers );
240              
241             This method requires that Precedence be enabled either by having enabled TieBreakerFallBackPrecedence or by setting the TieBreakMethod to Precedence.
242              
243             =head2 UnTieActive
244              
245             Produces a precedence list of all the active choices in the election. Passes the ranking1 and ranking2 arguments to UnTieList and the Active Set as the list to untie.
246              
247             my @untiedset = $Election->UnTieActive( 'ranking1' => 'TopCount', 'ranking2' => 'Approval');
248              
249             =head1 TopCount > Approval > Precedence
250              
251             Top Count > Approval > Precedence produces a fully resolveable Tie Breaker that will almost never fall back to Precedence. It makes sense to the voters and limits Later Harm by putting Top Count first. The Precedence order should be determined before counting, the old fashioned coffee can is great for this, or use CreatePrecedenceRandom.
252              
253             To apply Top Count > Approval > Precedence you need to start with a random Precedence File, Untie the choices, and switch Precedence Files:
254              
255             use Path::Tiny;
256             my $Election = Vote::Count->new(
257             BallotSet => read_ballots($ballots),
258             PrecedenceFile => $initial,
259             TieBreakMethod => 'Precedence',
260             );
261             # Create the new Precedence
262             my @newbreaker = $Election->UnTieActive(
263             'ranking1' => 'TopCount',
264             'ranking2' => 'Approval'
265             );
266             local $" = ' > '; # set list separator to >
267             $Election->logv("Setting Tie Break Order to: @newbreaker");
268             local $" = "\n"; # set list separator to new line.
269             path($newprecedence)->spew("@newbreaker");
270             $Election->PrecedenceFile($newprecedence);
271             $Election->UpdatePairMatrix();
272              
273             =cut
274              
275             my %ordered = ();
276             my $start = 0;
277             if ( defined $I->{'PRECEDENCEORDER'} ) {
278 312     312   429 %ordered = $I->{'PRECEDENCEORDER'}->%*;
  312         427  
  312         516  
  312         406  
279 312         497 }
280 312         440 else {
281 312 100       720 for ( split /\n/, path( $I->PrecedenceFile() )->slurp() ) {
282 287         1415 $_ =~ s/\s//g; #strip out any accidental white space
283             $ordered{$_} = ++$start;
284             }
285 25         1111 for my $c ( $I->GetChoices ) {
286 253         7239 unless ( defined $ordered{$c} ) {
287 253         465 croak "Choice $c missing from precedence file\n";
288             }
289 25         163 }
290 253 50       534 $I->{'PRECEDENCEORDER'} = \%ordered;
291 0         0 }
292             my %L = map { $ordered{$_} => $_ } @list;
293             return ( map { $L{$_} } ( sort { $a <=> $b } keys %L ) );
294 25         92 }
295              
296 312         766 my @list = $I->_precedence_sort(@tiedchoices);
  818         1962  
297 312         1084 return { 'winner' => $list[0], 'tie' => 0, 'tied' => [] };
  818         2142  
  851         1528  
298             }
299              
300 143     143 0 226 my @choices = $I->GetActiveList();
  143         534  
  143         248  
  143         190  
301 143         338 my %randomized = ();
302 143         898 srand( $I->BallotSet()->{'votescast'} );
303             while (@choices) {
304             my $next = shift @choices;
305 88     88 1 3102 my $random = int( rand(1000000) );
  88         149  
  88         228  
  88         255  
306 88         390 if ( defined $randomized{$random} ) {
307 88         286 # collision, this choice needs to do again.
308 88         2503 unshift @choices, ($next);
309 88         310 }
310 709         1056 else {
311 709         1315 $randomized{$random} = $next;
312 709 50       1352 }
313             }
314 0         0 my @precedence =
315             ( map { $randomized{$_} } sort { $a <=> $b } ( keys %randomized ) );
316             path($outfile)->spew( join( "\n", @precedence ) . "\n" );
317 709         1907 $I->PrecedenceFile( $outfile );
318             return @precedence;
319             }
320              
321 88         650 no warnings 'uninitialized';
  709         1184  
  1487         2153  
322 88         497 $tiebreaker = lc $tiebreaker;
323 88         95811 if ( $tiebreaker eq 'none' ) { return @tiedchoices }
324 88         957 if ( $tiebreaker eq 'all' ) { return () }
325             my $choices_hashref = { map { $_ => 1 } @tiedchoices };
326             my $ranked = undef;
327 417     417 0 6292 if ( $tiebreaker eq 'borda' ) {
  417         622  
  417         653  
  417         613  
  417         753  
  417         546  
328 39     39   339 $ranked = $I->Borda($active);
  39         93  
  39         38076  
329 417         795 }
330 417 100       983 elsif ( $tiebreaker eq 'borda_all' ) {
  157         507  
331 260 100       637 $ranked = $I->Borda( $I->BallotSet()->{'choices'} );
  17         80  
332 243         485 }
  536         1319  
333 243         490 elsif ( $tiebreaker eq 'approval' ) {
334 243 100       1087 $ranked = $I->Approval($choices_hashref);
    100          
    100          
    100          
    100          
    100          
    100          
335 3         24 }
336             elsif ( $tiebreaker eq 'topcount' ) {
337             $ranked = $I->TopCount($choices_hashref);
338 3         92 }
339             elsif ( $tiebreaker eq 'topcount_active' ) {
340             $ranked = $I->TopCount($active);
341 66         292 }
342             elsif ( $tiebreaker eq 'grandjunction' ) {
343             my $GJ = $I->TieBreakerGrandJunction(@tiedchoices);
344 2         11 if ( $GJ->{'winner'} ) { return $GJ->{'winner'} }
345             elsif ( $GJ->{'tie'} ) { return $GJ->{'tied'}->@* }
346             else { croak "unexpected (or no) result from $tiebreaker!\n" }
347 2         20 }
348             elsif ( $tiebreaker eq 'precedence' ) {
349             # The one nice thing about precedence is that there is always a winner.
350 64         200 return $I->TieBreakerPrecedence(@tiedchoices)->{'winner'};
351 64 100       232 }
  52 50       268  
352 12         74 else { croak "undefined tiebreak method $tiebreaker!\n" }
353 0         0 my @highchoice = ();
354             my $highest = 0;
355             my $counted = $ranked->RawCount();
356             for my $c (@tiedchoices) {
357 102         276 if ( $counted->{$c} > $highest ) {
358             @highchoice = ($c);
359 1         86 $highest = $counted->{$c};
360 76         148 }
361 76         147 elsif ( $counted->{$c} == $highest ) {
362 76         211 push @highchoice, $c;
363 76         147 }
364 178 100       402 }
    100          
365 85         165 my $terse =
366 85         163 "Tie Breaker $tiebreaker: "
367             . join( ', ', @tiedchoices )
368             . "\nwinner(s): "
369 80         162 . join( ', ', @highchoice );
370             $I->{'last_tiebreaker'} = {
371             'terse' => $terse,
372 76         422 'verbose' => $ranked->RankTable(),
373             };
374             if ( @highchoice > 1 && $I->TieBreakerFallBackPrecedence() ) {
375             my $winner = $I->TieBreakerPrecedence(@tiedchoices)->{'winner'};
376             $I->{'last_tiebreaker'}{'terse'} .= "\nWinner by Precedence: $winner";
377 76         224 return ( $winner );
378             }
379             return (@highchoice);
380             }
381 76 100 100     2200  
382 36         94 $active = $I->Active() unless defined $active;
383 36         152 return Vote::Count::RankCount->newFromList(
384 36         287 $I->_precedence_sort( keys( $active->%* ) ) );
385             }
386 40         331  
387              
388             my %T = map { $_ => $RC->{$_} } @tied;
389 39     39 1 77 my @order = ();
  39         142  
  39         65  
  39         53  
390 39 100       202 while ( keys %T ) {
391 39         155 my $best = min values %T;
392             my @leaders = ();
393             for my $leader ( keys %T ) {
394             push @leaders, $leader if $T{$leader} == $best;
395 20     20 0 62 }
396             @leaders = $I->_precedence_sort(@leaders);
397 44     44   64 push @order, @leaders;
  44         73  
  44         55  
  44         87  
  44         59  
398 44         82 for (@leaders) { delete $T{$_} }
  144         299  
399 44         86 }
400 44         116 return @order;
401 101         267 }
402 101         151  
403 101         204 no warnings 'uninitialized';
404 275 100       554 unless ( $I->TieBreakerFallBackPrecedence()
405             or lc($I->TieBreakMethod) eq 'precedence' )
406 101         213 {
407 101         182 croak
408 101         171 "TieBreakerFallBackPrecedence must be enabled or TieBreakMethod must be precedence to use UnTieList [UnTieActive and BottomRunOff call it]";
  144         319  
409             }
410 44         200 my $ranking1 = $args{ranking1} ;
411             my $ranking2 = $args{ranking2} || 'Precedence';
412             my @tied = $args{tied}->@*;
413 58     58 1 1064 my %tieactive = map { $_ => 1 } @tied;
  58         90  
  58         152  
  58         84  
414 39     39   594  
  39         106  
  39         25710  
415 58 100 100     1904 my @ordered = ();
416             return $I->_precedence_sort(@tied) if ( lc($ranking1) eq 'precedence' );
417             my $RC1 = try { $I->$ranking1( \%tieactive )->HashByRank() }
418 3         516 catch {
419             my $mthstr = $ranking1 ? $ranking1 : "missing ranking1 . methods $ranking1 ? $ranking2 ";
420             croak "Unable to rank choices by $mthstr."
421 55         133 };
422 55   100     164 my $RC2 = try {$I->$ranking2( \%tieactive )->HashWithOrder() }
423 55         164 catch {
424 55         111 my $mthstr = $ranking2 ? $ranking2 : "missing ranking2 . methods $ranking1 ? $ranking2 ";
  391         726  
425             croak "Unable to rank choices by $mthstr."
426 55         119 };
427 55 100       156 for my $level ( sort { $a <=> $b } ( keys $RC1->%* ) ) {
428 52     52   2920 my @l = @{ $RC1->{$level} };
429             my @suborder = ();
430 2 100   2   44 if ( 1 == $RC1->{$level}->@* ) { @suborder = @l }
431 2         193 elsif ( $ranking2 eq 'precedence' ) {
432 52         431 @suborder = $I->_precedence_sort(@l);
433 50     50   2328 }
434             else {
435 1 50   1   20 @suborder = $I->_shortuntie( $RC2, @l );
436 1         97 }
437 50         1252 push @ordered, @suborder;
438 49         1000 }
  232         384  
439 181         236 return @ordered;
  181         361  
440 181         264 }
441 181 100       460  
  111 100       171  
442             $ARGS{'tied'} = [ $I->GetActiveList() ];
443 26         59 $I->UnTieList( %ARGS );
444             }
445              
446 44         142 1;
447              
448 181         395 #FOOTER
449              
450 49         412 =pod
451              
452             BUG TRACKER
453 10     10 1 903  
  10         20  
  10         30  
  10         14  
454 10         41 L<https://github.com/brainbuz/Vote-Count/issues>
455 10         52  
456             AUTHOR
457              
458             John Karr (BRAINBUZ) brainbuz@cpan.org
459              
460             CONTRIBUTORS
461              
462             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
463              
464             LICENSE
465              
466             This module is released under the GNU Public License Version 3. See license file for details. For more information on this license visit L<http://fsf.org>.
467              
468             SUPPORT
469              
470             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
471              
472             =cut
473