File Coverage

blib/lib/Vote/Count/Charge.pm
Criterion Covered Total %
statement 306 327 93.5
branch 52 62 83.8
condition 1 3 33.3
subroutine 42 45 93.3
pod 23 27 85.1
total 424 464 91.3


line stmt bran cond sub pod time code
1 13     13   1187105 use strict;
  13         62  
  13         400  
2 13     13   71 use warnings;
  13         34  
  13         310  
3 13     13   227 use 5.024;
  13         47  
4 13     13   101 use feature qw /postderef signatures switch/;
  13         47  
  13         1378  
5              
6             use namespace::autoclean;
7 13     13   2283 use Moose;
  13         75116  
  13         92  
8 13     13   3583 extends 'Vote::Count';
  13         2281585  
  13         94  
9              
10             no warnings 'experimental::signatures';
11 13     13   97678 no warnings 'experimental::smartmatch';
  13         33  
  13         613  
12 13     13   79  
  13         26  
  13         395  
13             use Sort::Hash;
14 13     13   3573 use Data::Dumper;
  13         5386  
  13         649  
15 13     13   3569 use Time::Piece;
  13         37085  
  13         707  
16 13     13   6315 use Path::Tiny;
  13         109173  
  13         68  
17 13     13   5971 use Carp;
  13         71985  
  13         688  
18 13     13   91 use JSON::MaybeXS;
  13         28  
  13         664  
19 13     13   4569 use YAML::XS;
  13         49189  
  13         733  
20 13     13   3079 # use Storable 3.15 'dclone';
  13         19218  
  13         4872  
21              
22             our $VERSION='2.02';
23              
24             has 'Seats' => (
25             is => 'ro',
26             isa => 'Int',
27             required => 1,
28             );
29              
30             has 'FloorRule' => (
31             is => 'rw',
32             isa => 'Str',
33             default => '',
34             );
35              
36             has 'FloorThresshold' => (
37             is => 'ro',
38             isa => 'Num',
39             default => 0,
40             );
41              
42             my @choice_valid_states =
43             qw( elected pending defeated withdrawn active suspended );
44              
45             $I->{'choice_status'} = {};
46 31     31   60 $I->{'pending'} = [];
  31         65  
  31         327  
47 31         120 $I->{'elected'} = [];
48 31         102 $I->{'suspended'} = [];
49 31         83 $I->{'deferred'} = [];
50 31         79 $I->{'stvlog'} = [];
51 31         89 $I->{'stvround'} = 0;
52 31         79 for my $c ( $I->GetChoices() ) {
53 31         70 $I->{'choice_status'}->{$c} = {
54 31         193 state => 'hopeful',
55 268         1059 votes => 0,
56             };
57             }
58             if ( $I->WithdrawalList ) {
59             for my $w (path( $I->WithdrawalList )->lines({ chomp => 1})) {
60 31 100       891 $I->Withdraw($w) if defined $I->{'choice_status'}{$w};
61 1         21 }
62 4 100       219 }
63             }
64              
65             # Default tie breaking to Precedence,
66             # Force Precedence as fallback, and generate reproducible precedence
67             # file if one isn't provided.
68             no warnings 'uninitialized';
69             unless ( $I->TieBreakMethod() ) {
70 31     31   56 $I->logd('TieBreakMethod is undefined, setting to precedence');
  31         55  
  31         54  
71 13     13   105 $I->TieBreakMethod('precedence');
  13         29  
  13         43871  
72 31 50       829 }
73 31         206 if ( $I->TieBreakMethod ne 'precedence' ) {
74 31         806 $I->logv( 'Ties will be broken by: '
75             . $I->TieBreakMethod
76 31 50       812 . ' with a fallback of precedence' );
77 0         0 $I->TieBreakerFallBackPrecedence(1);
78             }
79             unless ( stat $I->PrecedenceFile ) {
80 0         0 my @order = $I->CreatePrecedenceRandom('/tmp/precedence.txt');
81             $I->PrecedenceFile('/tmp/precedence.txt');
82 31 50       882 $I->logv( "Order for Random Tie Breakers is: " . join( ", ", @order ) );
83 31         261 }
84 31         783 }
85 31         352  
86             my $ballots = $I->GetBallots();
87             for my $b ( keys $ballots->%* ) {
88             $ballots->{$b}->{'votevalue'} = $I->VoteValue();
89 32     32 1 57 $ballots->{$b}->{'topchoice'} = undef;
  32         122  
  32         51  
90 32         159 }
91 32         1810 }
92 10510         272191  
93 10510         18634  
94             my $self = shift;
95             unless ( $self->BallotSetType() eq 'rcv' ) {
96             croak "Charge only supports rcv Ballot Type";
97 0     0 1 0 }
  0         0  
  0         0  
  0         0  
98             $self->_setTieBreaks();
99             $self->ResetVoteValue();
100 32     32 0 92 $self->_init_choice_status();
101 32 100       166 $self->FloorRounding('down');
102 1         257 }
103              
104 31         142 =pod
105 31         174  
106 31         734 CountAbandoned
107 31         959  
108             =cut
109              
110             my @continuing = ( $I->Deferred(), $I->GetActiveList );
111             my $set = $I->GetBallots();
112             my %res = ( count_abandoned => 0, value_abandoned => 0, );
113             for my $k ( keys $set->%* ) {
114             if ( $set->{$k}{'votevalue'} == 0 ) {
115             $res{count_abandoned} += $set->{$k}{'count'};
116 28     28 0 4083 next;
  28         51  
  28         44  
117 28         125 }
118 28         136 my $continue = 0;
119 28         146 for my $c (@continuing) {
120 28         2521 $continue += ( grep /$c/, $set->{$k}{'votes'}->@* );
121 17412 50       45213 }
122 0         0 unless ($continue) {
123 0         0 $res{count_abandoned} += $set->{$k}{'count'};
124             $res{value_abandoned} += $set->{$k}{'count'} * $set->{$k}{'votevalue'};
125 17412         22933 }
126 17412         25388 }
127 82806         888698 $res{message} =
128             "Votes with no Choice left: $res{count_abandoned}, Value: $res{value_abandoned}";
129 17412 100       45759 return \%res;
130 631         1304 }
131 631         1346  
132             if ($choice) { return $I->{'choice_status'}{$choice} }
133             else { return $I->{'choice_status'} }
134             }
135 28         1418  
136 28         286 if ( $status->{'state'} ) {
137             unless ( grep ( /^$status->{'state'}$/, @choice_valid_states ) ) {
138             croak "invalid state *$status->{'state'}* assigned to choice $choice";
139 29     29 1 5863 }
  29         50  
  29         58  
  29         41  
140 29 100       76 $I->{'choice_status'}->{$choice}{'state'} = $status->{'state'};
  24         121  
141 5         22 }
142             if ( $status->{'votes'} ) {
143             $I->{'choice_status'}->{$choice}{'votes'} = int $status->{'votes'};
144 12     12 1 30 }
  12         24  
  12         20  
  12         23  
  12         21  
145 12 100       44 }
146 2 50       45  
147 0         0 my $active = {};
148             for my $k ( keys $I->GetChoiceStatus()->%* ) {
149 2         8 $active->{$k} = 1 if $I->{'choice_status'}->{$k}{'state'} eq 'hopeful';
150             # $active->{$k} = 1 if $I->{'choice_status'}->{$k}{'state'} eq 'pending';
151 12 100       34 }
152 11         40 $I->SetActive($active);
153             }
154              
155             delete $I->{'Active'}{$choice};
156 3     3 1 10 $I->{'choice_status'}->{$choice}{'state'} = 'elected';
  3         6  
  3         6  
157 3         7 $I->{'pending'} = [ grep ( !/^$choice$/, $I->{'pending'}->@* ) ];
158 3         9 push $I->{'elected'}->@*, $choice;
159 24 100       52 return $I->{'elected'}->@*;
160             }
161              
162 3         17  
163             delete $I->{'Active'}{$choice};
164             $I->{'choice_status'}->{$choice}{'state'} = 'defeated';
165 39     39 1 3906 }
  39         73  
  39         79  
  39         86  
166 39         113  
167 39         118 my @defeated = ();
168 39         163 for my $c ( keys $I->{'choice_status'}->%* ) {
169 39         119 if ( $I->{'choice_status'}{$c}{'state'} eq 'defeated') {
170 39         164 push @defeated, $c;
171             }
172             }
173 21     21 1 66 return sort(@defeated);
  21         41  
  21         40  
  21         128  
174             }
175 23     23 1 5524  
  23         48  
  23         44  
  23         44  
176 23         59 my @withdrawn = ();
177 23         86 for my $c ( keys $I->{'choice_status'}->%* ) {
178             if ( $I->{'choice_status'}{$c}{'state'} eq 'withdrawn') {
179             push @withdrawn, $c;
180 0     0 1 0 }
  0         0  
  0         0  
181 0         0 }
182 0         0 return sort(@withdrawn);
183 0 0       0 }
184 0         0  
185             delete $I->{'Active'}{$choice};
186             $I->{'choice_status'}->{$choice}{'state'} = 'withdrawn';
187 0         0 return $I->Withdrawn();
188             }
189              
190 12     12 1 488 delete $I->{'Active'}{$choice};
  12         17  
  12         15  
191 12         18 $I->{'choice_status'}->{$choice}{'state'} = 'suspended';
192 12         39 unless ( grep ( /^$choice$/, $I->{'suspended'}->@* ) ) {
193 128 100       230 push $I->{'suspended'}->@*, $choice;
194 25         36 }
195             return $I->Suspended();
196             }
197 12         40  
198             return $I->{'suspended'}->@*;
199             }
200 9     9 1 14  
  9         12  
  9         12  
  9         12  
201 9         12 delete $I->{'Active'}{$choice};
202 9         17 $I->{'choice_status'}->{$choice}{'state'} = 'deferred';
203 9         18 unless ( grep ( /^$choice$/, $I->{'deferred'}->@* ) ) {
204             push $I->{'deferred'}->@*, $choice;
205             }
206 8     8 1 27 return $I->Deferred();
  8         14  
  8         13  
  8         10  
207 8         20 }
208 8         19  
209 8 100       93 return $I->{'deferred'}->@*;
210 7         20 }
211              
212 8         26 if ($choice) {
213             unless ( grep /^$choice$/, $I->{'pending'}->@* ) {
214             $I->{'choice_status'}->{$choice}{'state'} = 'pending';
215 11     11 1 14 push $I->{'pending'}->@*, $choice;
  11         16  
  11         17  
216 11         41 delete $I->{'Active'}{$choice};
217             }
218             }
219 2     2 1 5 return $I->{'pending'}->@*;
  2         4  
  2         4  
  2         5  
220 2         5 }
221 2         8  
222 2 50       10 # if no choices are given reinstate all.
223 2         9 @choices = ($I->{'suspended'}->@*, $I->{'deferred'}->@* ) unless @choices;
224             my @reinstated = ();
225 2         9 REINSTATELOOP:
226             for my $choice (@choices) {
227             # I'm a fan of the give/when construct, but go to lengths not to use it
228 31     31 1 59 # because of past issues and that after 15 years it is still experimental.
  31         55  
  31         48  
229 31         157 given ($I->{'choice_status'}{$choice}{'state'}){
230             when ( 'suspended') { }
231             when ( 'deferred' ) { }
232 2     2 1 5 default { next REINSTATELOOP }
  2         4  
  2         5  
  2         3  
233 2 100       6 };
234 1 50       5 ($I->{'suspended'}->@*) = grep ( !/^$choice$/, $I->{'suspended'}->@* );
235 1         4 ($I->{'deferred'}->@*) = grep ( !/^$choice$/, $I->{'deferred'}->@* );
236 1         3 $I->{'choice_status'}->{$choice}{'state'} = 'hopeful';
237 1         3 $I->{'Active'}{$choice} = 1;
238             push @reinstated, $choice;
239             }
240 2         13 return @reinstated;
241             }
242              
243 4     4 1 9 my $charged = 0;
  4         8  
  4         9  
  4         5  
244             my $surplus = 0;
245 4 100       20 my @ballotschrgd = ();
246 4         9 my $cntchrgd = 0;
247             my $active = $I->Active();
248 4         7 my $ballots = $I->BallotSet()->{'ballots'};
249             # warn Dumper $ballots;
250             CHARGECHECKBALLOTS:
251 6         14 for my $B ( keys $ballots->%* ) {
252 6         20 next CHARGECHECKBALLOTS if ( $I->TopChoice($B) ne $choice );
253 2         4 my $ballot = $ballots->{$B};
254 1         2 if ( $charge == 0 ) {
  1         4  
255             $charged += $ballot->{'votevalue'} * $ballot->{'count'};
256 5         75 $ballot->{'charged'}{$choice} = $ballot->{'votevalue'};
257 5         34 $ballot->{'votevalue'} = 0;
258 5         11 }
259 5         9 elsif ( $ballot->{'votevalue'} >= $charge ) {
260 5         12 my $over = $ballot->{'votevalue'} - $charge;
261             $charged += ( $ballot->{'votevalue'} - $over ) * $ballot->{'count'};
262 4         16 $ballot->{'votevalue'} -= $charge;
263             $ballot->{'charged'}{$choice} = $charge;
264             }
265 22     22 1 5611 else {
  22         47  
  22         45  
  22         43  
  22         288  
  22         49  
266 22         54 $charged += $ballot->{'votevalue'} * $ballot->{'count'};
267 22         42 $ballot->{'charged'}{$choice} = $ballot->{'votevalue'};
268 22         48 $ballot->{'votevalue'} = 0;
269 22         39 }
270 22         644 push @ballotschrgd, $B;
271 22         612 $cntchrgd += $ballot->{'count'};
272             }
273             $I->{'choice_status'}->{$choice}{'votes'} += $charged;
274 22         2436 $surplus = $I->{'choice_status'}->{$choice}{'votes'} - $quota;
275 11820 100       24925 $I->{'choice_status'}->{$choice}{'votes'} = $charged;
276 2501         4084 return (
277 2501 100       5979 {
    100          
278 1         3 choice => $choice,
279 1         3 surplus => $surplus,
280 1         2 ballotschrgd => \@ballotschrgd,
281             cntchrgd => $cntchrgd,
282             quota => $quota
283 2344         3572 }
284 2344         4050 );
285 2344         3035 }
286 2344         5609  
287             return $I->{'stvlog'} unless $data;
288             push $I->{'stvlog'}->@*, $data;
289 156         302 }
290 156         357  
291 156         230 my $jsonpath = $I->LogTo . '_stvevents.json';
292             my $yamlpath = $I->LogTo . '_stvevents.yaml';
293 2501         4936 # my $yaml = ;
294 2501         4253 my $coder = JSON->new->ascii->pretty;
295             path($jsonpath)->spew( $coder->encode( $I->STVEvent() ) );
296 22         882 path($yamlpath)->spew( Dump $I->STVEvent() );
297 22         61 }
298 22         56  
299              
300              
301 22         240 my $tc = $I->TopCount;
302             $tc->{'total_votes'} = $I->VotesCast;
303             $tc->{'total_vote_value'} = $tc->{'total_votes'} * $I->VoteValue;
304             $tc->{'abandoned'} = $I->CountAbandoned;
305             $tc->{'active_vote_value'} =
306             $tc->{'total_vote_value'} - $tc->{'abandoned'}{'value_abandoned'};
307             return $tc;
308             }
309              
310 24     24 1 183 if ( $I->FloorRule() && $I->FloorThresshold() ) {
  24         51  
  24         52  
  24         46  
311 24 100       831 $I->FloorRule('ApprovalFloor') if $I->FloorRule() eq 'Approval';
312 19         79 $I->FloorRule('TopCountFloor') if $I->FloorRule() eq 'TopCount';
313             my @withdrawn =();
314             my $newactive =
315 2     2 1 525 $I->ApplyFloor(
  2         5  
  2         4  
316 2         73 $I->FloorRule(),
317 2         51 $I->FloorThresshold()
318             );
319 2         44 for my $choice (sort $I->GetChoices()) {
320 2         40 unless( $newactive->{$choice}) {
321 2         1285 $I->$action( $choice );
322             push @withdrawn, $choice;
323             }
324 0     0 1 0 }
  0         0  
  0         0  
  0         0  
325             @withdrawn = sort (@withdrawn);
326 12     12 1 27 my $done = $action;
  12         25  
  12         20  
  12         46  
327             $done = 'Withdrawn' if $action eq 'Withdraw';
328 2     2 0 8 $done = 'Defeated' if $action eq 'Defeat';
  2         3  
  2         2  
329 2         8 return @withdrawn;
330 2         9 }
331 2         41 }
332 2         15  
333             my $abandoned = $I->CountAbandoned();
334 2         9 my $abndnvotes = $abandoned->{'value_abandoned'};
335 2         15 my $cast = $I->BallotSet->{'votescast'};
336             my $numerator = ( $cast * $I->VoteValue ) - $abndnvotes;
337             my $denominator = $I->Seats();
338 2     2 0 11 my $adjust = 0;
  2         3  
  2         3  
  2         4  
339 2 50 33     47 if ( $style eq 'droop' ) {
340 2 100       41 $denominator++;
341 2 100       41 $adjust = 1;
342 2         4 }
343 2         41 return ( $adjust + int( $numerator / $denominator ) );
344             }
345              
346             =head1 NAME
347              
348 2         6 Vote::Count::Charge
349 24 100       36  
350 9         21 =head1 VERSION 2.02
351 9         13  
352             =cut
353              
354 2         7 # ABSTRACT: Vote::Charge - implementation of STV.
355 2         3  
356 2 100       5 =pod
357 2 100       5  
358 2         15 =head1 SYNOPSIS
359              
360             my $E = Vote::Count::Charge->new(
361             Seats => 3,
362 9     9 1 58 VoteValue => 1000,
  9         18  
  9         22  
  9         11  
363 9         38 BallotSet => read_ballots('t/data/data1.txt', ) );
364 9         27  
365 9         350 $E->Elect('SOMECHOICE');
366 9         253 $E->Charge('SOMECHOICE', $quota, $perCharge );
367 9         229 say E->GetChoiceStatus( 'CARAMEL'),
368 9         23 > { state => 'withdrawn', votes => 0 }
369 9 100       33  
370 7         13 =head1 Vote Charge implementation of Surplus Transfer
371 7         16  
372             Vote Charge is how Vote::Count implements Surplus Transfer. The wording is chosen to make the concept more accessible to a general audience. It also uses integer math and imposes truncation as the rounding rule.
373 9         78  
374             Vote Charge describes the process of Single Transferable Vote as:
375              
376             The Votes are assigned a value, based on the number of seats and the total value of all of the votes, a cost is determined for electing a choice. The votes supporting that choice are then charged to pay that cost. The remainder of the value for the vote, if any, is available for the next highest choice of the vote.
377              
378             When value is transferred back to the vote, Vote Charge refers to it as a Rebate.
379              
380             Vote Charge uses integer math and truncates all remainders. Setting the Vote Value is equivalent to setting a number of decimal places, a Vote Value of 100,000 is the same as a 5 decimal place precision.
381              
382             =head1 Description
383              
384             This module provides methods that can be shared between Charge implementations and does not present a complete tool for conducting STV elections. Look at the Methods that have been implemented as part of Vote::Count.
385              
386             =head1 Candidate / Choices States
387              
388             Single Transferable Vote rules have more states than Active, Eliminated and Elected. Not all methods need all of the possible states. The SetChoiceStatus method is not linked to the underlying Vote::Count objects Active Set, the action methods: Elect, Defeat, Suspend, Defer, Reinstate, Withdraw do update the Active Set.
389              
390             Active choices are referred to as Hopeful. The normal methods for accessing the Active list are available. Although not prevented from doing so, STV Methods should not directly set the active list, but rely on methods that manipulate candidate state. The VCUpdateActive method will sync the Active set with the STV choice states corresponding to active.
391              
392             =over
393              
394             =item *
395              
396             hopeful: The default active state of a choice.
397              
398             =item *
399              
400             withdrawn: A choice that will be treated as not present.
401              
402             =item *
403              
404             defeated: A choice that will no longer be considered for election.
405              
406             =item *
407              
408             deferred and suspended:
409              
410             A choice that is temporarily removed from consideration. Suspended is treated the same as Defeated, but is eligible for reinstatement. Deferred is removed from the ActiveSet, but treated as present when calculating Quota and Non-Continuing Votes.
411              
412             =item *
413              
414             elected and pending:
415              
416             Elected and Pending choices are removed from the Active Set, but Pending choices are not yet considered elected. The Pending state is available to hold newly elected choices for a method that will not immediately complete processing their election.
417              
418             =back
419              
420             =head3 GetChoiceStatus
421              
422             When called with the argument of a Choice, returns a hashref with the keys 'state' and 'votes'. When called without argument returns a hashref with the Choices as keys, and the values a hashref with the 'state' and 'votes' keys.
423              
424             =head3 SetChoiceStatus
425              
426             Takes the arguments of a Choice and a hashref with the keys 'state' and 'votes'. This method does not keep the underlying active list in Sync. Use either the targeted methods such as Suspend and Defeat or use VCUpdateActive to force the update.
427              
428             =head3 VCUpdateActive
429              
430             Update the ActiveSet of the underlying Vote::Count object to match the set of Choices that are currently 'hopeful'.
431              
432             =head2 Elected and Pending
433              
434             In addition to Elected, there is a Pending State. Pending means a Choice has reached the Quota, but not completed its Charges and Rebates. The distinction is for the benefit of methods that need choices held in pending, both Pending and Elected choices are removed from the active set.
435              
436             =head3 Elect, Elected
437              
438             Set Choice as Elected. Elected returns the list of currently elected choices.
439              
440             =head3 Pending
441              
442             Takes an Choice to set as Pending. Returns the list of Pending Choices.
443              
444             =head2 Eliminated: Withdrawn, Defeated, or Suspended
445              
446             In methods that set the Quota only once, choices eliminated before setting Quota are Withdrawn and may result in null ballots that can be exluded. Choices eliminated after setting Quota are Defeated. Some rules bring eliminated Choices back in later Rounds, Suspended distinguishes those eligible to return.
447              
448             =head3 Defeat, Defer, Withdraw, Suspend
449              
450             Perform the corresponding action for a Choice.
451              
452             $Election->Defeat('MARMALADE');
453              
454             =head3 Defeated, Deferred, Withdrawn, Suspended
455              
456             Returns a list of choices in that state.
457              
458             =head3 Reinstate
459              
460             Will reinstate all currently suspended choices or may be given a list of suspended choices that will be reinstated.
461              
462             =head2 STVRound, NextSTVRound
463              
464             STVRound returns the current Round, NextSTVRound advances the Round Counter and returns the new Round number.
465              
466             =head2 STVEvent
467              
468             Takes a reference as argument to add that reference to an Event History. This needs to be done seperately from logI<x> because STVEvent holds a list of data references instead of readably formatted events.
469              
470             =head2 WriteSTVEvent
471              
472             Writes JSON and YAML logs (path based on LogTo) of the STVEvents.
473              
474             =head2 SetQuota
475              
476             Calculate the Hare or Droop Quota. After the Division the result is rounded down and 1 is added to the result. The default is the Droop Quota, but either C<'hare'> or C<'droop'> may be requested as an optional parameter.
477              
478             my $droopquota = $Election->SetQuota();
479             my $harequota = $Election->SetQuota('hare');
480              
481             The Hare formula is Active Votes divided by number of Seats. Droop adds 1 to the number of seats, and to the result after rounding, resulting in a lower quota. The Droop Quota is the smallest for which it is impossible for more choices than the number of seats to reach the quota.
482              
483             =head2 Charge
484              
485             Charges Ballots for election of choice, parameters are $choice, $quota and $charge (defaults to VoteValue ).
486              
487             =head2 ResetVoteValue
488              
489             Resets all Ballots to their initial Vote Value.
490              
491             =head2 SeatsOpen
492              
493             Calculate and return the number of seats remaining to fill.
494              
495             =cut
496              
497             __PACKAGE__->meta->make_immutable;
498             1;
499              
500             #FOOTER
501              
502             =pod
503              
504             BUG TRACKER
505              
506             L<https://github.com/brainbuz/Vote-Count/issues>
507              
508             AUTHOR
509              
510             John Karr (BRAINBUZ) brainbuz@cpan.org
511              
512             CONTRIBUTORS
513              
514             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
515              
516             LICENSE
517              
518             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>.
519              
520             SUPPORT
521              
522             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
523              
524             =cut
525