File Coverage

blib/lib/Vote/Count/Method/MinMax.pm
Criterion Covered Total %
statement 125 125 100.0
branch 24 24 100.0
condition n/a
subroutine 15 15 100.0
pod 3 3 100.0
total 167 167 100.0


line stmt bran cond sub pod time code
1 1     1   542 use strict;
  1         28  
  1         34  
2 1     1   6 use warnings;
  1         2  
  1         28  
3 1     1   17 use 5.024;
  1         4  
4 1     1   1841 use feature qw /postderef signatures/;
  1         3  
  1         126  
5              
6             package Vote::Count::Method::MinMax;
7              
8 1     1   7 use namespace::autoclean;
  1         2  
  1         7  
9 1     1   100 use Moose;
  1         2  
  1         6  
10             extends 'Vote::Count';
11              
12             our $VERSION='2.01';
13              
14             =head1 NAME
15              
16             Vote::Count::Method::MinMax
17              
18             =head1 VERSION 2.01
19              
20             =cut
21              
22             # ABSTRACT: Methods in the MinMax Family.
23              
24             =pod
25              
26             =head1 SYNOPSIS
27              
28             my $MinMaxElection =
29             Vote::Count::Method::MinMax->new( 'BallotSet' => $ballotset );
30              
31             # $method is one of: winning margin opposition
32             my $Winner = $MinMaxElection->MinMax( $method )->{'winner'};
33             say $MinMaxElection->logv();
34              
35             =head1 The MinMax Methods
36              
37             MinMax (also known as Minimax and Simpson-Kramer) uses a Pairwise comparison Matrix. Instead of looking at wins and losses as with Condorcet Methods, it scores each pairing, the choice with the lowest worst pairing score wins.
38              
39             =head2 The Three MinMax Scoring Rules
40              
41             =head3 Winning Votes ('winning')
42              
43             When the choice being scored loses, the votes for the winner in the pairing are scored. When the choice wins or ties the pairing is scored as 0.
44              
45             This scoring method meets the Condorcet Winner, but not the Smith, Condorcet Loser or Later Harm Criteria.
46              
47             =head3 Margin ('margin')
48              
49             When the choice being scored loses, the votes for the winner minus the votes for that choice in the pairing are scored. When the choice wins or ties the pairing is scored as 0.
50              
51             This scoring method meets the Condorcet Winner, but not the Smith, Condorcet Loser or Later Harm Criteria.
52              
53             =head3 Opposition ('opposition')
54              
55             The votes for the other choice in the pairing are scored regardless of whether the choice won or lost.
56              
57             This scoring method is claimed to meet the Later Harm Criteria, but fails Condorcet Winner and Smith.
58              
59             =head2 Tie Breaker
60              
61             As a Tie Breaker it is recommended to use the next worst pairing score. Because it follows the method and should resolve well, this Tie Breaker is implemented by Vote::Count within the MinMax method itself. If it returns a tie your implementation can apply another method like Modified Grand-Junction.
62              
63             =cut
64              
65 1     1   6113 no warnings 'experimental';
  1         2  
  1         41  
66              
67 1     1   5 use Vote::Count::TextTableTiny qw/generate_table/;
  1         2  
  1         64  
68 1     1   7 use Carp;
  1         1  
  1         56  
69 1     1   6 use Try::Tiny;
  1         1  
  1         1194  
70             # use Data::Dumper;
71              
72             =pod
73              
74             =head2 ScoreMinMax
75              
76             Generate hashref scoring according the requested $method which is one of three scoring rules: 'winning', 'margin', 'opposition'.
77              
78             my $scores = $MinMaxElection->ScoreMinMax( $method );
79              
80             =cut
81              
82 16     16 1 28925 sub ScoreMinMax ( $self, $method ) {
  16         32  
  16         25  
  16         22  
83 16         29 my $scores = {};
84             # Always grab the matrix by calling PairMatrix,
85             # build is lazy.
86 16         459 my $Matrix = $self->PairMatrix()->{'Matrix'};
87 16         342 my @choices = sort ( keys $self->Active()->%* );
88 16         43 for my $Choice (@choices) {
89 97         126 my @ChoiceLoss = ();
90 97         121 LOOPMMMO: for my $Opponent (@choices) {
91 643 100       919 next LOOPMMMO if $Opponent eq $Choice;
92 546         669 my $M = $Matrix->{$Choice}{$Opponent};
93 546         547 my $S = undef;
94 546 100       751 if ( $method eq 'opposition' ) {
    100          
95 274         322 $S = $M->{$Opponent};
96             }
97             elsif ( $M->{'winner'} eq $Opponent ) {
98 136 100       215 $S = $M->{$Opponent} if $method eq 'winning';
99 136 100       193 $S = $M->{$Opponent} - $M->{$Choice} if $method eq 'margin';
100             }
101             else {
102 136         143 $S = 0;
103             }
104 546         730 $scores->{$Choice}{$Opponent} = $S;
105             # there was a bug where sometimes @ChoiceLoss was sorted
106             # alphanumerically. resolution force the sort to be numeric.
107 546         713 push @ChoiceLoss, ( $S );
108             } # LOOPMMMO:
109             $scores->{$Choice}{score}
110 97         159 = [ reverse sort { $a <=> $b } @ChoiceLoss ];
  952         1147  
111             }
112 16         51 return $scores;
113             }
114              
115 8     8   12 sub _pairmatrixtable1 ( $I, $scores ) {
  8         12  
  8         8  
  8         12  
116 8         25 my @rows = ( [qw/Choice Choice Votes Opponent Votes Score/] );
117 8         167 my @choices = sort ( keys $I->Active()->%* );
118 8         171 my $Matrix = $I->PairMatrix()->{'Matrix'};
119 8         19 for my $Choice (@choices) {
120 47         78 push @rows, [$Choice];
121 47         59 for my $Opponent (@choices) {
122 305         337 my $Cstr = $Choice;
123 305         320 my $Ostr = $Opponent;
124 305 100       419 next if $Opponent eq $Choice;
125 258         315 my $CVote = $Matrix->{$Choice}{$Opponent}{$Choice};
126 258         301 my $OVote = $Matrix->{$Choice}{$Opponent}{$Opponent};
127 258 100       394 if ( $Matrix->{$Choice}{$Opponent}{'winner'} eq $Choice ) {
128 127         166 $Cstr = "**$Cstr**";
129             }
130 258 100       371 if ( $Matrix->{$Choice}{$Opponent}{'winner'} eq $Opponent ) {
131 127         178 $Ostr = "**$Ostr**";
132             }
133 258         306 my $Score = $scores->{$Choice}{$Opponent};
134 258         563 push @rows, [ ' ', $Cstr, $CVote, $Ostr, $OVote, $Score ];
135             }
136             }
137 8         30 return generate_table( rows => \@rows );
138             }
139              
140 8     8   15 sub _pairmatrixtable2 ( $I, $scores ) {
  8         16  
  8         10  
  8         16  
141 8         28 my @rows = ( [qw/Choice Scores/] );
142 8         288 my @choices = sort ( keys $I->Active()->%* );
143 8         23 for my $Choice (@choices) {
144 47         144 my $scores = join ', ', ( $scores->{$Choice}{'score'}->@* );
145 47         98 push @rows, [ $Choice, $scores ];
146             }
147 8         25 return generate_table( rows => \@rows );
148             }
149              
150             =pod
151              
152             =head2 MinMaxPairingVotesTable
153              
154             Generate a formatted table of the Pairing Matrix from a set of scores generated by ScoreMinMax.
155              
156             say $MinMaxElection->MinMaxPairingVotesTable( $scores );
157              
158             =cut
159              
160 8     8 1 15 sub MinMaxPairingVotesTable ( $I, $scores ) {
  8         14  
  8         9  
  8         14  
161 8         21 my $table1 = $I->_pairmatrixtable1($scores);
162 8         119 my $table2 = $I->_pairmatrixtable2($scores);
163 8         160 return "\n$table1\n\n$table2\n";
164             }
165              
166             =pod
167              
168             =head2 MinMax
169              
170             Run and log the election with MinMax according to scoring $method: 'winning', 'margin', 'opposition'.
171              
172             my $result = $MinMaxElection->MinMax( $method );
173              
174             The returned value is a HashRef:
175              
176             { 'tie' => true or false value,
177             'winner' => will be false if tie is true --
178             otherwise the winning choice.
179             # tied is only present when tie is true.
180             'tied' => [ array ref of tied choices ],
181             }
182              
183             =cut
184              
185 7     7 1 4583 sub MinMax ( $self, $method ) {
  7         12  
  7         10  
  7         8  
186 7         20 my $score = $self->ScoreMinMax($method);
187 7         43 my @active = $self->GetActiveList();
188 7         53 $self->logt( "MinMax $method Choices: ", join( ', ', @active ) );
189 7         25 $self->logv( $self->MinMaxPairingVotesTable($score) );
190 7         18 my $winner = '';
191 7         14 my @tied = ();
192 7         12 my $round = 0;
193             # round inited to 0. The 7th round is 6. round increments at
194             # end of the loop. this sets correct number of rounds.
195 7         17 my $roundlimit = scalar(@active) -1;
196 7         19 LOOPMINMAX: while ( $round < $roundlimit ) {
197             # start with $bestscore larger than any possible score
198 10         41 my $bestscore = $self->VotesCast() + 1;
199 10         18 my @hasbest = ();
200 10         23 for my $a (@active) {
201 49         81 my $this = $score->{$a}{'score'}[$round];
202 49 100       95 if ( $this == $bestscore ) { push @hasbest, $a }
  4 100       9  
203             elsif ( $this < $bestscore ) {
204 22         29 $bestscore = $this;
205 22         36 @hasbest = ($a);
206             }
207             }
208 10 100       25 if ( scalar(@hasbest) == 1 ) {
209 6         9 $winner = shift @hasbest;
210 6         29 $self->logt("Winner is $winner.");
211 6         103 return { 'tie' => 0, 'winner' => $winner };
212             }
213             # only choices that are tied continue to tie breaking.
214 4         10 @active = @hasbest;
215             # if this is the last round @tied must be set.
216 4         8 @tied = @hasbest;
217 4 100       10 if( $bestscore == 0 ) {
218 1         6 $self->logt(
219             "Tie between @tied. Best Score is 0. No more Tiebreakers available." );
220 1         3 last LOOPMINMAX;
221             }
222 3         6 $round++;
223 3         19 $self->logt(
224             "Tie between @tied. Best Score is $bestscore. Going to Tiebreaker Round $round."
225             );
226             }
227 1         7 $self->logt( "Tied: " . join( ', ', @tied ) );
228 1         24 return { 'tie' => 1, 'tied' => \@tied, 'winner' => 0 };
229             }
230              
231             =pod
232              
233             =head2 Floor Rules
234              
235             It is recommended to use a low Floor.
236              
237             This method specifies that the scores from less worst pairings be used as the tie breaker, removing inconsequential choices can affect the resolveability of the tie breaker. Unlike IRV where the presence of inconsequential choices can be seen as a randomizing factor, and their bulk removal as improving the consistency of the method, this method does not benefit from that.
238              
239             =cut
240              
241             1;
242              
243             #FOOTER
244              
245             =pod
246              
247             BUG TRACKER
248              
249             L<https://github.com/brainbuz/Vote-Count/issues>
250              
251             AUTHOR
252              
253             John Karr (BRAINBUZ) brainbuz@cpan.org
254              
255             CONTRIBUTORS
256              
257             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
258              
259             LICENSE
260              
261             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>.
262              
263             SUPPORT
264              
265             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
266              
267             =cut
268