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   975038 use strict;
  4         32  
  4         115  
2 4     4   21 use warnings;
  4         6  
  4         102  
3 4     4   93 use 5.024;
  4         13  
4              
5             package Vote::Count::Charge::Cascade;
6 4     4   1942 use namespace::autoclean;
  4         73578  
  4         17  
7 4     4   2593 use Moose;
  4         1905585  
  4         30  
8             extends 'Vote::Count::Charge';
9             # with 'Vote::Count::Charge::NthApproval';
10              
11 4     4   29879 no warnings 'experimental';
  4         13  
  4         569  
12 4     4   27 use feature qw /postderef signatures/;
  4         10  
  4         561  
13              
14 4     4   3757 use Storable 3.15 'dclone';
  4         15834  
  4         339  
15 4     4   2454 use Mojo::Template;
  4         826763  
  4         48  
16 4     4   2481 use Sort::Hash;
  4         3769  
  4         208  
17 4     4   30 use Data::Dumper;
  4         9  
  4         180  
18 4     4   31 use Try::Tiny;
  4         8  
  4         168  
19 4     4   2182 use JSON::MaybeXS;
  4         25532  
  4         284  
20 4     4   2011 use YAML::XS;
  4         11450  
  4         305  
21 4     4   3515 use Path::Tiny;
  4         44946  
  4         275  
22 4     4   39 use Carp;
  4         10  
  4         229  
23 4     4   2352 use Vote::Count::Helper::FullCascadeCharge;
  4         12  
  4         7109  
24              
25             our $VERSION='2.01';
26              
27             =head1 NAME
28              
29             Vote::Count::Charge::Cascade
30              
31             =head1 VERSION 2.01
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 40 my $I = shift;
67             # $I->TieBreakMethod('precedence');
68 13         384 $I->TieBreakerFallBackPrecedence(1);
69 13         62 $I->{'roundstatus'} = { 0 => {} };
70 13         47 $I->{'currentround'} = 0;
71             # to hold the last charged values for elected choices.
72 13         391 $I->{'lastcharge'} = {};
73             }
74              
75             our $coder = JSON->new->ascii->pretty;
76              
77 7     7 0 15 sub Round($I) { return $I->{'currentround'}; }
  7         12  
  7         17  
  7         19  
78              
79             # quota and charge of from previous round!
80 11     11 0 1639 sub NewRound ( $I, $quota = 0, $charge = {} ) {
  11         25  
  11         25  
  11         74  
  11         29  
81 11         63 $I->TopCount();
82 11         49 my $round = ++$I->{'currentround'};
83 11         78 $I->{'roundstatus'}{ $round - 1 } = {
84             'charge' => $charge,
85             'quota' => $quota,
86             };
87 11 100       51 if ( keys $charge->%* ) { $I->{'lastcharge'} = $charge }
  5         20  
88 11         85 return $round;
89             }
90              
91 13     13   5250 sub _preEstimate ( $I, $quota, @elected ) {
  13         27  
  13         26  
  13         46  
  13         36  
92 13         558 my $estrule = $I->EstimationRule();
93 13 100       60 my $lastround = $I->{'currentround'} ? $I->{'currentround'} - 1 : 0;
94 13         31 my $lastcharge = $I->{'lastcharge'};
95 13         387 my $unw = $I->LastTopCountUnWeighted();
96 13 100       70 die 'LastTopCountUnWeighted failed' unless ( keys $unw->%* );
97 12         27 my %estimate = ();
98 12         28 my %caps = ();
99 12 100 100     415 if ( $I->EstimationFresh && $I->EstimationRule eq 'estimate') {
100 1         14 die "Fresh Estimation is not compatible with EstimationRule estimate, because prior winners are not in current top count!";
101             }
102 11         36 for my $e (@elected) {
103 25 100 100     369 if ( $I->{'lastcharge'}{$e} && ! $I->EstimationFresh ) {
104 5         15 $estimate{$e} = $I->{'lastcharge'}{$e};
105 5         14 $caps{$e} = $I->{'lastcharge'}{$e};
106             }
107             else {
108 20 100       66 if ($estrule eq 'estimate') { $estimate{$e} = int( $quota / $unw->{$e} ) }
  13 100       49  
    100          
    50          
109 3         87 elsif ( $estrule eq 'votevalue' ) { $estimate{$e} = $I->VoteValue }
110 2         6 elsif ( $estrule eq 'zero') { $estimate{$e} = 0 }
111 2         56 elsif ( $estrule eq 'halfvalue' ){ $estimate{$e} = int( $I->VoteValue / 2 ) }
112 20         574 $caps{$e} = $I->VoteValue;
113             }
114             }
115 11         58 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 23 sub QuotaElectDo ( $I, $quota ) {
  3         7  
  3         6  
  3         8  
121 3         15 my %TC = $I->TopCount()->RawCount()->%*;
122 3         21 my @Electable = ();
123 3         13 for my $C ( keys %TC ) {
124 13 100       36 if ( $TC{$C} >= $quota ) {
125 4         26 $I->Elect($C);
126 4         9 push @Electable, $C;
127             }
128             }
129 3         17 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   3464 sub _chargeInsight ( $I, $quota, $est, $cap, $bottom, $freeze, @elected ) {
  29         41  
  29         47  
  29         40  
  29         34  
  29         50  
  29         45  
  29         65  
  29         46  
136 29         125 my $active = $I->GetActive();
137 29         98 my %estnew = ();
138             # make sure a new freeze is applied before charge evaluation.
139 29         97 for my $froz ( keys $freeze->%* ) {
140 70 50       217 $est->{$froz} = $freeze->{$froz} if $freeze->{$froz};
141             }
142 29         71 my %elect = map { $_ => 1 } (@elected);
  76         207  
143 29         135 my $B = dclone $I->GetBallots();
144 29         1227 my $charge =
145             FullCascadeCharge( $B, $quota, $est, $active, $I->VoteValue() );
146 29         74 LOOPINSIGHT: for my $E (@elected) {
147 76 50       214 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         173 $est->{$E} - int( $charge->{$E}{'surplus'} / $charge->{$E}{'count'} );
154             }
155             else {
156             $estnew{$E} = $est->{$E} -
157 33         99 ( int( $charge->{$E}{'surplus'} / $charge->{$E}{'count'} ) ) + 1 ;
158             }
159 76 100       190 $estnew{$E} = $cap->{$E} if $cap->{$E} < $estnew{$E}; # apply cap.
160             $estnew{$E} = $bottom->{$E}
161 76 100       186 if $bottom->{$E} > $estnew{$E}; # apply bottom.
162             }
163 29         17164 return { 'result' => $charge, 'estimate' => \%estnew };
164             }
165              
166 5     5   9 sub _write_iteration_log ( $I, $round, $data ) {
  5         9  
  5         10  
  5         9  
  5         8  
167 5 100       267 if( $I->IterationLog() ) {
168 2         63 my $jsonpath = $I->IterationLog() . ".$round.json";
169 2         57 my $yamlpath = $I->IterationLog() . ".$round.yaml";
170 2         15 path( $jsonpath )->spew( $coder->encode( $data ) );
171 2         1688 path( $yamlpath )->spew( Dump $data );
172             }
173             }
174              
175 6     6 0 1114 sub CalcCharge ( $I, $quota ) {
  6         14  
  6         13  
  6         9  
176 6         29 my @elected = $I->Elected();
177 6         32 my $round = $I->Round();
178 6         15 my $estimates = {};
179 6         11 my $iteration = 0;
180 6         26 my $freeze = { map { $_ => 0 } @elected };
  13         39  
181 6         19 my $bottom = { map { $_ => 0 } @elected };
  13         29  
182 6         28 my ( $estimate, $cap ) = _preEstimate( $I, $quota, @elected );
183 5         17 $estimates->{$iteration} = $estimate;
184 5         8 my $done = 0;
185 5         11 my $charged = undef ; # the last value from loop is needed for log.
186 5         14 until ( $done ) {
187 26         43 ++$iteration;
188 26 50       57 if ( $iteration > 100 ) { die "Exceeded Iteration Limit!\n"}
  0         0  
189             # for ( $estimate, $cap, $bottom, $freeze, @elected ) { warn Dumper $_}
190             $charged =
191 26         86 _chargeInsight( $I, $quota, $estimate, $cap, $bottom, $freeze,
192             @elected );
193 26         105 $estimate = $charged->{'estimate'};
194 26         144 $estimates->{$iteration} = $charged->{'estimate'};
195 26         47 $done = 1;
196 26         59 for my $V (@elected) {
197 70         133 my $est1 = $estimates->{$iteration}{$V};
198 70         135 my $est2 = $estimates->{ $iteration - 1 }{$V};
199 70 100       170 if ( $est1 != $est2 ) { $done = 0 }
  35         75  
200             }
201             }
202             _write_iteration_log( $I, $round, {
203             estimates => $estimates,
204             quota => $quota,
205             charge => $estimate,
206             iterations => $iteration,
207 5         71 detail => $charged->{'result'} } );
208              
209             $I->STVEvent( {
210             round => $round,
211             quota => $quota,
212             charge => $estimate,
213             iterations => $iteration,
214 5         1535 detail => $charged->{'result'}
215             } );
216 5         45 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