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