File Coverage

blib/lib/Vote/Count/Method/WIGM.pm
Criterion Covered Total %
statement 139 142 97.8
branch 15 18 83.3
condition n/a
subroutine 18 18 100.0
pod 0 1 0.0
total 172 179 96.0


line stmt bran cond sub pod time code
1 2     2   423154 use strict;
  2         17  
  2         49  
2 2     2   10 use warnings;
  2         3  
  2         40  
3 2     2   42 use 5.024;
  2         6  
4              
5             package Vote::Count::Method::WIGM;
6 2     2   912 use namespace::autoclean;
  2         30708  
  2         7  
7 2     2   1077 use Moose;
  2         791744  
  2         12  
8             extends 'Vote::Count::Charge';
9              
10 2     2   12368 no warnings 'experimental';
  2         4  
  2         96  
11 2     2   10 use feature qw /postderef signatures/;
  2         4  
  2         238  
12              
13 2     2   1436 use Storable 3.15 'dclone';
  2         6436  
  2         111  
14 2     2   885 use Mojo::Template;
  2         341893  
  2         19  
15 2     2   918 use Sort::Hash;
  2         1571  
  2         87  
16 2     2   13 use Data::Dumper;
  2         4  
  2         2790  
17              
18             our $VERSION='2.01';
19              
20             =head1 NAME
21              
22             Vote::Count::Method::WIGM
23              
24             =head1 VERSION 2.01
25              
26             =cut
27              
28             # ABSTRACT: An implementation of WIGM STV using Vote::Charge.
29              
30             =pod
31              
32             =head1 SYNOPSIS
33              
34             use Vote::Count::Method::WIGM;
35             use Vote::Count::ReadBallots 'read_ballots';
36              
37             my $ballotset = read_ballots('t/data/Scotland2012/Cumbernauld_South.txt');
38             my $Cumbernauld = Vote::Count::Method::WIGM->new(
39             Seats => 4,
40             BallotSet => $ballotset,
41             VoteValue => 100000, # default
42             LogTo => '/tmp/cumbernauld_south_2012',
43             );
44              
45             # Run the Election
46             $D->WIGRun();
47             # Write the Human Readable Logs
48             $D->WriteLog();
49             # Write the Events in JSON and YAML
50             $D->WriteSTVEvent();
51              
52             =head1 Description
53              
54             Implements Weighted Improved Gregory Single Transferable Vote based on Scotland's rules.
55              
56             =head1 WIGRun
57              
58             Run and log the Election.
59              
60             =head2 Implementation Notes
61              
62             The Scottish Rules specify 5 decimal places, a weight of 100,000 is used which is equivalent.
63              
64             When more than one choice is Pending the rules call for a full Stage to elect each of them. Pending Choices cannot recieve additional votes, this implementation elects, charges, and rebates the Pending Choices, then starts a new Round. The final result will be the same, but Vote::Count::Method::WIGM rounds will not always match the stages of the Hand Count rules.
65              
66             =head1 Experimental
67              
68             Small discrepencies with the stages data available for testing have been seen, which are likely to be rounding issues. Until further review can be taken, this code should be considered a preview.
69              
70             =head1 The Rules
71              
72             L<The Official Rules|http://www.opsi.gov.uk/legislation/scotland/ssi2007/ssi_20070042_en.pdf>
73              
74             =cut
75              
76             has 'VoteValue' => (
77             is => 'ro',
78             isa => 'Int',
79             default => 100000,
80             );
81              
82 4     4   17 sub _SetWIGQuota ( $I, $ballots = 0 ) {
  4         6  
  4         14  
  4         7  
83 4 100       21 $ballots = $I->VotesCast() unless $ballots;
84 4         136 my $denominator = $I->Seats() + 1;
85 4         22 my $q = 1 + int( $ballots / $denominator );
86 4         101 return ( $q * $I->VoteValue );
87             }
88              
89             # If a floor is applied there may be abandoned ballots,
90             # _WIGStart will remove these from the quota.
91              
92 12     12   22 sub _format_round_result ( $rslt, $topcount, $votevalue ) {
  12         22  
  12         22  
  12         22  
  12         15  
93 12         1549 my $top = dclone $topcount;
94 12         47 my $rawcount = $top->{'rawcount'};
95 12         51 for my $t ( keys $rawcount->%* ) {
96 54         100 my $folded = $rawcount->{$t} / $votevalue;
97 54         295 $rawcount->{$t} = "$folded ($rawcount->{$t})";
98             }
99             # die Dumper $rawcount;
100 12         31 my $tmpl = q|% use feature 'postderef';
101             % my @pending = $rslt->{'pending'}->@*;
102             ## Round: <%= $rslt->{'round'} %>
103              
104             <%= $tc->RankTable() %>
105             % if ( @pending ) {
106             ### Winners:
107              
108             % for my $p ( @pending ) {
109             *<%= $p %>*: <%= $rslt->{'winvotes'}{$p} / $votevalue %> (<%= $rslt->{'winvotes'}{$p} %>)
110              
111             % }
112             % } else {
113             ### No Winners
114              
115             % }|;
116 12         117 my $mt = Mojo::Template->new;
117 12         133 return $mt->vars(1)->render( $tmpl, {rslt => $rslt, tc => $top, votevalue => $votevalue });
118             }
119              
120 1     1   2 sub _WIGStart ( $I ) {
  1         2  
  1         1  
121 1         27 my $seats = $I->Seats();
122 1         7 my $ballots_cast = $I->VotesCast();
123 1         8 $I->TopCount(); # Set the topchoice values on the ballots.
124 1         9 my $abandoned = $I->CountAbandoned()->{'count_abandoned'};
125 1         5 my $ballots_valid = $ballots_cast - $abandoned;
126 1 50       3 my $abandonmsg =
127             $abandoned
128             ? "\nThere are $ballots_cast ballots. \n$abandoned ballots have no choices and will be disregarded"
129             : '';
130 1         7 my $quota = $I->_SetWIGQuota($ballots_valid);
131 1         17 $I->logt(
132             qq/Using Weighted Inclusive Gregory, Scottish Rules. $abandonmsg
133             Seats to Fill: $seats
134             Ballots Counted for Quota: $ballots_valid
135             Quota: $quota;
136             /
137             );
138 1         4 my $event = { ballots => $ballots_valid, quota => $quota };
139 1         9 $I->STVEvent($event);
140 1         3 return $event;
141             }
142              
143 12     12   1110 sub _WIGRound ( $I, $quota ) {
  12         20  
  12         23  
  12         17  
144 12         58 my $round_num = $I->NextSTVRound();
145 12         89 my $round = $I->TopCount();
146 12         64 my $roundcnt = $round->RawCount();
147 12         62 my @choices = $I->GetActiveList();
148 12         34 my %rndvotes = ();
149 12         48 my $leader = $round->Leader()->{'winner'};
150 12         46 my $votes4leader = $round->RawCount->{$leader};
151 12 100       40 my $pending = $votes4leader >= $quota ? $leader : '';
152              
153 12         32 for my $C (@choices ) {
154 54 100       95 if( $roundcnt->{ $C} >= $quota ) {
155 9         20 $rndvotes{ $C } = $roundcnt->{ $C } ;
156             }
157             }
158 12         78 my @pending = sort_hash( \%rndvotes, 'numeric', 'desc' );
159              
160             my $rslt = {
161             pending => \@pending,
162             winvotes => \%rndvotes,
163             quota => $quota,
164             round => $round_num,
165             allvotes => $round->RawCount(),
166             lowest => $round->ArrayBottom()->[0],
167 12         2204 noncontinuing => $I->CountAbandoned()->{'value_abandoned'},
168             };
169 12         104 $I->STVEvent($rslt);
170 12         507 $I->logv( _format_round_result( $rslt, $round, $I->VoteValue() ) );
171 12         227 return ($rslt);
172             }
173              
174 7     7   25 sub _WIGElect ( $I, $chrg ) {
  7         12  
  7         12  
  7         11  
175 7         13 my $choice = $chrg->{'choice'};
176 7         27 my $refund = int( $chrg->{'surplus'} / $chrg->{'cntchrgd'} );
177 7         13 my $refunded = 0;
178 7         22 for my $b ( $chrg->{'ballotschrgd'}->@* ) {
179 1047         18978 my $ballot = $I->BallotSet()->{'ballots'}{$b};
180 1047 50       2011 if ( $ballot->{'charged'}{$choice} < $refund )
181             {
182 0         0 $ballot->{'votevalue'} += $ballot->{'charged'}{$choice};
183 0         0 $refunded += $ballot->{'charged'}{$choice} * $ballot->{'count'};
184 0         0 $ballot->{'charged'}{$choice} = 0;
185             } else
186             {
187 1047         1168 $ballot->{'votevalue'} += $refund;
188 1047         1216 $refunded += $ballot->{'count'} * $refund;
189 1047         1401 $ballot->{'charged'}{$choice} -= $refund;
190             }
191             }
192             my $candvotes =
193 7         36 $I->GetChoiceStatus( $choice )->{'votes'} - $refunded;
194 7         40 $I->SetChoiceStatus( $choice, { votes => $candvotes } );
195 7         30 $I->Elect( $choice );
196             }
197              
198 10     10   20 sub _wigcomplete ( $I, $_WIGRound ) {
  10         16  
  10         17  
  10         12  
199 10         43 my @choices = $I->GetActiveList();
200 10         21 my $choiceremain = scalar @choices;
201 10         18 my $numelected = scalar( $I->{'elected'}->@* );
202 10         214 my $openseats = $I->Seats() - $numelected;
203 10 100       25 if ( $choiceremain <= $openseats ) {
204 1         7 $I->logv( "Electing all Remaining Choices: @choices.\n");
205 1         4 for my $C (@choices) {
206 1         3 my $cvotes = $_WIGRound->{'allvotes'}{$C};
207 1         5 $I->Elect($C);
208 1         6 $I->SetChoiceStatus( $C, { votes => $cvotes } );
209             }
210 1         5 return 1;
211             }
212 9         33 return 0;
213             }
214              
215 1     1 0 6 sub WIGRun ( $I ) {
  1         2  
  1         1  
216 1         4 my $pre_rslt = $I->_WIGStart();
217 1         4 my $quota = $pre_rslt->{'quota'};
218 1         25 my $seats = $I->Seats();
219              
220             WIGDOROUNDLOOP:
221 1         8 while ( $I->Elected() < $seats ) {
222 6         22 my $rnd = $I->_WIGRound($quota);
223 6 50       24 last WIGDOROUNDLOOP if _wigcomplete ( $I, $rnd );
224 6         22 my @pending = $rnd->{'pending'}->@*;
225 6 100       17 if ( scalar(@pending)){
226 2         6 for my $pending (@pending) {
227 3         21 my $chrg = $I->Charge( $pending, $quota );
228 3         20 $I->_WIGElect($chrg);
229             }
230             } else {
231 4         26 $I->logv( "Eliminating low choice: $rnd->{'lowest'}\n");
232 4         27 $I->Defeat($rnd->{'lowest'});
233 4 100       11 last WIGDOROUNDLOOP if _wigcomplete ( $I, $rnd );
234             }
235             }
236 1         4 my @elected = $I->Elected();
237 1         7 $I->STVEvent( { winners => \@elected });
238 1         8 $I->logt( "Winners: " . join( ', ', @elected ));
239             }
240              
241             __PACKAGE__->meta->make_immutable;
242             1;
243              
244             #FOOTER
245              
246             =pod
247              
248             BUG TRACKER
249              
250             L<https://github.com/brainbuz/Vote-Count/issues>
251              
252             AUTHOR
253              
254             John Karr (BRAINBUZ) brainbuz@cpan.org
255              
256             CONTRIBUTORS
257              
258             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
259              
260             LICENSE
261              
262             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>.
263              
264             SUPPORT
265              
266             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
267              
268             =cut
269