File Coverage

blib/lib/Vote/Count/IRV.pm
Criterion Covered Total %
statement 78 78 100.0
branch 19 20 95.0
condition n/a
subroutine 11 11 100.0
pod 1 1 100.0
total 109 110 99.0


line stmt bran cond sub pod time code
1 39     39   27694 use strict;
  39         108  
  39         1444  
2 39     39   231 use warnings;
  39         100  
  39         1336  
3 39     39   1037 use 5.024;
  39         1473  
4 39     39   278 use feature qw /postderef signatures/;
  39         95  
  39         5031  
5              
6             package Vote::Count::IRV;
7              
8 39     39   351 use namespace::autoclean;
  39         102  
  39         457  
9 39     39   4392 use Moose::Role;
  39         129  
  39         1331  
10              
11             with 'Vote::Count::TopCount';
12             with 'Vote::Count::TieBreaker';
13              
14 39     39   230760 use Storable 3.15 'dclone';
  39         796  
  39         2962  
15              
16             our $VERSION='2.00';
17              
18             =head1 NAME
19              
20             Vote::Count::IRV
21              
22             =head1 VERSION 2.00
23              
24             =cut
25              
26             # ABSTRACT: IRV Method for Vote::Count
27              
28 39     39   283 no warnings 'experimental';
  39         87  
  39         2254  
29 39     39   274 use List::Util qw( min max );
  39         108  
  39         36039  
30             #use Data::Dumper;
31              
32 179     179   315 sub _ResolveTie ( $self, $active, $tiebreaker, @tiedchoices ) {
  179         265  
  179         276  
  179         292  
  179         350  
  179         257  
33 179 100       630 return @tiedchoices if @tiedchoices == 1;
34             my %high =
35 44         241 map { $_ => 1 } $self->TieBreaker( $tiebreaker, $active, @tiedchoices );
  56         151  
36 44 100       136 if ( defined $self->{'last_tiebreaker'} ) {
37 5         23 $self->logt( $self->{'last_tiebreaker'}{'terse'} );
38 5         25 $self->logv( $self->{'last_tiebreaker'}{'verbose'} );
39 5         17 $self->{'last_tiebreaker'} = undef;
40             }
41 44 100       150 if ( @tiedchoices == scalar( keys %high ) ) { return @tiedchoices }
  11         48  
42             # tiebreaker returns winner, we want losers!
43             # use map to remove winner(s) from @tiedchoices.
44             # warning about sort interpreted as function fixed
45             my @low = sort map {
46 33 100       78 if ( $high{$_} ) { }
  88         179  
47 70         164 else { $_ }
48             } @tiedchoices;
49 33         116 return @low;
50             }
51              
52 48     48 1 16684 sub RunIRV ( $self, $active = undef, $tiebreaker = undef ) {
  48         114  
  48         99  
  48         87  
  48         80  
53             # external $active should not be changed.
54 48 100       136 if ( defined $active ) { $active = dclone $active }
  34         2404  
55             # Object's active is altered by IRV.
56 14         446 else { $active = dclone $self->Active() }
57 48 100       222 unless ( defined $tiebreaker ) {
58 9 100       303 if ( defined $self->TieBreakMethod() ) {
59 1         28 $tiebreaker = $self->TieBreakMethod();
60             }
61             else {
62 8         19 $tiebreaker = 'all';
63             }
64             }
65 48         118 my $roundctr = 0;
66 48         88 my $maxround = scalar( keys %{$active} );
  48         144  
67             $self->logt( "Instant Runoff Voting",
68 48         127 'Choices: ', join( ', ', ( sort keys %{$active} ) ) );
  48         571  
69             # forever loop normally ends with return from $majority
70             # a tie should be detected and also generate a
71             # return from the else loop.
72             # if something goes wrong roundcountr/maxround
73             # will generate exception.
74             IRVLOOP:
75 48         134 until (0) {
76 219         435 $roundctr++;
77 219 50       553 die "IRVLOOP infinite stopped at $roundctr" if $roundctr > $maxround;
78 219         852 my $round = $self->TopCount($active);
79 219         1011 $self->logv( '---', "IRV Round $roundctr", $round->RankTable() );
80 219         1480 my $majority = $self->EvaluateTopCountMajority($round);
81 219 100       676 if ( defined $majority->{'winner'} ) {
82 40         403 return $majority;
83             }
84             else {
85 179         630 my @bottom =
86             $self->_ResolveTie( $active, $tiebreaker, $round->ArrayBottom()->@* );
87 179 100       311 if ( scalar(@bottom) == scalar( keys %{$active} ) ) {
  179         535  
88             # if there is a tie at the end, the finalists should
89             # be both top and bottom and the active set.
90 8         62 $self->logt( "Tied: " . join( ', ', @bottom ) );
91 8         96 return { tie => 1, tied => \@bottom, winner => 0 };
92             }
93 171         885 $self->logv( "Eliminating: " . join( ', ', @bottom ) );
94 171         438 for my $b (@bottom) {
95 227         1549 delete $active->{$b};
96             }
97             }
98             }
99             }
100              
101             1;
102              
103             =pod
104              
105             =head1 IRV
106              
107             Implements Instant Runoff Voting for Vote::Count.
108              
109             =head1 SYNOPSIS
110              
111             use Vote::Count::Method;
112             use Vote::Count::ReadBallots 'read_ballots';
113              
114             my $Election = Vote::Count::->new(
115             BallotSet => read_ballots('%path_to_my_ballots'),
116             TieBreakMethod => 'grandjunction');
117              
118             my $result = $Election->RunIRV();
119             my $winner = $result->{'winner'};
120              
121             say $Election->logv(); # Print the full Log.
122              
123             =head1 Method Summary
124              
125             Instant Runoff Voting Looks for a Majority Winner. If one isn't present the choice with the lowest Top Count is removed.
126              
127             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.
128              
129             Instant Runoff Voting is also known as Alternative Vote and as the Hare Method.
130              
131             =head2 Tie Handling
132              
133             There is no standard accepted method for IRV tie resolution, Eliminate All is a common one and the default.
134              
135             Returns a tie when all of the remaining choices are in a tie.
136              
137             An optional value to RunIRV is to specify tiebreaker, see TieBreaker.
138              
139             =head2 RunIRV
140              
141             $Election->RunIRV();
142              
143             $Election->RunIRV( $active )
144              
145             $Election->RunIRV( $active, 'approval' )
146              
147             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.
148              
149             Returns results in a hashref which will be the results of Vote::Count::TopCount->EvaluateTopCountMajority, if there is no winner hash will instead be:
150              
151             tie => [true or false],
152             tied => [ array of tied choices ],
153             winner => a false value
154              
155             Supports the Vote::Count logt, logv, and logd methods for providing details of the method.
156              
157             =head2 TieBreaker
158              
159             Uses TieBreaker from the TieBreaker Role. The default is 'all', which is to not break ties. 'none' the default for the Matrix (Condorcet) Object should not be used for IRV.
160              
161             All was chosen as the module default because it is Later Harm safe. Modified Grand Junction is extremely resolvable and simple.
162              
163             In the event that the tie-breaker returns a tie eliminate all that remain tied is used, unless that would eliminate all choices, in which case the election returns a tie.
164              
165             =cut
166              
167             #FOOTER
168              
169             =pod
170              
171             BUG TRACKER
172              
173             L<https://github.com/brainbuz/Vote-Count/issues>
174              
175             AUTHOR
176              
177             John Karr (BRAINBUZ) brainbuz@cpan.org
178              
179             CONTRIBUTORS
180              
181             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
182              
183             LICENSE
184              
185             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>.
186              
187             SUPPORT
188              
189             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
190              
191             =cut
192