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   522780 use strict;
  2         19  
  2         59  
2 2     2   12 use warnings;
  2         4  
  2         46  
3 2     2   47 use 5.024;
  2         7  
4              
5             package Vote::Count::Method::WIGM;
6 2     2   1072 use namespace::autoclean;
  2         37941  
  2         8  
7 2     2   1409 use Moose;
  2         979509  
  2         16  
8             extends 'Vote::Count::Charge';
9              
10 2     2   15731 no warnings 'experimental';
  2         6  
  2         119  
11 2     2   16 use feature qw /postderef signatures/;
  2         4  
  2         307  
12              
13 2     2   2068 use Storable 3.15 'dclone';
  2         8396  
  2         139  
14 2     2   1144 use Mojo::Template;
  2         428697  
  2         22  
15 2     2   1169 use Sort::Hash;
  2         1857  
  2         107  
16 2     2   15 use Data::Dumper;
  2         4  
  2         3353  
17              
18             our $VERSION='2.00';
19              
20             =head1 NAME
21              
22             Vote::Count::Method::WIGM
23              
24             =head1 VERSION 2.00
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   19 sub _SetWIGQuota ( $I, $ballots = 0 ) {
  4         9  
  4         9  
  4         7  
83 4 100       26 $ballots = $I->VotesCast() unless $ballots;
84 4         193 my $denominator = $I->Seats() + 1;
85 4         19 my $q = 1 + int( $ballots / $denominator );
86 4         131 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   31 sub _format_round_result ( $rslt, $topcount, $votevalue ) {
  12         28  
  12         27  
  12         28  
  12         24  
93 12         2143 my $top = dclone $topcount;
94 12         57 my $rawcount = $top->{'rawcount'};
95 12         69 for my $t ( keys $rawcount->%* ) {
96 54         139 my $folded = $rawcount->{$t} / $votevalue;
97 54         381 $rawcount->{$t} = "$folded ($rawcount->{$t})";
98             }
99             # die Dumper $rawcount;
100 12         38 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         143 my $mt = Mojo::Template->new;
117 12         197 return $mt->vars(1)->render( $tmpl, {rslt => $rslt, tc => $top, votevalue => $votevalue });
118             }
119              
120 1     1   110 sub _WIGStart ( $I ) {
  1         6  
  1         3  
121 1         42 my $seats = $I->Seats();
122 1         8 my $ballots_cast = $I->VotesCast();
123 1         38 $I->TopCount(); # Set the topchoice values on the ballots.
124 1         20 my $abandoned = $I->CountAbandoned()->{'count_abandoned'};
125 1         6 my $ballots_valid = $ballots_cast - $abandoned;
126 1 50       8 my $abandonmsg =
127             $abandoned
128             ? "\nThere are $ballots_cast ballots. \n$abandoned ballots have no choices and will be disregarded"
129             : '';
130 1         9 my $quota = $I->_SetWIGQuota($ballots_valid);
131 1         45 $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         6 my $event = { ballots => $ballots_valid, quota => $quota };
139 1         12 $I->STVEvent($event);
140 1         4 return $event;
141             }
142              
143 12     12   1273 sub _WIGRound ( $I, $quota ) {
  12         29  
  12         26  
  12         24  
144 12         62 my $round_num = $I->NextSTVRound();
145 12         117 my $round = $I->TopCount();
146 12         77 my $roundcnt = $round->RawCount();
147 12         93 my @choices = $I->GetActiveList();
148 12         42 my %rndvotes = ();
149 12         66 my $leader = $round->Leader()->{'winner'};
150 12         58 my $votes4leader = $round->RawCount->{$leader};
151 12 100       54 my $pending = $votes4leader >= $quota ? $leader : '';
152              
153 12         50 for my $C (@choices ) {
154 54 100       125 if( $roundcnt->{ $C} >= $quota ) {
155 9         26 $rndvotes{ $C } = $roundcnt->{ $C } ;
156             }
157             }
158 12         88 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         2788 noncontinuing => $I->CountAbandoned()->{'value_abandoned'},
168             };
169 12         189 $I->STVEvent($rslt);
170 12         808 $I->logv( _format_round_result( $rslt, $round, $I->VoteValue() ) );
171 12         372 return ($rslt);
172             }
173              
174 7     7   28 sub _WIGElect ( $I, $chrg ) {
  7         26  
  7         13  
  7         14  
175 7         20 my $choice = $chrg->{'choice'};
176 7         36 my $refund = int( $chrg->{'surplus'} / $chrg->{'cntchrgd'} );
177 7         15 my $refunded = 0;
178 7         26 for my $b ( $chrg->{'ballotschrgd'}->@* ) {
179 1047         24101 my $ballot = $I->BallotSet()->{'ballots'}{$b};
180 1047 50       2464 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         1596 $ballot->{'votevalue'} += $refund;
188 1047         1561 $refunded += $ballot->{'count'} * $refund;
189 1047         1801 $ballot->{'charged'}{$choice} -= $refund;
190             }
191             }
192             my $candvotes =
193 7         63 $I->GetChoiceStatus( $choice )->{'votes'} - $refunded;
194 7         68 $I->SetChoiceStatus( $choice, { votes => $candvotes } );
195 7         45 $I->Elect( $choice );
196             }
197              
198 10     10   24 sub _wigcomplete ( $I, $_WIGRound ) {
  10         18  
  10         18  
  10         17  
199 10         62 my @choices = $I->GetActiveList();
200 10         28 my $choiceremain = scalar @choices;
201 10         28 my $numelected = scalar( $I->{'elected'}->@* );
202 10         294 my $openseats = $I->Seats() - $numelected;
203 10 100       35 if ( $choiceremain <= $openseats ) {
204 1         11 $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         6 $I->Elect($C);
208 1         6 $I->SetChoiceStatus( $C, { votes => $cvotes } );
209             }
210 1         8 return 1;
211             }
212 9         47 return 0;
213             }
214              
215 1     1 0 8 sub WIGRun ( $I ) {
  1         4  
  1         2  
216 1         6 my $pre_rslt = $I->_WIGStart();
217 1         4 my $quota = $pre_rslt->{'quota'};
218 1         29 my $seats = $I->Seats();
219              
220             WIGDOROUNDLOOP:
221 1         8 while ( $I->Elected() < $seats ) {
222 6         32 my $rnd = $I->_WIGRound($quota);
223 6 50       35 last WIGDOROUNDLOOP if _wigcomplete ( $I, $rnd );
224 6         30 my @pending = $rnd->{'pending'}->@*;
225 6 100       22 if ( scalar(@pending)){
226 2         6 for my $pending (@pending) {
227 3         33 my $chrg = $I->Charge( $pending, $quota );
228 3         21 $I->_WIGElect($chrg);
229             }
230             } else {
231 4         36 $I->logv( "Eliminating low choice: $rnd->{'lowest'}\n");
232 4         43 $I->Defeat($rnd->{'lowest'});
233 4 100       18 last WIGDOROUNDLOOP if _wigcomplete ( $I, $rnd );
234             }
235             }
236 1         8 my @elected = $I->Elected();
237 1         10 $I->STVEvent( { winners => \@elected });
238 1         9 $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