File Coverage

blib/lib/Vote/Count/IRV.pm
Criterion Covered Total %
statement 94 94 100.0
branch 23 24 95.8
condition n/a
subroutine 13 13 100.0
pod 2 2 100.0
total 132 133 99.2


line stmt bran cond sub pod time code
1 39     39   25690 use strict;
  39         99  
  39         1354  
2 39     39   219 use warnings;
  39         84  
  39         1284  
3 39     39   947 use 5.024;
  39         132  
4 39     39   208 use feature qw /postderef signatures/;
  39         81  
  39         4589  
5              
6             package Vote::Count::IRV;
7              
8 39     39   278 use namespace::autoclean;
  39         117  
  39         403  
9 39     39   3919 use Moose::Role;
  39         96  
  39         414  
10              
11             with 'Vote::Count::TopCount';
12             with 'Vote::Count::TieBreaker';
13              
14 39     39   208787 use Storable 3.15 'dclone';
  39         775  
  39         2817  
15              
16             our $VERSION='2.01';
17              
18             =head1 NAME
19              
20             Vote::Count::IRV
21              
22             =head1 VERSION 2.01
23              
24             =cut
25              
26             # ABSTRACT: IRV Method for Vote::Count
27              
28 39     39   242 no warnings 'experimental';
  39         80  
  39         1887  
29 39     39   264 use List::Util qw( min max );
  39         82  
  39         39073  
30             #use Data::Dumper;
31             # use Data::Printer;
32              
33 179     179   304 sub _ResolveTie ( $self, $active, $tiebreaker, @tiedchoices ) {
  179         256  
  179         249  
  179         296  
  179         364  
  179         247  
34 179 100       588 return @tiedchoices if @tiedchoices == 1;
35             my %high =
36 44         275 map { $_ => 1 } $self->TieBreaker( $tiebreaker, $active, @tiedchoices );
  56         161  
37 44 100       147 if ( defined $self->{'last_tiebreaker'} ) {
38 5         19 $self->logt( $self->{'last_tiebreaker'}{'terse'} );
39 5         17 $self->logv( $self->{'last_tiebreaker'}{'verbose'} );
40 5         11 $self->{'last_tiebreaker'} = undef;
41             }
42 44 100       146 if ( @tiedchoices == scalar( keys %high ) ) { return @tiedchoices }
  11         54  
43             # tiebreaker returns winner, we want losers!
44             # use map to remove winner(s) from @tiedchoices.
45             # warning about sort interpreted as function fixed
46             my @low = sort map {
47 33 100       68 if ( $high{$_} ) { }
  88         175  
48 70         154 else { $_ }
49             } @tiedchoices;
50 33         101 return @low;
51             }
52              
53 48     48 1 14685 sub RunIRV ( $self, $active = undef, $tiebreaker = undef ) {
  48         93  
  48         91  
  48         91  
  48         80  
54 48         243 $self->_IRVDO( active => $active, tiebreaker => $tiebreaker );
55             }
56              
57 4     4 1 42 sub RunBTRIRV ( $self, %args ) {
  4         9  
  4         9  
  4         8  
58 4 100       17 my $ranking2 = $args{'ranking2'} ? $args{'ranking2'} : 'precedence';
59 4         22 $self->_IRVDO( 'btr' => 1, ranking2 => $ranking2 );
60             }
61              
62             # RunIRV needed a new argument and was a long established method,
63             # so now it hands everything off to this private method that uses
64             # named arguments.
65 52     52   89 sub _IRVDO ( $self, %args ) {
  52         101  
  52         212  
  52         110  
66 52         148 local $" = ', ';
67 52 100       3307 my $active = defined $args{'active'} ? dclone $args{'active'} : dclone $self->Active() ;
68 52         161 my $tiebreaker = do {
69 52 100       590 if ( defined $args{'tiebreaker'} ) { $args{'tiebreaker'} }
  39 100       155  
70 5         128 elsif ( defined $self->TieBreakMethod() ) { $self->TieBreakMethod() }
71 8         23 else { 'all' }
72             };
73 52         112 my $roundctr = 0;
74 52         95 my $maxround = scalar( keys %{$active} );
  52         168  
75             $self->logt( "Instant Runoff Voting",
76 52         145 'Choices: ', join( ', ', ( sort keys %{$active} ) ) );
  52         598  
77             # forever loop normally ends with return from $majority
78             # a tie should be detected and also generate a
79             # return from the else loop.
80             # if something goes wrong roundcountr/maxround
81             # will generate exception.
82             IRVLOOP:
83 52         140 until (0) {
84 251         517 $roundctr++;
85 251 50       675 die "IRVLOOP infinite stopped at $roundctr" if $roundctr > $maxround;
86 251         1093 my $round = $self->TopCount($active);
87 251         1268 $self->logv( '---', "IRV Round $roundctr", $round->RankTable() );
88 251         1745 my $majority = $self->EvaluateTopCountMajority($round);
89 251 100       1002 if ( defined $majority->{'winner'} ) {
    100          
90 44         602 return $majority;
91             }
92             elsif ( $args{'btr'}) {
93             my $br = $self->BottomRunOff(
94 28         220 'active' => $active, 'ranking2' => $args{'ranking2'} );
95 28         171 $self->logv( $br->{'runoff'});
96 28         99 $self->logt( "Eliminating: ${\ $br->{'eliminate'} }" );
  28         189  
97 28         396 delete $active->{ $br->{'eliminate'} };
98             }
99             else { #--
100 179         612 my @bottom = $self->_ResolveTie( $active, $tiebreaker, $round->ArrayBottom()->@* );
101 179 100       309 if ( scalar(@bottom) == scalar( keys %{$active} ) ) {
  179         545  
102             # if there is a tie at the end, the finalists should
103             # be both top and bottom and the active set.
104 8         48 $self->logt( "Tied: @bottom" );
105 8         91 return { tie => 1, tied => \@bottom, winner => 0 };
106             }
107 171         1006 $self->logt( "Eliminating: @bottom" );
108 171         420 for my $b (@bottom) {
109 227         1523 delete $active->{$b};
110             }
111             } #--
112             }
113             }
114              
115             1;
116              
117             =pod
118              
119             =head1 IRV
120              
121             Implements Instant Runoff Voting for Vote::Count.
122              
123             =head1 SYNOPSIS
124              
125             use Vote::Count::Method;
126             use Vote::Count::ReadBallots 'read_ballots';
127              
128             my $Election = Vote::Count::->new(
129             BallotSet => read_ballots('%path_to_my_ballots'),
130             TieBreakMethod => 'grandjunction');
131              
132             my $result = $Election->RunIRV();
133             my $winner = $result->{'winner'};
134              
135             say $Election->logv(); # Print the full Log.
136              
137             =head1 Method Summary
138              
139             Instant Runoff Voting Looks for a Majority Winner. If one isn't present the choice with the lowest Top Count is removed.
140              
141             Instant Runoff Voting is easy to count by hand and meets the Later Harm and Condorcet Loser Criteria. It, unfortunately, fails a large number of consistency criteria; the order of candidate dropping matters and small changes to the votes of non-winning choices that result in changes to the dropping order can change the outcome.
142              
143             Instant Runoff Voting is also known as Alternative Vote and as the Hare Method.
144              
145             =head2 Tie Handling
146              
147             There is no standard accepted method for IRV tie resolution, Eliminate All is a common one and the default.
148              
149             Returns a tie when all of the remaining choices are in a tie.
150              
151             An optional value to RunIRV is to specify tiebreaker, see L<Vote::Count::TieBreaker>.
152              
153             =head2 RunIRV
154              
155             $Election->RunIRV();
156              
157             $Election->RunIRV( $active )
158              
159             $Election->RunIRV( $active, 'approval' )
160              
161             Runs IRV on the provided Ballot Set. Takes an optional parameter of $active which is a hashref for which the keys are the currently active choices.
162              
163             Returns results in a hashref which will be the results of Vote::Count::TopCount->EvaluateTopCountMajority, if there is no winner hash will instead be:
164              
165             tie => [true or false],
166             tied => [ array of tied choices ],
167             winner => a false value
168              
169             Supports the Vote::Count logt, logv, and logd methods for providing details of the method.
170              
171             =head2 RunBTRIRV
172              
173             This is the simplest modification to IRV which meets the Condorcet Winner Criteria. Instead of eliminating the low choice, the lowest two choices enter a virtual runoff, eliminating the loser. This is the easiest possible Hand Count Condorcet method, there will always be fewer pairings than choices. This method fails LNH, when there is no Condorcet Winner LNH can come into play for each runoff. BTR IRV will only eliminate a member of the Smith Set when both members of the runoff are in it, so it can never eliminate the final member of the Smith Set. BTR IRV meets both Condorcet Criteria and the Smith Criteria.
174              
175             my $result = $Election->RunBTRIRV();
176              
177             RunBTRIRV has no optional arguments. Choices are ordered by TopCount, ties for position are decided by Precedence. It is mandatory that either the TieBreakMethod is Precedence or TieBreakerFallBackPrecedence is True. See UnTieList in L<Vote::Count::TieBreaker>.
178              
179             The returned values and logging are the same as for RunIRV.
180              
181             =cut
182              
183             #FOOTER
184              
185             =pod
186              
187             BUG TRACKER
188              
189             L<https://github.com/brainbuz/Vote-Count/issues>
190              
191             AUTHOR
192              
193             John Karr (BRAINBUZ) brainbuz@cpan.org
194              
195             CONTRIBUTORS
196              
197             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
198              
199             LICENSE
200              
201             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>.
202              
203             SUPPORT
204              
205             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
206              
207             =cut
208