File Coverage

blib/lib/Vote/Count/TopCount.pm
Criterion Covered Total %
statement 119 119 100.0
branch 25 26 96.1
condition 3 3 100.0
subroutine 17 17 100.0
pod 4 4 100.0
total 168 169 99.4


line stmt bran cond sub pod time code
1 39     39   29957 use strict;
  39         104  
  39         1343  
2 39     39   236 use warnings;
  39         89  
  39         1142  
3 39     39   934 use 5.024;
  39         160  
4              
5 39     39   238 use feature qw /postderef signatures/;
  39         104  
  39         3937  
6              
7             package Vote::Count::TopCount;
8 39     39   261 use Moose::Role;
  39         86  
  39         300  
9              
10 39     39   211284 no warnings 'experimental';
  39         108  
  39         2034  
11 39     39   280 use List::Util qw( min max );
  39         304  
  39         3665  
12 39     39   323 use Vote::Count::RankCount;
  39         159  
  39         1297  
13 39     39   285 use Vote::Count::TextTableTiny 'generate_table';
  39         108  
  39         2454  
14              
15 39     39   35371 use Math::BigRat try => 'GMP';
  39         3999186  
  39         286  
16 39     39   39827 use Storable 'dclone';
  39         117  
  39         56540  
17              
18             # ABSTRACT: TopCount and related methods for Vote::Count. Toolkit for vote counting.
19              
20             our $VERSION='2.00';
21              
22             =head1 NAME
23              
24             Vote::Count::TopCount
25              
26             =head1 VERSION 2.00
27              
28             =head1 Synopsis
29              
30             This Role is consumed by Vote::Count it provides TopCount and related Methods to Vote::Count objects.
31              
32             =head1 Definition of Top Count
33              
34             Top Count is tabulation of the Top Choice vote on each ballot. As choices are eliminated the first choice on some ballots will be removed, the next highest remaining choice becomes the Top Choice for that ballot. When all choices on a ballot are eliminated it becomes exhausted and is no longer counted.
35              
36             =head1 TopCount Methods
37              
38             =head2 TopCount
39              
40             Takes a hashref of active choices as an optional parameter, if one is not provided it uses the internal active list accessible via the ->Active() method, which itself defaults to the BallotSet's Choices list.
41              
42             Returns a L<RankCount|Vote::Count::RankCount> object containing the TopCount.
43              
44             TopCount supports both Ranked and Range Ballot Types.
45              
46             For RCV, TopCount respects weighting, 'votevalue' is defaulted to 1 by readballots. Integers or Floating point values may be used.
47              
48             =head2 LastTopCountUnWeighted
49              
50             Returns a hashref of the unweighted raw count from the last TopCount operation.
51              
52             =cut
53              
54             has 'LastTopCountUnWeighted' => (
55             is => 'rw',
56             isa => 'HashRef',
57             required => 0,
58             );
59              
60 9     9   16 sub _RangeTopCount ( $self, $active = undef ) {
  9         14  
  9         15  
  9         13  
61 9 100       66 $active = $self->Active() unless defined $active;
62 9         32 my %topcount = ( map { $_ => Math::BigRat->new(0) } keys( $active->%* ) );
  50         25441  
63             TOPCOUNTRANGEBALLOTS:
64 9         5533 for my $b ( $self->BallotSet()->{'ballots'}->@* ) {
65 44         5372 my $vv = dclone $b->{'votes'};
66 44         195 my %votes = $vv->%*;
67 44         117 for my $v ( keys %votes ) {
68 156 100       318 delete $votes{$v} unless defined $active->{$v};
69             }
70 44 100       100 next TOPCOUNTRANGEBALLOTS unless keys %votes;
71 41         105 my $max = max( values %votes );
72 41         65 my @top = ();
73 41         75 for my $c ( keys %votes ) {
74 118 100       219 if ( $votes{$c} == $max ) { push @top, $c }
  44         160  
75             }
76 41         169 my $topvalue = Math::BigRat->new( $b->{'count'} / scalar(@top) );
77 41         24752 for (@top) { $topcount{$_} += $topvalue }
  44         810  
78             }
79 9         1308 for my $k ( keys %topcount ) {
80 50         48081 $topcount{$k} = $topcount{$k}->as_float(5)->numify();
81             }
82 9         11260 return Vote::Count::RankCount->Rank( \%topcount );
83             }
84              
85 382     382   672 sub _RCVTopCount ( $self, $active = undef ) {
  382         656  
  382         687  
  382         586  
86 382         9109 my %ballotset = $self->BallotSet()->%*;
87 382         24697 my %ballots = ( $ballotset{'ballots'}->%* );
88 382 100       4877 $active = $self->Active() unless defined $active;
89 382         1421 my %topcount = ( map { $_ => 0 } keys( $active->%* ) );
  1978         4029  
90 382         1232 my %lasttopcount = ( map { $_ => 0 } keys( $active->%* ) );
  1978         3363  
91             TOPCOUNTBALLOTS:
92 382         5243 for my $b ( keys %ballots ) {
93             # reset topchoice so that if there is none the value will be false.
94 44929         77291 $ballots{$b}{'topchoice'} = 'NONE';
95 44929         102710 my @votes = $ballots{$b}->{'votes'}->@*;
96 44929         61148 for my $v (@votes) {
97 65083 100       112252 if ( defined $topcount{$v} ) {
98 43354         71718 $topcount{$v} += $ballots{$b}{'count'} * $ballots{$b}{'votevalue'};
99 43354         60452 $lasttopcount{$v} += $ballots{$b}{'count'};
100 43354         61699 $ballots{$b}{'topchoice'} = $v;
101 43354         72957 next TOPCOUNTBALLOTS;
102             }
103             }
104             }
105 382         17505 $self->LastTopCountUnWeighted( \%lasttopcount );
106 382         2205 return Vote::Count::RankCount->Rank( \%topcount );
107             }
108              
109 392     392 1 7562 sub TopCount ( $self, $active = undef ) {
  392         698  
  392         686  
  392         610  
110             # An STV method was performing a TopCount to reset the topchoices
111             # after elimination. Decided it was better to check here.
112 392 100 100     10163 unless( keys( $self->Active()->%* ) or defined( $active) ) {
113 1         8 return { 'error' => 'no active choices'};
114             }
115 391 100       9620 if ( $self->BallotSet()->{'options'}{'rcv'} == 1 ) {
    50          
116 382         1430 return $self->_RCVTopCount($active);
117             }
118             elsif ( $self->BallotSet()->{'options'}{'range'} == 1 ) {
119 9         37 return $self->_RangeTopCount($active);
120             }
121             }
122              
123             =head2 TopChoice
124              
125             Returns the Top Choice on a specific ballot from the last TopCount operation. The ballot is identified by it's key in the ballotset.
126              
127             $Election->TopCount();
128             my $top = $Election->TopChoice( 'FOO:BAZ:BAR:ZAB');
129              
130             =cut
131              
132 11822     11822 1 14318 sub TopChoice( $self, $ballot ) {
  11822         15287  
  11822         16313  
  11822         13635  
133 11822         266661 return $self->BallotSet()->{ballots}{$ballot}{topchoice};
134             }
135              
136             =head2 TopCountMajority
137              
138             $self->TopCountMajority( $round_topcount )
139             or
140             $self->TopCountMajority( undef, $active_choices )
141              
142             Will find the majority winner from the results of a topcount, or alternately may be given undef and a hashref of active choices and will topcount the ballotset for just those choices and then find the majority winner.
143              
144             Returns a hashref of results. It will always include the votes in the round and the threshold for majority. If there is a winner it will also include the winner and winvotes.
145              
146             =cut
147              
148 276     276 1 2458 sub TopCountMajority ( $self, $topcount = undef, $active = undef ) {
  276         405  
  276         451  
  276         411  
  276         367  
149 276 100       7531 $active = $self->Active() unless defined $active;
150 276 100       680 unless ( defined $topcount ) { $topcount = $self->TopCount($active) }
  26         101  
151 276         1044 my $topc = $topcount->RawCount();
152 276         779 my $numvotes = $topcount->CountVotes();
153 276         851 my @choices = keys $topc->%*;
154 276         928 my $threshold = 1 + int( $numvotes / 2 );
155 276         600 for my $t (@choices) {
156 1302 100       2544 if ( $topc->{$t} >= $threshold ) {
157             return (
158             {
159             votes => $numvotes,
160             threshold => $threshold,
161             winner => $t,
162 52         393 winvotes => $topc->{$t}
163             }
164             );
165             }
166             }
167             # No winner
168             return (
169             {
170 224         1272 votes => $numvotes,
171             threshold => $threshold,
172             }
173             );
174             }
175              
176             =head2 EvaluateTopCountMajority
177              
178             This method wraps TopCountMajority adding logging, the logging of which would be a lot of boiler plate in round oriented methods. It takes the same parameters and returns the same hashref.
179              
180             =cut
181              
182 266     266 1 522 sub EvaluateTopCountMajority ( $self, $topcount = undef, $active = undef ) {
  266         444  
  266         447  
  266         433  
  266         440  
183 266         937 my $majority = $self->TopCountMajority( $topcount, $active );
184 266 100       788 if ( $majority->{'winner'} ) {
185 48         119 my $winner = $majority->{'winner'};
186             my $rows = [
187             [ 'Winner', $winner ],
188             [ 'Votes in Final Round', $majority->{'votes'} ],
189             [ 'Votes Needed for Majority', $majority->{'threshold'} ],
190 48         281 [ 'Winning Votes', $majority->{'winvotes'} ],
191             ];
192 48         212 $self->logt(
193             '---',
194             generate_table(
195             rows => $rows,
196             header_row => 0,
197             )
198             );
199             }
200 266         761 return $majority;
201             }
202              
203             =pod
204              
205             =head1 Top Counting Range Ballots
206              
207             Since Range Ballots often allow ranking choices equally, those equal votes need to be split. The other option is to have a rule that assigns an order among the tied choices in a conversion to Ranked Ballots. To prevent Rounding errors in the addition on large sets the fractions are added as Rational Numbers. The totals are converted to floating point numbers with a precision of 5 places for display.
208              
209             It is recommended to install Math::BigInt::GMP to improve performance on the Rational Number math used for Top Count on Range Ballots.
210              
211             =cut
212              
213             1;
214              
215             #FOOTER
216              
217             =pod
218              
219             BUG TRACKER
220              
221             L<https://github.com/brainbuz/Vote-Count/issues>
222              
223             AUTHOR
224              
225             John Karr (BRAINBUZ) brainbuz@cpan.org
226              
227             CONTRIBUTORS
228              
229             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
230              
231             LICENSE
232              
233             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>.
234              
235             SUPPORT
236              
237             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
238              
239             =cut
240