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
|
|
|
|
|
|
|
|