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   22250 use strict;
  39         92  
  39         1168  
2 39     39   198 use warnings;
  39         72  
  39         945  
3 39     39   722 use 5.024;
  39         142  
4              
5 39     39   198 use feature qw /postderef signatures switch/;
  39         74  
  39         3480  
6              
7             package Vote::Count::TieBreaker;
8 39     39   225 use Moose::Role;
  39         76  
  39         282  
9              
10 39     39   193924 no warnings 'experimental';
  39         92  
  39         1701  
11 39     39   231 use List::Util qw( min max sum );
  39         76  
  39         3040  
12 39     39   280 use Path::Tiny;
  39         81  
  39         1904  
13             # use Data::Dumper;
14             # use Data::Printer;
15 39     39   280 use Vote::Count::RankCount;
  39         87  
  39         1218  
16 39     39   221 use List::Util qw( min max sum);
  39         94  
  39         1960  
17 39     39   243 use Carp;
  39         72  
  39         2069  
18 39     39   240 use Try::Tiny;
  39         87  
  39         57840  
19              
20             our $VERSION='2.01';
21              
22             =head1 NAME
23              
24             Vote::Count::TieBreaker
25              
26             =head1 VERSION 2.01
27              
28             =head1 Synopsis
29              
30             my $Election = Vote::Count->new(
31             'BallotSet' => $ballotsirvtie2,
32             'TieBreakMethod' => 'approval',
33             'TieBreakerFallBackPrecedence' => 0,
34             );
35              
36             =cut
37              
38             # ABSTRACT: TieBreaker object for Vote::Count. Toolkit for vote counting.
39              
40             =head TieBreakMethods
41              
42             =head TieBreakMethod argement to new
43              
44             'approval'
45             'topcount' [ of just tied choices ]
46             'topcount_active' [ currently active choices ]
47             'all' [ eliminate all tied choices ]
48             'borda' [ Borda Count to current Active set ]
49             'borda_all' [ includes all choices in Borda Count ]
50             'grandjunction' [ more resolveable than simple TopCount would be ]
51             'none' [ eliminate no choices ]
52             'precedence' [ requires also setting PrecedenceFile ]
53              
54             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.
55              
56             =head2 (Modified) Grand Junction
57              
58             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.
59              
60             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.
61              
62             =head3 The (Standard) Grand Junction Method
63              
64             Only the Tie-Breaker variant is currently implemented in Vote::Count.
65              
66             =over
67              
68             =item 1
69              
70             Count the Ballots to determine the quota for a majority.
71              
72             =item 2
73              
74             Count the first choices and elect a choice which has a majority.
75              
76             =item 3
77              
78             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).
79              
80             =item 4
81              
82             Keep adding the next rank to the totals until either there is a winner or all ballots are exhausted.
83              
84             =item 5
85              
86             When all ballots are exhausted the choice with the highest total wins.
87              
88             =back
89              
90             =head3 As a Tie Breaker
91              
92             The Tie Breaker Method is modified.
93              
94             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.
95              
96             The winner is the last choice remaining.
97              
98             =head3 TieBreakerGrandJunction
99              
100             my $resolve = $Election->TieBreakerGrandJunction( $choice1, $choice2 [ $choice3 ... ] );
101             if ( $resolve->{'winner'}) { say "Tie Winner is $resolve->{'winner'}"}
102             elsif ( $resolve->{'tie'}) {
103             my @tied = $resolve->{'tied'}->@*;
104             say "Still tied between @tied."
105             }
106              
107             The Tie Breaking will be logged to the verbose log, any number of tied choices may be provided.
108              
109             =head2 Changing Tie Breakers
110              
111             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.
112              
113             =cut
114              
115             has 'TieBreakMethod' => (
116             is => 'rw',
117             isa => 'Str',
118             required => 0,
119             );
120              
121             # This is only used for the precedence tiebreaker and fallback!
122             has 'PrecedenceFile' => (
123             is => 'rw',
124             isa => 'Str',
125             required => 0,
126             trigger => \&_triggercheckprecedence,
127             );
128              
129             has 'TieBreakerFallBackPrecedence' => (
130             is => 'rw',
131             isa => 'Bool',
132             default => 0,
133             lazy => 0,
134             trigger => \&_triggercheckprecedence,
135             );
136              
137 283     283   6505 sub _triggercheckprecedence ( $I, $new, $old = undef ) {
  283         461  
  283         465  
  283         483  
  283         515  
138 283 100       8056 unless ( $I->PrecedenceFile() ) {
139 53         1461 $I->PrecedenceFile('/tmp/precedence.txt');
140 53         275 $I->logt( "Generated FallBack TieBreaker Precedence Order: \n"
141             . join( ', ', $I->CreatePrecedenceRandom() ) );
142             }
143 283         7109 $I->{'PRECEDENCEORDER'} = undef; # clear cached if the file changes.
144             }
145              
146 70     70 1 8614 sub TieBreakerGrandJunction ( $self, @tiedchoices ) {
  70         103  
  70         150  
  70         100  
147 70         1889 my $ballots = $self->BallotSet()->{'ballots'};
148 70         174 my %current = ( map { $_ => 0 } @tiedchoices );
  169         387  
149 70         153 my $deepest = 0;
150 70         298 for my $b ( keys $ballots->%* ) {
151 756         1286 my $depth = scalar $ballots->{$b}{'votes'}->@*;
152 756 100       1298 $deepest = $depth if $depth > $deepest;
153             }
154 70         211 my $round = 1;
155 70         174 while ( $round <= $deepest ) {
156 134         547 $self->logv("Tie Breaker Round: $round");
157 134         470 for my $b ( keys $ballots->%* ) {
158 1416 100       2747 my $pick = $ballots->{$b}{'votes'}[ $round - 1 ] or next;
159 1024 100       2373 if ( defined $current{$pick} ) {
160 227         453 $current{$pick} += $ballots->{$b}{'count'};
161             }
162             }
163 134         522 my $max = max( values %current );
164 134         415 for my $c ( sort @tiedchoices ) {
165 308         1061 $self->logv("\t$c: $current{$c}");
166             }
167 134         357 for my $c ( sort @tiedchoices ) {
168 308 100       720 if ( $current{$c} < $max ) {
169 76         138 delete $current{$c};
170 76         231 $self->logv("Tie Breaker $c eliminated");
171             }
172             }
173 134         422 @tiedchoices = ( sort keys %current );
174 134 100       346 if ( 1 == @tiedchoices ) {
175 53         204 $self->logv("Tie Breaker Won By: $tiedchoices[0]");
176 53         350 return { 'winner' => $tiedchoices[0], 'tie' => 0, 'tied' => [] };
177             }
178 81         192 $round++;
179             }
180 17 100       509 if ( $self->TieBreakerFallBackPrecedence() ) {
181 4         13 $self->logv('Applying Precedence fallback');
182 4         13 return $self->TieBreakerPrecedence(@tiedchoices);
183             }
184             else {
185 13         90 return { 'winner' => 0, 'tie' => 1, 'tied' => \@tiedchoices };
186             }
187             }
188              
189             =head1 TieBreaker
190              
191             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.
192              
193             my @keep = $Election->TieBreaker( $tiebreaker, $active, @tiedchoices );
194              
195             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.
196              
197             =head1 Breaking Ties With Precedence
198              
199             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.
200              
201             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.
202              
203             my $Election = Vote::Count->new(
204             BallotSet => read_ballots('somefile'),
205             TieBreakMethod => 'precedence',
206             PrecedenceFile => '/path/to/precedencefile');
207              
208             =head2 Precedence (Method)
209              
210             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.
211              
212             my $RankCountByPrecedence = $Election->Precedence();
213             my $RankCountByPrecedence = $Election->Precedence( $active );
214              
215             =head2 CreatePrecedenceRandom
216              
217             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.
218              
219             # Generate a random precedence file
220             my @precedence = Vote::Count->new( BallotSet => read_ballots('somefile') )
221             ->CreatePrecedenceRandom( '/tmp/precedence.txt');
222             # Create a new Election with it.
223             my $Election = Vote::Count->new( BallotSet => read_ballots('somefile'),
224             PrecedenceFile => '/tmp/precedence.txt', TieBreakMethod => 'Precedence' );
225              
226             =head2 TieBreakerFallBackPrecedence
227              
228             This optional argument enables or disables using precedence as a fallback, generates /tmp/precedence.txt using CreatePrecedenceRandom if no PrecedenceFile is specified. Default is off (0).
229              
230             =head2 UnTieList
231              
232             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.
233              
234             my @orderedlosers = $Election->UnTieList(
235             'ranking1' => $Election->TieBreakMethod(), 'tied' => \@unorderedlosers );
236              
237             A second method may be provided.
238              
239             my @orderedlosers = $Election->UnTieList(
240             'ranking1' => 'TopCount', 'ranking2' => 'Borda', 'tied' => \@unorderedlosers );
241              
242             This method requires that Precedence be enabled either by having enabled TieBreakerFallBackPrecedence or by setting the TieBreakMethod to Precedence.
243              
244             =head2 UnTieActive
245              
246             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.
247              
248             my @untiedset = $Election->UnTieActive( 'ranking1' => 'TopCount', 'ranking2' => 'Approval');
249              
250             =head1 TopCount > Approval > Precedence
251              
252             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.
253              
254             To apply Top Count > Approval > Precedence you need to start with a random Precedence File, Untie the choices, and switch Precedence Files:
255              
256             use Path::Tiny;
257             # create your official initial precedence file
258             my $Election = Vote::Count->new(
259             BallotSet => read_ballots('our_ballots.txt'),
260             PrecedenceFile => 'our_official_initial_precedence.txt',
261             TieBreakMethod => 'Precedence',
262             );
263             # Create the new Precedence
264             my @newbreaker = $Election->UnTieList( 'ranking1' => 'TopCount', 'ranking2' => 'Approval');
265             local $" = ' > '; # set list separator to >
266             $Election->logv( "Setting Tie Break Order to: @newbreaker");
267             local $" = "\n" ; # set list separator to new line.
268             path( 'topapproveprecedence.txt')->spew( "@newbreaker" );
269             $Election->PrecedenceFile( 'topapproveprecedence.txt' );
270              
271              
272              
273             =cut
274              
275 269     269   400 sub _precedence_sort ( $I, @list ) {
  269         346  
  269         438  
  269         324  
276 269         421 my %ordered = ();
277 269         375 my $start = 0;
278 269 100       560 if ( defined $I->{'PRECEDENCEORDER'} ) {
279 248         1296 %ordered = $I->{'PRECEDENCEORDER'}->%*;
280             }
281             else {
282 21         656 for ( split /\n/, path( $I->PrecedenceFile() )->slurp() ) {
283 205         6759 $_ =~ s/\s//g; #strip out any accidental white space
284 205         407 $ordered{$_} = ++$start;
285             }
286 21         154 for my $c ( $I->GetChoices ) {
287 205 50       357 unless ( defined $ordered{$c} ) {
288 0         0 croak "Choice $c missing from precedence file\n";
289             }
290             }
291 21         79 $I->{'PRECEDENCEORDER'} = \%ordered;
292             }
293 269         773 my %L = map { $ordered{$_} => $_ } @list;
  723         1722  
294 269         1043 return ( map { $L{$_} } ( sort { $a <=> $b } keys %L ) );
  723         1849  
  820         1463  
295             }
296              
297 107     107 0 157 sub TieBreakerPrecedence ( $I, @tiedchoices ) {
  107         160  
  107         183  
  107         123  
298 107         238 my @list = $I->_precedence_sort(@tiedchoices);
299 107         618 return { 'winner' => $list[0], 'tie' => 0, 'tied' => [] };
300             }
301              
302 88     88 1 2742 sub CreatePrecedenceRandom ( $I, $outfile = '/tmp/precedence.txt' ) {
  88         162  
  88         194  
  88         241  
303 88         472 my @choices = $I->GetActiveList();
304 88         267 my %randomized = ();
305 88         2380 srand( $I->BallotSet()->{'votescast'} );
306 88         297 while (@choices) {
307 709         1018 my $next = shift @choices;
308 709         1334 my $random = int( rand(1000000) );
309 709 50       1309 if ( defined $randomized{$random} ) {
310             # collision, this choice needs to do again.
311 0         0 unshift @choices, ($next);
312             }
313             else {
314 709         1783 $randomized{$random} = $next;
315             }
316             }
317             my @precedence =
318 88         657 ( map { $randomized{$_} } sort { $a <=> $b } ( keys %randomized ) );
  709         1182  
  1480         2154  
319 88         535 path($outfile)->spew( join( "\n", @precedence ) . "\n" );
320 88         71103 $I->PrecedenceFile( $outfile );
321 88         926 return @precedence;
322             }
323              
324 381     381 0 5946 sub TieBreaker ( $I, $tiebreaker, $active, @tiedchoices ) {
  381         564  
  381         638  
  381         537  
  381         735  
  381         462  
325 39     39   348 no warnings 'uninitialized';
  39         88  
  39         35588  
326 381         776 $tiebreaker = lc $tiebreaker;
327 381 100       883 if ( $tiebreaker eq 'none' ) { return @tiedchoices }
  157         534  
328 224 100       593 if ( $tiebreaker eq 'all' ) { return () }
  17         53  
329 207         486 my $choices_hashref = { map { $_ => 1 } @tiedchoices };
  464         1072  
330 207         431 my $ranked = undef;
331 207 100       967 if ( $tiebreaker eq 'borda' ) {
    100          
    100          
    100          
    100          
    100          
    100          
332 3         12 $ranked = $I->Borda($active);
333             }
334             elsif ( $tiebreaker eq 'borda_all' ) {
335 3         95 $ranked = $I->Borda( $I->BallotSet()->{'choices'} );
336             }
337             elsif ( $tiebreaker eq 'approval' ) {
338 66         229 $ranked = $I->Approval($choices_hashref);
339             }
340             elsif ( $tiebreaker eq 'topcount' ) {
341 2         8 $ranked = $I->TopCount($choices_hashref);
342             }
343             elsif ( $tiebreaker eq 'topcount_active' ) {
344 2         9 $ranked = $I->TopCount($active);
345             }
346             elsif ( $tiebreaker eq 'grandjunction' ) {
347 64         205 my $GJ = $I->TieBreakerGrandJunction(@tiedchoices);
348 64 100       228 if ( $GJ->{'winner'} ) { return $GJ->{'winner'} }
  52 50       262  
349 12         75 elsif ( $GJ->{'tie'} ) { return $GJ->{'tied'}->@* }
350 0         0 else { croak "unexpected (or no) result from $tiebreaker!\n" }
351             }
352             elsif ( $tiebreaker eq 'precedence' ) {
353             # The one nice thing about precedence is that there is always a winner.
354 66         187 return $I->TieBreakerPrecedence(@tiedchoices)->{'winner'};
355             }
356 1         157 else { croak "undefined tiebreak method $tiebreaker!\n" }
357 76         146 my @highchoice = ();
358 76         104 my $highest = 0;
359 76         169 my $counted = $ranked->RawCount();
360 76         119 for my $c (@tiedchoices) {
361 178 100       350 if ( $counted->{$c} > $highest ) {
    100          
362 83         134 @highchoice = ($c);
363 83         137 $highest = $counted->{$c};
364             }
365             elsif ( $counted->{$c} == $highest ) {
366 80         142 push @highchoice, $c;
367             }
368             }
369 76         340 my $terse =
370             "Tie Breaker $tiebreaker: "
371             . join( ', ', @tiedchoices )
372             . "\nwinner(s): "
373             . join( ', ', @highchoice );
374 76         178 $I->{'last_tiebreaker'} = {
375             'terse' => $terse,
376             'verbose' => $ranked->RankTable(),
377             };
378 76 100 100     1818 if ( @highchoice > 1 && $I->TieBreakerFallBackPrecedence() ) {
379 36         117 my $winner = $I->TieBreakerPrecedence(@tiedchoices)->{'winner'};
380 36         122 $I->{'last_tiebreaker'}{'terse'} .= "\nWinner by Precedence: $winner";
381 36         251 return ( $winner );
382             }
383 40         267 return (@highchoice);
384             }
385              
386 38     38 1 71 sub Precedence ( $I, $active = undef ) {
  38         174  
  38         77  
  38         70  
387 38 100       181 $active = $I->Active() unless defined $active;
388 38         264 return Vote::Count::RankCount->newFromList(
389             $I->_precedence_sort( keys( $active->%* ) ) );
390             }
391              
392 20     20 0 91 sub precedence { Precedence(@_) }
393              
394 41     41   58 sub _shortuntie ( $I, $RC, @tied ) {
  41         63  
  41         64  
  41         80  
  41         70  
395 41         81 my %T = map { $_ => $RC->{$_} } @tied;
  133         281  
396 41         84 my @order = ();
397 41         119 while ( keys %T ) {
398 95         224 my $best = min values %T;
399 95         143 my @leaders = ();
400 95         208 for my $leader ( keys %T ) {
401 255 100       512 push @leaders, $leader if $T{$leader} == $best;
402             }
403 95         277 @leaders = $I->_precedence_sort(@leaders);
404 95         197 push @order, @leaders;
405 95         155 for (@leaders) { delete $T{$_} }
  133         292  
406             }
407 41         209 return @order;
408             }
409              
410 57     57 1 923 sub UnTieList ( $I, %args ) {
  57         110  
  57         205  
  57         94  
411 39     39   592 no warnings 'uninitialized';
  39         90  
  39         24046  
412 57 100 100     1916 unless ( $I->TieBreakerFallBackPrecedence()
413             or lc($I->TieBreakMethod) eq 'precedence' )
414             {
415 3         606 croak
416             "TieBreakerFallBackPrecedence must be enabled or TieBreakMethod must be precedence to use UnTieList [UnTieActive and BottomRunOff call it]";
417             }
418 54         165 my $ranking1 = $args{ranking1} ;
419 54   100     273 my $ranking2 = $args{ranking2} || 'Precedence';
420 54         208 my @tied = $args{tied}->@*;
421 54         157 my %tieactive = map { $_ => 1 } @tied;
  379         723  
422              
423 54         141 my @ordered = ();
424 54 100       217 return $I->_precedence_sort(@tied) if ( lc($ranking1) eq 'precedence' );
425 51     51   3658 my $RC1 = try { $I->$ranking1( \%tieactive )->HashByRank() }
426             catch {
427 2 100   2   35 my $mthstr = $ranking1 ? $ranking1 : "missing ranking1 . methods $ranking1 ? $ranking2 ";
428 2         314 croak "Unable to rank choices by $mthstr."
429 51         675 };
430 49     49   2373 my $RC2 = try {$I->$ranking2( \%tieactive )->HashWithOrder() }
431             catch {
432 1 50   1   15 my $mthstr = $ranking2 ? $ranking2 : "missing ranking2 . methods $ranking1 ? $ranking2 ";
433 1         162 croak "Unable to rank choices by $mthstr."
434 49         1595 };
435 48         1126 for my $level ( sort { $a <=> $b } ( keys $RC1->%* ) ) {
  238         420  
436 177         264 my @l = @{ $RC1->{$level} };
  177         385  
437 177         270 my @suborder = ();
438 177 100       476 if ( 1 == $RC1->{$level}->@* ) { @suborder = @l }
  110 100       192  
439             elsif ( $ranking2 eq 'precedence' ) {
440 26         127 @suborder = $I->_precedence_sort(@l);
441             }
442             else {
443 41         139 @suborder = $I->_shortuntie( $RC2, @l );
444             }
445 177         419 push @ordered, @suborder;
446             }
447 48         473 return @ordered;
448             }
449              
450 9     9 1 447 sub UnTieActive ( $I, %ARGS ) {
  9         16  
  9         42  
  9         18  
451 9         43 $ARGS{'tied'} = [ $I->GetActiveList() ];
452 9         50 $I->UnTieList( %ARGS );
453             }
454              
455             1;
456              
457             #FOOTER
458              
459             =pod
460              
461             BUG TRACKER
462              
463             L<https://github.com/brainbuz/Vote-Count/issues>
464              
465             AUTHOR
466              
467             John Karr (BRAINBUZ) brainbuz@cpan.org
468              
469             CONTRIBUTORS
470              
471             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
472              
473             LICENSE
474              
475             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>.
476              
477             SUPPORT
478              
479             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
480              
481             =cut
482