File Coverage

blib/lib/Vote/Count/Matrix.pm
Criterion Covered Total %
statement 192 192 100.0
branch 44 50 88.0
condition n/a
subroutine 21 21 100.0
pod 0 6 0.0
total 257 269 95.5


line stmt bran cond sub pod time code
1 2     2   597 use strict;
  2         5  
  2         71  
2 2     2   11 use warnings;
  2         5  
  2         50  
3 2     2   44 use 5.022;
  2         5  
4 2     2   9 use feature qw /postderef signatures/;
  2         4  
  2         241  
5              
6             package Vote::Count::Matrix;
7             $Vote::Count::Matrix::VERSION = '0.007'; # TRIAL
8 2     2   12 use Moose;
  2         4  
  2         12  
9              
10 2     2   12397 no warnings 'experimental';
  2         4  
  2         76  
11 2     2   10 use List::Util qw( min max sum );
  2         3  
  2         141  
12 2     2   389 use TextTableTiny qw/generate_markdown_table/;
  2         4  
  2         98  
13              
14             # use Try::Tiny;
15 2     2   12 use Data::Printer;
  2         4  
  2         19  
16 2     2   144 use Data::Dumper;
  2         3  
  2         85  
17              
18 2     2   928 use YAML::XS;
  2         5124  
  2         4047  
19              
20             has BallotSet => (
21             is => 'ro',
22             required => 1,
23             isa => 'HashRef',
24             );
25              
26             has BallotSetType => (
27             is => 'ro',
28             isa => 'Str',
29             default => 'rcv',
30             );
31              
32             has Active => (
33             is => 'rw',
34             isa => 'HashRef',
35             builder => 'Vote::Count::Matrix::_buildActive',
36             lazy => 1,
37             );
38              
39 5     5   11 sub _buildActive ( $self ) {
  5         9  
  5         8  
40 5         123 return $self->BallotSet->{'choices'};
41             }
42              
43 569     569   556 sub _conduct_pair ( $ballotset, $A, $B ) {
  569         569  
  569         548  
  569         547  
  569         542  
44 569         695 my $ballots = $ballotset->{'ballots'};
45 569         562 my $countA = 0;
46 569         544 my $countB = 0;
47             FORVOTES:
48 569         1119 for my $b ( keys $ballots->%* ) {
49 4516         6069 for my $v ( values $ballots->{$b}{'votes'}->@* ) {
50 8905 100       13800 if ( $v eq $A ) {
    100          
51 1186         1285 $countA += $ballots->{$b}{'count'};
52 1186         1394 next FORVOTES;
53             }
54             elsif ( $v eq $B ) {
55 1211         1300 $countB += $ballots->{$b}{'count'};
56 1211         1454 next FORVOTES;
57             }
58             }
59             } # FORVOTES
60 569         1676 my %retval = (
61             $A => $countA,
62             $B => $countB,
63             'tie' => 0,
64             'winner' => '',
65             'loser' => '',
66             'margin' => abs( $countA - $countB )
67             );
68 569 100       1029 if ( $countA == $countB ) {
    100          
    50          
69 33         46 $retval{'winner'} = '';
70 33         40 $retval{'tie'} = 1;
71             }
72             elsif ( $countA > $countB ) {
73 224         279 $retval{'winner'} = $A;
74 224         254 $retval{'loser'} = $B;
75             }
76             elsif ( $countB > $countA ) {
77 312         371 $retval{'winner'} = $B;
78 312         355 $retval{'loser'} = $A;
79             }
80 569         805 return \%retval;
81             }
82              
83             sub BUILD {
84 29     29 0 37962 my $self = shift;
85 29         50 my $results = {};
86 29         771 my $ballotset = $self->BallotSet();
87 29         703 my @choices = keys $self->Active()->%*;
88 29         81 while ( scalar(@choices) ) {
89 179         235 my $A = shift @choices;
90 179         275 for my $B (@choices) {
91 567         756 my $result = Vote::Count::Matrix::_conduct_pair( $ballotset, $A, $B );
92             # Each result has two hash keys so it can be found without
93             # having to try twice or sort the names for a single key.
94 567         771 $results->{$A}{$B} = $result;
95 567         883 $results->{$B}{$A} = $result;
96             }
97             }
98 29         110 $self->{'Matrix'} = $results;
99             }
100              
101 97     97   8651 sub _scorematrix ( $self ) {
  97         111  
  97         101  
102 97         128 my $scores = {};
103 97         2400 my %active = $self->Active()->%*;
104 97         258 for my $A ( keys %active ) {
105 509         559 my $hasties = 0;
106 509         594 $scores->{$A} = 0;
107 509         882 for my $B ( keys %active ) {
108 3167 100       4043 next if $B eq $A;
109 2658 100       3993 if( $A eq $self->{'Matrix'}{$A}{$B}{'winner'} ) { $scores->{$A}++ }
  1282         1332  
110 2658 100       3892 if( $self->{'Matrix'}{$A}{$B}{'tie'} ) { $hasties = .001 }
  94         132  
111             }
112 509 100       875 if ( $scores->{$A} == 0 ) { $scores->{$A} += $hasties }
  58         104  
113             }
114 97         287 return $scores;
115             }
116              
117             # return the choice with fewest wins in matrix.
118 7     7 0 8 sub LeastWins ( $matrix ) {
  7         9  
  7         23  
119 7         11 my @lowest = ();
120 7         12 my %scored = $matrix->_scorematrix()->%*;
121 7         27 my $lowscore = min( values %scored );
122 7         20 for my $A ( keys %scored ) {
123 32 100       48 if ( $scored{ $A } == $lowscore ) {
124 19         29 push @lowest, $A;
125             }
126             }
127 7         27 return @lowest;
128             }
129              
130 23     23 0 2732 sub CondorcetLoser( $self ) {
  23         32  
  23         28  
131 23         36 my $unfinished = 1;
132 23         38 my $wordy = "Removing Condorcet Losers\n";
133 23         34 my @eliminated = ();
134             CONDORCETLOSERLOOP:
135 23         52 while ($unfinished) {
136 58         76 $unfinished = 0;
137 58         105 my $scores = $self->_scorematrix;
138 58         1210 my @alist = ( keys $self->Active()->%* );
139             # Check that tied choices at the top won't be
140             # eliminated. alist is looped over twice because we
141             # don't want to report the scores when the list is
142             # reduced to either a condorcet winner or tied situation.
143 58         107 for my $A (@alist) {
144 292 100       595 unless ( max( values $scores->%* ) ) {
145 1         3 last CONDORCETLOSERLOOP;
146             }
147             }
148 57         1995 $wordy .= YAML::XS::Dump($scores);
149 57         195 for my $A (@alist) {
150 198 100       375 if ( $scores->{$A} == 0 ) {
151 35         66 push @eliminated, ($A);
152 35         68 $wordy .= "Eliminationg Condorcet Loser: *$A*\n";
153 35         61 delete $self->{'Active'}{$A};
154 35         43 $unfinished = 1;
155 35         116 next CONDORCETLOSERLOOP;
156             }
157             }
158             }
159 23 100       80 my $elimstr =
160             scalar(@eliminated)
161             ? "Eliminated Condorcet Losers: " . join( ', ', @eliminated ) . "\n"
162             : "No Condorcet Losers Eliminated\n";
163             return {
164 23         160 verbose => $wordy,
165             terse => $elimstr,
166             eliminated => \@eliminated,
167             eliminations => scalar(@eliminated),
168             };
169             }
170              
171 30     30 0 2713 sub CondorcetWinner( $self ) {
  30         46  
  30         39  
172 30         64 my $scores = $self->_scorematrix;
173 30         92 my @choices = keys $scores->%*;
174             # # if there is only one choice left they win.
175             # if ( scalar(@choices) == 1 ) { return $choices[0]}
176 30         58 my $mustwin = scalar(@choices) -1;
177 30         48 my $winner = '';
178 30         73 for my $c (@choices) {
179 173 100       289 if ( $scores->{$c} == $mustwin) {
180 5         25 $winner .= $c;
181             }
182             }
183 30         158 return $winner;
184             }
185              
186 19     19   4496 sub _getsmithguessforchoice ( $h, $matrix ) {
  19         24  
  19         23  
  19         19  
187 19         22 my @winners = ($h);
188 19         66 for my $P ( keys $matrix->{$h}->%* ) {
189 141 100       310 if ( $matrix->{$h}{$P}{'winner'} eq $P ) {
    100          
190 63         77 push @winners, ($P);
191             }
192             elsif ( $matrix->{$h}{$P}{'tie'} ) {
193 5         7 push @winners, ($P);
194             }
195             }
196 19         41 return ( map { $_ => 1 } @winners );
  87         134  
197             }
198              
199 3     3 0 4761 sub SmithSet ( $self ) {
  3         6  
  3         5  
200 3         8 my $matrix = $self->{'Matrix'};
201 3         133 my @alist = ( keys $self->Active()->%* );
202 3         8 my $sets = {};
203 3         94 my $setcounts = {};
204             # my $shortest = scalar(@list);
205 3         7 for my $h (@alist) {
206 17         25 my %set = Vote::Count::Matrix::_getsmithguessforchoice( $h, $matrix );
207 17         27 $sets->{$h} = \%set;
208             # the keys of setcounts are the counts
209 17         46 $setcounts->{ scalar( keys %set ) }{$h} = 1;
210             }
211 3         5 my $proposal = {};
212 3         20 my $minset = min( keys( $setcounts->%* ) );
213 3         8 for my $h ( keys $setcounts->{$minset}->%* ) {
214 6         12 for my $k ( keys( $sets->{$h}->%* ) ) {
215 11         17 $proposal->{$k} = 1;
216             }
217             }
218 3         5 SMITHLOOP: while (1) {
219 4         44 my $cntchoice = scalar( keys $proposal->%* );
220 4         11 for my $h ( keys $proposal->%* ) {
221 11         12 $proposal = { %{$proposal}, %{ $sets->{$h} } };
  11         18  
  11         29  
222             }
223             # done when no choices get added on a pass through loop
224 4 100       12 if ( scalar( keys $proposal->%* ) == $cntchoice ) {
225 3         8 last SMITHLOOP;
226             }
227             }
228 3         19 return $proposal;
229             }
230              
231             # options may later be used to add rankcount objects
232             # from boorda, approval, and topcount.
233 24     24 0 33 sub MatrixTable ( $self, $options = {} ) {
  24         34  
  24         32  
  24         35  
234 24         70 my @header = ( 'Choice', 'Wins', 'Losses', 'Ties' );
235             my $o_topcount = defined $options->{'topcount'}
236 24 50       57 ? $options->{'topcount'} : 0;
237 24 50       48 push @header, 'Top Count' if $o_topcount;
238 24         615 my @active = sort ( keys $self->Active()->%* );
239 24         59 my @rows = ( \@header ); # [ 'Rank', 'Choice', 'TopCount']);
240 24         53 for my $A (@active) {
241 135         153 my $wins = 0;
242 135         126 my $ties = 0;
243 135         145 my $losses = 0;
244 135 50       185 my $topcount = $o_topcount ? $options->{'topcount'}{$A} : 0;
245             MTNEWROW:
246 135         162 for my $B (@active) {
247 913 100       1541 if ( $A eq $B ) { next MTNEWROW }
  135 100       201  
    100          
    50          
248             elsif ( $self->{'Matrix'}{$A}{$B}{'winner'} eq $A ) {
249 379         408 $wins++;
250             }
251             elsif ( $self->{'Matrix'}{$A}{$B}{'winner'} eq $B ) {
252 379         456 $losses++;
253             }
254             elsif ( $self->{'Matrix'}{$A}{$B}{'tie'} ) {
255 20         24 $ties++;
256             }
257             }
258 135         297 my @newrow = ( $A, $wins, $losses, $ties);
259 135 50       205 push @newrow, $topcount if $o_topcount ;
260 135         229 push @rows, \@newrow;
261             }
262 24         82 return generate_markdown_table( rows => \@rows );
263             }
264              
265             1;