File Coverage

blib/lib/Vote/Count/Charge/Cascade.pm
Criterion Covered Total %
statement 171 174 98.2
branch 32 36 88.8
condition 6 6 100.0
subroutine 25 25 100.0
pod 0 5 0.0
total 234 246 95.1


line stmt bran cond sub pod time code
1 4     4   1018302 use strict;
  4         31  
  4         118  
2 4     4   23 use warnings;
  4         7  
  4         93  
3 4     4   88 use 5.024;
  4         12  
4              
5             package Vote::Count::Charge::Cascade;
6 4     4   2126 use namespace::autoclean;
  4         75569  
  4         17  
7 4     4   2744 use Moose;
  4         1905214  
  4         27  
8             extends 'Vote::Count::Charge';
9             # with 'Vote::Count::Charge::NthApproval';
10              
11 4     4   30018 no warnings 'experimental';
  4         12  
  4         201  
12 4     4   24 use feature qw /postderef signatures/;
  4         9  
  4         567  
13              
14 4     4   3812 use Storable 3.15 'dclone';
  4         16666  
  4         282  
15 4     4   2459 use Mojo::Template;
  4         857529  
  4         37  
16 4     4   2173 use Sort::Hash;
  4         3930  
  4         240  
17 4     4   34 use Data::Dumper;
  4         9  
  4         192  
18 4     4   26 use Try::Tiny;
  4         8  
  4         181  
19 4     4   2249 use JSON::MaybeXS;
  4         26257  
  4         292  
20 4     4   2073 use YAML::XS;
  4         11737  
  4         233  
21 4     4   3683 use Path::Tiny;
  4         47349  
  4         230  
22 4     4   38 use Carp;
  4         9  
  4         212  
23 4     4   2712 use Vote::Count::Helper::FullCascadeCharge;
  4         55  
  4         7566  
24              
25             our $VERSION='2.00';
26              
27             =head1 NAME
28              
29             Vote::Count::Charge::Cascade
30              
31             =head1 VERSION 2.00
32              
33             =cut
34              
35             has 'VoteValue' => (
36             is => 'ro',
37             isa => 'Int',
38             default => 100000,
39             );
40              
41             has 'IterationLog' => (
42             is => 'rw',
43             isa => 'Str',
44             required => 0,
45             );
46              
47             has 'EstimationRule' => (
48             is => 'ro',
49             isa => 'Str',
50             default => 'estimate',
51             );
52              
53             has 'EstimationFresh' => (
54             is => 'ro',
55             isa => 'Bool',
56             default => 0,
57             );
58              
59             has 'TieBreakMethod' => (
60             is => 'rw',
61             isa => 'Str',
62             default => ''
63             );
64              
65             sub BUILD {
66 13     13 0 49 my $I = shift;
67             # $I->TieBreakMethod('precedence');
68 13         433 $I->TieBreakerFallBackPrecedence(1);
69 13         71 $I->{'roundstatus'} = { 0 => {} };
70 13         33 $I->{'currentround'} = 0;
71             # to hold the last charged values for elected choices.
72 13         382 $I->{'lastcharge'} = {};
73             }
74              
75             our $coder = JSON->new->ascii->pretty;
76              
77 7     7 0 16 sub Round($I) { return $I->{'currentround'}; }
  7         13  
  7         13  
  7         18  
78              
79             # quota and charge of from previous round!
80 11     11 0 1693 sub NewRound ( $I, $quota = 0, $charge = {} ) {
  11         24  
  11         27  
  11         26  
  11         20  
81 11         60 $I->TopCount();
82 11         47 my $round = ++$I->{'currentround'};
83 11         71 $I->{'roundstatus'}{ $round - 1 } = {
84             'charge' => $charge,
85             'quota' => $quota,
86             };
87 11 100       53 if ( keys $charge->%* ) { $I->{'lastcharge'} = $charge }
  5         16  
88 11         79 return $round;
89             }
90              
91 13     13   5384 sub _preEstimate ( $I, $quota, @elected ) {
  13         56  
  13         27  
  13         31  
  13         19  
92 13         517 my $estrule = $I->EstimationRule();
93 13 100       58 my $lastround = $I->{'currentround'} ? $I->{'currentround'} - 1 : 0;
94 13         40 my $lastcharge = $I->{'lastcharge'};
95 13         398 my $unw = $I->LastTopCountUnWeighted();
96 13 100       76 die 'LastTopCountUnWeighted failed' unless ( keys $unw->%* );
97 12         30 my %estimate = ();
98 12         29 my %caps = ();
99 12 100 100     362 if ( $I->EstimationFresh && $I->EstimationRule eq 'estimate') {
100 1         13 die "Fresh Estimation is not compatible with EstimationRule estimate, because prior winners are not in current top count!";
101             }
102 11         37 for my $e (@elected) {
103 25 100 100     370 if ( $I->{'lastcharge'}{$e} && ! $I->EstimationFresh ) {
104 5         17 $estimate{$e} = $I->{'lastcharge'}{$e};
105 5         15 $caps{$e} = $I->{'lastcharge'}{$e};
106             }
107             else {
108 20 100       67 if ($estrule eq 'estimate') { $estimate{$e} = int( $quota / $unw->{$e} ) }
  13 100       46  
    100          
    50          
109 3         89 elsif ( $estrule eq 'votevalue' ) { $estimate{$e} = $I->VoteValue }
110 2         5 elsif ( $estrule eq 'zero') { $estimate{$e} = 0 }
111 2         56 elsif ( $estrule eq 'halfvalue' ){ $estimate{$e} = int( $I->VoteValue / 2 ) }
112 20         559 $caps{$e} = $I->VoteValue;
113             }
114             }
115 11         53 return ( \%estimate, \%caps );
116             }
117              
118             # Must move directly to charge after this
119             # --- if another topcount happens estimate will crash!
120 3     3 0 24 sub QuotaElectDo ( $I, $quota ) {
  3         7  
  3         17  
  3         5  
121 3         19 my %TC = $I->TopCount()->RawCount()->%*;
122 3         20 my @Electable = ();
123 3         12 for my $C ( keys %TC ) {
124 13 100       34 if ( $TC{$C} >= $quota ) {
125 4         23 $I->Elect($C);
126 4         10 push @Electable, $C;
127             }
128             }
129 3         19 return @Electable;
130             }
131              
132             # Produce a better estimate than the previous by running
133             # FullCascadeCharge of the last estimate. Clones a copy of
134             # Ballots for the Cascade Charge.
135 29     29   3581 sub _chargeInsight ( $I, $quota, $est, $cap, $bottom, $freeze, @elected ) {
  29         44  
  29         54  
  29         42  
  29         42  
  29         46  
  29         41  
  29         69  
  29         40  
136 29         126 my $active = $I->GetActive();
137 29         84 my %estnew = ();
138             # make sure a new freeze is applied before charge evaluation.
139 29         97 for my $froz ( keys $freeze->%* ) {
140 70 50       183 $est->{$froz} = $freeze->{$froz} if $freeze->{$froz};
141             }
142 29         67 my %elect = map { $_ => 1 } (@elected);
  76         195  
143 29         100 my $B = dclone $I->GetBallots();
144 29         1190 my $charge =
145             FullCascadeCharge( $B, $quota, $est, $active, $I->VoteValue() );
146 29         73 LOOPINSIGHT: for my $E (@elected) {
147 76 50       202 if ( $freeze->{$E} ) { # if frozen stay frozen.
    100          
148 0         0 $estnew{$E} = $freeze->{$E};
149 0         0 next LOOPINSIGHT;
150             }
151             elsif ( $charge->{$E}{'surplus'} >= 0 ) {
152             $estnew{$E} =
153 43         160 $est->{$E} - int( $charge->{$E}{'surplus'} / $charge->{$E}{'count'} );
154             }
155             else {
156             $estnew{$E} = $est->{$E} -
157 33         94 ( int( $charge->{$E}{'surplus'} / $charge->{$E}{'count'} ) ) + 1 ;
158             }
159 76 100       179 $estnew{$E} = $cap->{$E} if $cap->{$E} < $estnew{$E}; # apply cap.
160             $estnew{$E} = $bottom->{$E}
161 76 100       176 if $bottom->{$E} > $estnew{$E}; # apply bottom.
162             }
163 29         16155 return { 'result' => $charge, 'estimate' => \%estnew };
164             }
165              
166 5     5   11 sub _write_iteration_log ( $I, $round, $data ) {
  5         10  
  5         13  
  5         10  
  5         8  
167 5 100       231 if( $I->IterationLog() ) {
168 2         76 my $jsonpath = $I->IterationLog() . ".$round.json";
169 2         60 my $yamlpath = $I->IterationLog() . ".$round.yaml";
170 2         15 path( $jsonpath )->spew( $coder->encode( $data ) );
171 2         1903 path( $yamlpath )->spew( Dump $data );
172             }
173             }
174              
175 6     6 0 1081 sub CalcCharge ( $I, $quota ) {
  6         14  
  6         10  
  6         12  
176 6         35 my @elected = $I->Elected();
177 6         27 my $round = $I->Round();
178 6         13 my $estimates = {};
179 6         14 my $iteration = 0;
180 6         17 my $freeze = { map { $_ => 0 } @elected };
  13         35  
181 6         15 my $bottom = { map { $_ => 0 } @elected };
  13         27  
182 6         26 my ( $estimate, $cap ) = _preEstimate( $I, $quota, @elected );
183 5         17 $estimates->{$iteration} = $estimate;
184 5         7 my $done = 0;
185 5         11 my $charged = undef ; # the last value from loop is needed for log.
186 5         15 until ( $done ) {
187 26         48 ++$iteration;
188 26 50       60 if ( $iteration > 100 ) { die "Exceeded Iteration Limit!\n"}
  0         0  
189             # for ( $estimate, $cap, $bottom, $freeze, @elected ) { warn Dumper $_}
190             $charged =
191 26         82 _chargeInsight( $I, $quota, $estimate, $cap, $bottom, $freeze,
192             @elected );
193 26         91 $estimate = $charged->{'estimate'};
194 26         129 $estimates->{$iteration} = $charged->{'estimate'};
195 26         40 $done = 1;
196 26         64 for my $V (@elected) {
197 70         121 my $est1 = $estimates->{$iteration}{$V};
198 70         130 my $est2 = $estimates->{ $iteration - 1 }{$V};
199 70 100       160 if ( $est1 != $est2 ) { $done = 0 }
  35         70  
200             }
201             }
202             _write_iteration_log( $I, $round, {
203             estimates => $estimates,
204             quota => $quota,
205             charge => $estimate,
206             iterations => $iteration,
207 5         54 detail => $charged->{'result'} } );
208              
209             $I->STVEvent( {
210             round => $round,
211             quota => $quota,
212             charge => $estimate,
213             iterations => $iteration,
214 5         1469 detail => $charged->{'result'}
215             } );
216 5         47 return $estimate;
217             }
218              
219             __PACKAGE__->meta->make_immutable;
220             1;
221              
222             #FOOTER
223              
224             =pod
225              
226             BUG TRACKER
227              
228             L<https://github.com/brainbuz/Vote-Count/issues>
229              
230             AUTHOR
231              
232             John Karr (BRAINBUZ) brainbuz@cpan.org
233              
234             CONTRIBUTORS
235              
236             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
237              
238             LICENSE
239              
240             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>.
241              
242             SUPPORT
243              
244             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
245              
246             =cut
247