File Coverage

blib/lib/Vote/Count/TieBreaker.pm
Criterion Covered Total %
statement 207 210 98.5
branch 62 66 93.9
condition 3 3 100.0
subroutine 21 21 100.0
pod 2 6 33.3
total 295 306 96.4


line stmt bran cond sub pod time code
1 39     39   24593 use strict;
  39         97  
  39         1349  
2 39     39   234 use warnings;
  39         91  
  39         1044  
3 39     39   769 use 5.024;
  39         147  
4              
5 39     39   232 use feature qw /postderef signatures/;
  39         95  
  39         3715  
6              
7             package Vote::Count::TieBreaker;
8 39     39   261 use Moose::Role;
  39         77  
  39         334  
9              
10 39     39   211649 no warnings 'experimental';
  39         113  
  39         2035  
11 39     39   274 use List::Util qw( min max sum );
  39         93  
  39         3422  
12 39     39   306 use Path::Tiny;
  39         84  
  39         2133  
13 39     39   271 use Data::Dumper;
  39         87  
  39         2009  
14 39     39   274 use Vote::Count::RankCount;
  39         106  
  39         1050  
15 39     39   237 use Carp;
  39         118  
  39         63332  
16              
17             our $VERSION='2.00';
18              
19             =head1 NAME
20              
21             Vote::Count::TieBreaker
22              
23             =head1 VERSION 2.00
24              
25             =head1 Synopsis
26              
27             my $Election = Vote::Count->new(
28             BallotSet => $ballotsirvtie2,
29             TieBreakMethod => 'approval'
30             );
31              
32             =cut
33              
34             # ABSTRACT: TieBreaker object for Vote::Count. Toolkit for vote counting.
35              
36             =head1 Tie Breakers
37              
38             The most important thing for a Tie Breaker to do is it should use some reproducible difference in the Ballots to pick a winner from a Tie. The next thing it should do is make sense. Finally, the ideal Tie Breaker will resolve when there is any difference to be found. The only fully resolvable method is unfortunately Random, but that is not reproducable between runs. Precedence sets a fixed resolution order and can be used to make Random reproducible.
39              
40             TieBreakMethod is specified as an argument to Vote::Count->new(). The TieBreaker is called internally from the resolution method via the TieBreaker function, which requires the caller to pass its TieBreakMethod.
41              
42             =head1 TieBreakMethod argument to Vote::Count->new
43              
44             'approval'
45             'all' [ eliminate all tied choices ]
46             'borda' [ applies Borda Count to current Active set ]
47             'grandjunction' [ more resolveable than simple TopCount would be ]
48             'none' [ eliminate no choices ]
49             'precedence' [ requires also setting PrecedenceFile ]
50              
51             =head1 Grand Junction
52              
53             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.
54              
55             Because it is simple, and 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. It is the Vote::Count author's preferred Tie-Breaker.
56              
57             =head2 The (Standard) Grand Junction Method
58              
59             Only the Tie-Breaker variant is currently implemented in Vote::Count.
60              
61             =over
62              
63             =item 1
64              
65             Count the Ballots to determine the quota for a majority.
66              
67             =item 2
68              
69             Count the first choices and elect a choice which has a majority.
70              
71             =item 3
72              
73             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).
74              
75             =item 4
76              
77             Keep adding the next rank to the totals until either there is a winner or all ballots are exhausted.
78              
79             =item 5
80              
81             When all ballots are exhausted the choice with the highest total wins.
82              
83             =back
84              
85             =head2 As a Tie Breaker
86              
87             The Tie Breaker Method is modified.
88              
89             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.
90              
91             The winner is the last choice remaining.
92              
93             =head2 TieBreakerGrandJunction
94              
95             my $resolve = $Election->TieBreakerGrandJunction( $choice1, $choice2 [ $choice3 ... ] );
96             if ( $resolve->{'winner'}) { say "Tie Winner is $resolve->{'winner'}"}
97             elsif ( $resolve->{'tie'}) {
98             my @tied = $resolve->{'tied'}->@*;
99             say "Still tied between @tied."
100             }
101              
102             The Tie Breaking will be logged to the verbose log, any number of tied choices may be provided.
103              
104             =cut
105              
106             has 'TieBreakMethod' => (
107             is => 'rw',
108             isa => 'Str',
109             required => 0,
110             );
111              
112             # This is only used for the precedence tiebreaker and fallback!
113             has 'PrecedenceFile' => (
114             is => 'rw',
115             isa => 'Str',
116             required => 0,
117             trigger => \&_triggercheckprecedence,
118             );
119              
120             has 'TieBreakerFallBackPrecedence' => (
121             is => 'rw',
122             isa => 'Bool',
123             default => 0,
124             lazy => 0,
125             trigger => \&_triggercheckprecedence,
126             );
127              
128 65     65   4228 sub _triggercheckprecedence ( $I, $new, $old = undef ) {
  65         120  
  65         119  
  65         125  
  65         96  
129 65 100       1856 unless ( $I->PrecedenceFile() ) {
130 3         73 $I->PrecedenceFile('/tmp/precedence.txt');
131 3         63 $I->logt( "Generated FallBack TieBreaker Precedence Order: \n"
132             . join( ', ', $I->CreatePrecedenceRandom() ) );
133             }
134 65         1659 $I->{'PRECEDENCEORDER'} = undef; # clear cached if the file changes.
135             }
136              
137 68     68 1 8279 sub TieBreakerGrandJunction ( $self, @tiedchoices ) {
  68         108  
  68         160  
  68         96  
138 68         2004 my $ballots = $self->BallotSet()->{'ballots'};
139 68         160 my %current = ( map { $_ => 0 } @tiedchoices );
  165         389  
140 68         143 my $deepest = 0;
141 68         271 for my $b ( keys $ballots->%* ) {
142 734         1151 my $depth = scalar $ballots->{$b}{'votes'}->@*;
143 734 100       1244 $deepest = $depth if $depth > $deepest;
144             }
145 68         154 my $round = 1;
146 68         165 while ( $round <= $deepest ) {
147 128         532 $self->logv("Tie Breaker Round: $round");
148 128         442 for my $b ( keys $ballots->%* ) {
149 1350 100       2693 my $pick = $ballots->{$b}{'votes'}[ $round - 1 ] or next;
150 990 100       1825 if ( defined $current{$pick} ) {
151 222         407 $current{$pick} += $ballots->{$b}{'count'};
152             }
153             }
154 128         462 my $max = max( values %current );
155 128         467 for my $c ( sort @tiedchoices ) {
156 296         971 $self->logv("\t$c: $current{$c}");
157             }
158 128         417 for my $c ( sort @tiedchoices ) {
159 296 100       603 if ( $current{$c} < $max ) {
160 75         140 delete $current{$c};
161 75         218 $self->logv("Tie Breaker $c eliminated");
162             }
163             }
164 128         398 @tiedchoices = ( sort keys %current );
165 128 100       327 if ( 1 == @tiedchoices ) {
166 52         194 $self->logv("Tie Breaker Won By: $tiedchoices[0]");
167 52         323 return { 'winner' => $tiedchoices[0], 'tie' => 0, 'tied' => [] };
168             }
169 76         250 $round++;
170             }
171 16 100       546 if ( $self->TieBreakerFallBackPrecedence() ) {
172 3         10 $self->logv('Applying Precedence fallback');
173 3         11 return $self->TieBreakerPrecedence(@tiedchoices);
174             }
175             else {
176 13         88 return { 'winner' => 0, 'tie' => 1, 'tied' => \@tiedchoices };
177             }
178             }
179              
180             =head1 TieBreaker
181              
182             Implements some basic methods for resolving ties. The default value for IRV is 'all', and the default value for Matrix is 'none'. 'all' is inappropriate for Matrix, and 'none' is inappropriate for IRV.
183              
184             my @keep = $Election->TieBreaker( $tiebreaker, $active, @tiedchoices );
185              
186             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.
187              
188             =head1 Precedence
189              
190             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.
191              
192             The Precedence list takes the choices of the election one per line. Choices defeat any choice lower than them in the list. When Precedence is used an additional attribute must be specified for the Precedence List.
193              
194             my $Election = Vote::Count->new(
195             BallotSet => read_ballots('somefile'),
196             TieBreakMethod => 'precedence',
197             PrecedenceFile => '/path/to/precedencefile');
198              
199             A compound Tie Breaker can be created with a precedence list and any other methods that create an ordered list (Top Count, Approval, Borda), that can then be used for a new Precdence File. This is slight different than using Precedence as a fall back as the methods are normally checked against the current state, this variant only used the initial state.
200              
201             =head2 CreatePrecedenceRandom
202              
203             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.
204              
205             my @precedence = Vote::Count->new( BallotSet => read_ballots('somefile') )
206             ->CreatePrecedenceRandom( '/tmp/precedence.txt');
207              
208             =head2 TieBreakerFallBackPrecedence
209              
210             This optional argument enables or disables using precedence as a fallback, generates /tmp/precedence.txt if no PrecedenceFile is specified. Default is off.
211              
212             =head1 UntieList
213              
214             Sort a list in an order determined by a TieBreaker method, sorted in Descending Order. The TieBreaker must be a method that returns a RankCount object, Borda, TopCount, and Approval, Precedence. To guarrantee reliable resolution Precedence must be used or have been set for fallback.
215              
216             my @orderedlosers = $Election->UntieList( 'Approval', @unorderedlosers );
217              
218             =head1 UntieActive
219              
220             Produces a precedence list of all the active choices in the election. Takes a first and optional second method name, if one of the methods is not Precedence, TieBreakerPrecedence must be true. The methods may be TopCount, Approval, or any other method that returns a RankCount object. Returns a RankCount object (with the OrderedList method enabled).
221              
222             my $precedenceRankCount = $Election->UntieActive( 'TopCount', 'Approval');
223              
224             =cut
225              
226 34     34   46 sub _precedence_sort ( $I, @list ) {
  34         44  
  34         68  
  34         48  
227 34         58 my %ordered = ();
228 34         47 my $start = 0;
229 34 100       74 if ( defined $I->{'PRECEDENCEORDER'} ) {
230 26         119 %ordered = $I->{'PRECEDENCEORDER'}->%*;
231             }
232             else {
233 8         220 for ( split /\n/, path( $I->PrecedenceFile() )->slurp() ) {
234 82         2527 $_ =~ s/\s//g; #strip out any accidental white space
235 82         189 $ordered{$_} = ++$start;
236             }
237 8         68 for my $c ( $I->GetChoices ) {
238 82 50       139 unless ( defined $ordered{$c} ) {
239 0         0 croak "Choice $c missing from precedence file\n";
240             }
241             }
242 8         26 $I->{'PRECEDENCEORDER'} = \%ordered;
243             }
244 34         123 my %L = map { $ordered{$_} => $_ } @list;
  138         292  
245 34         132 return ( map { $L{$_} } ( sort { $a <=> $b } keys %L ) );
  138         263  
  185         259  
246             }
247              
248 14     14 0 38 sub TieBreakerPrecedence ( $I, @tiedchoices ) {
  14         24  
  14         31  
  14         20  
249 14         35 my @list = $I->_precedence_sort(@tiedchoices);
250 14         172 return { 'winner' => $list[0], 'tie' => 0, 'tied' => [] };
251             }
252              
253 38     38 1 2629 sub CreatePrecedenceRandom ( $I, $outfile = '/tmp/precedence.txt' ) {
  38         81  
  38         88  
  38         64  
254 38         206 my @choices = $I->GetActiveList();
255 38         122 my %randomized = ();
256 38         1345 srand( $I->BallotSet()->{'votescast'} );
257 38         130 while (@choices) {
258 325         532 my $next = shift @choices;
259 325         592 my $random = int( rand(1000000) );
260 325 50       633 if ( defined $randomized{$random} ) {
261             # collision, this choice needs to do again.
262 0         0 unshift @choices, ($next);
263             }
264             else {
265 325         888 $randomized{$random} = $next;
266             }
267             }
268             my @precedence =
269 38         284 ( map { $randomized{$_} } sort { $a <=> $b } ( keys %randomized ) );
  325         549  
  683         1015  
270 38         229 path($outfile)->spew( join( "\n", @precedence ) . "\n" );
271 38         37124 return @precedence;
272             }
273              
274 259     259 0 4126 sub TieBreaker ( $I, $tiebreaker, $active, @tiedchoices ) {
  259         386  
  259         413  
  259         364  
  259         526  
  259         352  
275 39     39   378 no warnings 'uninitialized';
  39         94  
  39         25579  
276 259 100       620 if ( $tiebreaker eq 'none' ) { return @tiedchoices }
  157         530  
277 102 100       260 if ( $tiebreaker eq 'all' ) { return () }
  17         60  
278 85         197 my $choices_hashref = { map { $_ => 1 } @tiedchoices };
  220         543  
279 85         182 my $ranked = undef;
280 85 100       439 if ( $tiebreaker eq 'borda' ) {
    100          
    100          
    100          
    100          
    100          
281 3         16 $ranked = $I->Borda($active);
282             }
283             elsif ( $tiebreaker eq 'borda_all' ) {
284 3         73 $ranked = $I->Borda( $I->BallotSet()->{'choices'} );
285             }
286             elsif ( $tiebreaker eq 'approval' ) {
287 12         51 $ranked = $I->Approval($choices_hashref);
288             }
289             elsif ( $tiebreaker eq 'topcount' ) {
290 2         17 $ranked = $I->TopCount($choices_hashref);
291             }
292             elsif ( $tiebreaker eq 'grandjunction' ) {
293 62         209 my $GJ = $I->TieBreakerGrandJunction(@tiedchoices);
294 62 100       205 if ( $GJ->{'winner'} ) { return $GJ->{'winner'} }
  50 50       278  
295 12         76 elsif ( $GJ->{'tie'} ) { return $GJ->{'tied'}->@* }
296 0         0 else { croak "unexpected (or no) result from $tiebreaker!\n" }
297             }
298             elsif ( $tiebreaker eq 'precedence' ) {
299             # The one nice thing about precedence is that there is always a winner.
300 2         7 return $I->TieBreakerPrecedence(@tiedchoices)->{'winner'};
301             }
302 1         128 else { croak "undefined tiebreak method $tiebreaker!\n" }
303 20         40 my @highchoice = ();
304 20         33 my $highest = 0;
305 20         66 my $counted = $ranked->RawCount();
306 20         50 for my $c (@tiedchoices) {
307 66 100       142 if ( $counted->{$c} > $highest ) {
    100          
308 24         45 @highchoice = ($c);
309 24         47 $highest = $counted->{$c};
310             }
311             elsif ( $counted->{$c} == $highest ) {
312 37         63 push @highchoice, $c;
313             }
314             }
315 20         106 my $terse =
316             "Tie Breaker $tiebreaker: "
317             . join( ', ', @tiedchoices )
318             . "\nwinner(s): "
319             . join( ', ', @highchoice );
320 20         65 $I->{'last_tiebreaker'} = {
321             'terse' => $terse,
322             'verbose' => $ranked->RankTable(),
323             };
324 20 100       86 if ( @highchoice > 1 ) {
325 11 100       383 if ( $I->TieBreakerFallBackPrecedence() ) {
326 8         33 return ( $I->TieBreakerPrecedence(@tiedchoices)->{'winner'} );
327             }
328             }
329 12         94 return (@highchoice);
330             }
331              
332 28     28 0 2531 sub UnTieList ( $I, $method, @tied ) {
  28         42  
  28         39  
  28         53  
  28         35  
333 39     39   351 no warnings 'uninitialized';
  39         93  
  39         25113  
334 28 100       82 return $I->_precedence_sort( @tied ) if ( lc($method) eq 'precedence' );
335 20 100 100     616 unless ( $I->TieBreakerFallBackPrecedence() or $I->TieBreakMethod eq 'precedence') {
336 2         306 croak
337             "TieBreakerFallBackPrecedence must be enabled or the specified method must be precedence to use UnTieList";
338             }
339 18 50       47 return @tied if scalar(@tied) == 1;
340 18         33 my @ordered = ();
341 18         39 my %active = ( map { $_ => 1 } @tied );
  52         99  
342             # method should be topcount borda or approval which all take argument of active.
343 18         71 my $RC = $I->$method(\%active)->HashByRank();
344              
345 18         92 for my $level ( sort { $a <=> $b } ( keys $RC->%* ) ) {
  22         58  
346 38         48 my @l = @{ $RC->{$level} };
  38         66  
347             my @suborder =
348 38         84 ( 1 == @{ $RC->{$level} } )
349 38 100       51 ? @{ $RC->{$level} }
  27         41  
350             : $I->_precedence_sort( @l );
351 38         80 push @ordered, @suborder;
352             }
353 18         78 return @ordered;
354             }
355              
356 14     14 0 6250 sub UntieActive ( $I, $method1, $method2='precedence' ) {
  14         23  
  14         24  
  14         21  
  14         23  
357 14 100       46 if ( lc($method1) eq 'precedence' ) {
358 1         6 return Vote::Count::RankCount->newFromList(
359             $I->_precedence_sort( $I->GetActiveList() ));
360             }
361 13         22 my $hasprecedence = 0;
362 13 100       459 $hasprecedence = 1 if 1 == $I->TieBreakerFallBackPrecedence();
363 13 100       39 $hasprecedence = 1 if lc($method2) eq 'precedence';
364 13 100       28 unless ($hasprecedence) {
365 1         185 croak
366             "TieBreakerFallBackPrecedence must be enabled or one of the specified methods must be precedence to use UntieActive";
367             }
368 12         23 my @ordered = ();
369 12         52 my $first = $I->$method1()->HashByRank();
370 12         55 for my $level ( sort { $a <=> $b } ( keys %{$first} ) ) {
  100         147  
  12         52  
371 64         84 my @l = @{ $first->{$level} };
  64         103  
372             my @suborder =
373 64         130 ( 1 == @{ $first->{$level} } )
374 64 100       90 ? @{ $first->{$level} }
  49         77  
375             : $I->UnTieList( $method2, @l );
376 64         129 push @ordered, @suborder;
377             }
378 12         48 return Vote::Count::RankCount->newFromList( @ordered );
379             }
380              
381             1;
382              
383             #FOOTER
384              
385             =pod
386              
387             BUG TRACKER
388              
389             L<https://github.com/brainbuz/Vote-Count/issues>
390              
391             AUTHOR
392              
393             John Karr (BRAINBUZ) brainbuz@cpan.org
394              
395             CONTRIBUTORS
396              
397             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
398              
399             LICENSE
400              
401             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>.
402              
403             SUPPORT
404              
405             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
406              
407             =cut
408