File Coverage

blib/lib/Vote/Count/TopCount.pm
Criterion Covered Total %
statement 120 120 100.0
branch 25 26 96.1
condition 3 3 100.0
subroutine 18 18 100.0
pod 4 5 80.0
total 170 172 98.8


line stmt bran cond sub pod time code
1 39     39   28699 use strict;
  39         212  
  39         1238  
2 39     39   211 use warnings;
  39         132  
  39         1196  
3 39     39   733 use 5.024;
  39         131  
4              
5 39     39   215 use feature qw /postderef signatures/;
  39         88  
  39         3837  
6              
7             package Vote::Count::TopCount;
8 39     39   235 use Moose::Role;
  39         89  
  39         299  
9              
10 39     39   194976 no warnings 'experimental';
  39         97  
  39         1817  
11 39     39   242 use List::Util qw( min max );
  39         84  
  39         3421  
12 39     39   1767 use Vote::Count::RankCount;
  39         97  
  39         1155  
13 39     39   252 use Vote::Count::TextTableTiny 'generate_table';
  39         78  
  39         2130  
14              
15 39     39   30996 use Math::BigRat try => 'GMP';
  39         3642911  
  39         1667  
16 39     39   52021 use Storable 'dclone';
  39         92  
  39         52388  
17              
18             # ABSTRACT: TopCount and related methods for Vote::Count. Toolkit for vote counting.
19              
20             our $VERSION='2.01';
21              
22             =head1 NAME
23              
24             Vote::Count::TopCount
25              
26             =head1 VERSION 2.01
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 14     14   25 sub _RangeTopCount ( $self, $active = undef ) {
  14         24  
  14         26  
  14         20  
61 14 100       73 $active = $self->Active() unless defined $active;
62 14         45 my %topcount = ( map { $_ => Math::BigRat->new(0) } keys( $active->%* ) );
  66         32651  
63             TOPCOUNTRANGEBALLOTS:
64 14         8866 for my $b ( $self->BallotSet()->{'ballots'}->@* ) {
65 64         8626 my $vv = dclone $b->{'votes'};
66 64         295 my %votes = $vv->%*;
67 64         179 for my $v ( keys %votes ) {
68 236 100       495 delete $votes{$v} unless defined $active->{$v};
69             }
70 64 100       150 next TOPCOUNTRANGEBALLOTS unless keys %votes;
71 61         179 my $max = max( values %votes );
72 61         105 my @top = ();
73 61         116 for my $c ( keys %votes ) {
74 182 100       345 if ( $votes{$c} == $max ) { push @top, $c }
  64         134  
75             }
76 61         275 my $topvalue = Math::BigRat->new( $b->{'count'} / scalar(@top) );
77 61         35913 for (@top) { $topcount{$_} += $topvalue }
  64         806  
78             }
79 14         1935 for my $k ( keys %topcount ) {
80 66         67933 $topcount{$k} = $topcount{$k}->as_float(5)->numify();
81             }
82 14         19290 return Vote::Count::RankCount->Rank( \%topcount );
83             }
84              
85 428     428   730 sub _RCVTopCount ( $self, $active = undef ) {
  428         769  
  428         714  
  428         646  
86 428         9963 my %ballotset = $self->BallotSet()->%*;
87 428         21482 my %ballots = ( $ballotset{'ballots'}->%* );
88 428 100       4402 $active = $self->Active() unless defined $active;
89 428         1432 my %topcount = ( map { $_ => 0 } keys( $active->%* ) );
  2358         4402  
90 428         1330 my %lasttopcount = ( map { $_ => 0 } keys( $active->%* ) );
  2358         3982  
91             TOPCOUNTBALLOTS:
92 428         5183 for my $b ( keys %ballots ) {
93             # reset topchoice so that if there is none the value will be false.
94 45415         70922 $ballots{$b}{'topchoice'} = 'NONE';
95 45415         94008 my @votes = $ballots{$b}->{'votes'}->@*;
96 45415         57720 for my $v (@votes) {
97 65571 100       104846 if ( defined $topcount{$v} ) {
98 43828         67548 $topcount{$v} += $ballots{$b}{'count'} * $ballots{$b}{'votevalue'};
99 43828         56730 $lasttopcount{$v} += $ballots{$b}{'count'};
100 43828         57599 $ballots{$b}{'topchoice'} = $v;
101 43828         69651 next TOPCOUNTBALLOTS;
102             }
103             }
104             }
105 428         18205 $self->LastTopCountUnWeighted( \%lasttopcount );
106 428         2871 return Vote::Count::RankCount->Rank( \%topcount );
107             }
108              
109 443     443 1 7411 sub TopCount ( $self, $active = undef ) {
  443         757  
  443         866  
  443         674  
110             # An STV method was performing a TopCount to reset the topchoices
111             # after elimination. Decided it was better to check here.
112 443 100 100     11430 unless( keys( $self->Active()->%* ) or defined( $active) ) {
113 1         8 return { 'error' => 'no active choices'};
114             }
115 442 100       10708 if ( $self->BallotSet()->{'options'}{'rcv'} == 1 ) {
    50          
116 428         1812 return $self->_RCVTopCount($active);
117             }
118             elsif ( $self->BallotSet()->{'options'}{'range'} == 1 ) {
119 14         61 return $self->_RangeTopCount($active);
120             }
121             }
122              
123 4     4 0 15 sub topcount { TopCount(@_) }
124              
125             =head2 TopChoice
126              
127             Returns the Top Choice on a specific ballot from the last TopCount operation. The ballot is identified by it's key in the ballotset.
128              
129             $Election->TopCount();
130             my $top = $Election->TopChoice( 'FOO:BAZ:BAR:ZAB');
131              
132             =cut
133              
134 11822     11822 1 14063 sub TopChoice( $self, $ballot ) {
  11822         13581  
  11822         14381  
  11822         12606  
135 11822         246206 return $self->BallotSet()->{ballots}{$ballot}{topchoice};
136             }
137              
138             =head2 TopCountMajority
139              
140             $self->TopCountMajority( $round_topcount )
141             or
142             $self->TopCountMajority( undef, $active_choices )
143              
144             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.
145              
146             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.
147              
148             =cut
149              
150 308     308 1 2450 sub TopCountMajority ( $self, $topcount = undef, $active = undef ) {
  308         471  
  308         571  
  308         509  
  308         434  
151 308 100       8509 $active = $self->Active() unless defined $active;
152 308 100       971 unless ( defined $topcount ) { $topcount = $self->TopCount($active) }
  26         117  
153 308         1209 my $topc = $topcount->RawCount();
154 308         1036 my $numvotes = $topcount->CountVotes();
155 308         950 my @choices = keys $topc->%*;
156 308         1173 my $threshold = 1 + int( $numvotes / 2 );
157 308         746 for my $t (@choices) {
158 1484 100       3030 if ( $topc->{$t} >= $threshold ) {
159             return (
160             {
161             votes => $numvotes,
162             threshold => $threshold,
163             winner => $t,
164 56         418 winvotes => $topc->{$t}
165             }
166             );
167             }
168             }
169             # No winner
170             return (
171             {
172 252         1450 votes => $numvotes,
173             threshold => $threshold,
174             }
175             );
176             }
177              
178             =head2 EvaluateTopCountMajority
179              
180             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.
181              
182             =cut
183              
184 298     298 1 599 sub EvaluateTopCountMajority ( $self, $topcount = undef, $active = undef ) {
  298         827  
  298         535  
  298         488  
  298         454  
185 298         1140 my $majority = $self->TopCountMajority( $topcount, $active );
186 298 100       926 if ( $majority->{'winner'} ) {
187 52         136 my $winner = $majority->{'winner'};
188             my $rows = [
189             [ 'Winner', $winner ],
190             [ 'Votes in Final Round', $majority->{'votes'} ],
191             [ 'Votes Needed for Majority', $majority->{'threshold'} ],
192 52         325 [ 'Winning Votes', $majority->{'winvotes'} ],
193             ];
194 52         239 $self->logt(
195             '---',
196             generate_table(
197             rows => $rows,
198             header_row => 0,
199             )
200             );
201             }
202 298         1500 return $majority;
203             }
204              
205             =pod
206              
207             =head1 Top Counting Range Ballots
208              
209             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.
210              
211             It is recommended to install Math::BigInt::GMP to improve performance on the Rational Number math used for Top Count on Range Ballots.
212              
213             =cut
214              
215             1;
216              
217             #FOOTER
218              
219             =pod
220              
221             BUG TRACKER
222              
223             L<https://github.com/brainbuz/Vote-Count/issues>
224              
225             AUTHOR
226              
227             John Karr (BRAINBUZ) brainbuz@cpan.org
228              
229             CONTRIBUTORS
230              
231             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
232              
233             LICENSE
234              
235             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>.
236              
237             SUPPORT
238              
239             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
240              
241             =cut
242