3 # Does STV according to
4 # http://www.rosenstiel.co.uk/stvrules/av/index.htm
10 use List::MoreUtils qw(nsort_by);
20 our $places = shift @ARGV;
21 die unless $places eq ($places + 0);
25 m/^(\w+) ([A-Z]+)$/ or die "$_ ?";
30 Prefs => [ split //, $prefs ],
32 push @allvotes, $vote;
37 return sprintf "%10.6f=%-10s", $f, $f;
42 push @{ $vote->{Log} }, "stage $stage: $m";
46 my ($cand, $why) = @_;
47 foreach my $vote (@allvotes) {
48 my $oldprefs = $vote->{Prefs};
49 my @prefs = grep { $_ ne $cand } $oldprefs;
50 next if @prefs == @$oldprefs;
51 votelog $vote, "crossed out candidate $cand: $why";
55 our @elected; # $elected[] = $candidate
57 our @unsorted = @allvotes;
60 # $sorted{$firstpref}{Votes} = [ $vote, ... ]
61 # $sorted{$firstpref}{Cand} = $firstpref
62 # $sorted{$firstpref}{Total} = $totalweight
63 our @sorted; # values same as %sorted
64 our @surpluses; # values same as %sorted
66 our @exhausted; # votes
68 our %continuing; # $continuing{$candidate}=1
70 our @stagerecord; # $stagerecord[]{$candidate} = $total
72 foreach my $vote (@allvotes) {
73 $continuing{$_}=1 foreach @{ $vote->{Prefs} };
77 my ($how, @sorted) = @_;
78 return () unless @sorted;
80 my $eqtotal = $s->{Total};
82 while ($count < @$sorted && $sorted[$count]{Total} == $eqtotal) {
83 printf "%7s %10s\n", $how, $sorted[$count]{Cand};
91 while (my $vote = shift @unsorted) {
92 my ($firstpref) = grep { $continuing{$_} } @{ $vote->{Prefs} };
93 if (!defined $firstpref) {
94 votelog $vote, "ballot exhausted";
95 push @exhausted, $vote;
97 push @{ $sorted{$firstpref}{Votes} }, $vote;
100 $sorted{$_}{Cand} = $_ foreach keys %sorted;
101 foreach my $firstpref (sort keys %sorted) {
102 foreach my $vote (@{ $sorted{$firstpref}{Votes} }) {
103 votelog $vote, "counted $vote->{Weight} for $firstpref";
104 $sorted{$firstpref}{Total} += $vote->{Weight};
108 my $sort_update = sub {
109 @sorted = nsort_by { -$_->{Total} } values %sorted;
113 push @stagerecord, { map { ($_->{Cand}, $_->{Total}) } @sorted };
116 my $countvalid = sub {
118 foreach my $s (@$l) {
119 printf "%-7s %10s %s\n", $what, $s->{Cand}, pr $s->{Total};
120 $totalvalid += $s->{Total};
123 $countvalid->(\@sorted, '1stpref');
124 $countvalid->(\@surpluses, 'surplus');
126 printf "%7s %10s %s\n", 'TOTAL', '', pr $totalvalid;
128 unless ($totalvalid > 0) {
129 printf "No more votes!\n";
133 my $placesremain = $places - @elected;
135 unless ($placesremain > 0) {
136 printf "Complete.\n";
140 my $quota = $totalvalid / ($placesremain + 1);
141 printf "%7s %10s %s\n", '', 'quota', pr $quota;
143 my $need_to_transfer_surplus = 1;
145 # Look for people to elect.
146 # We elect as many as we can, rather than recomputing the (lower) quota
150 my $topvoters = $s->{Weight};
151 my $surplus = $topvoters - $quota;
152 last unless $surplus > 0;
154 printf "%7s %10s\n", 'ELECTED', $s->{Cand};
155 push @elected, $s->{Cand};
157 my $derate = $topvoters / $surplus;
158 printf "%7s %10s\n", 'derate', $s->{Cand}, pr $derate;
160 foreach my $vote (@{ $s->{Votes} }) {
161 votelog $vote, "elected $s->{Cand}, derated $weightmult";
162 $vote->{Weight} /= $derate;
165 delete %sorted{ $s->{Cand} };
166 delete $continuing{ $s->{Cand} };
169 $need_to_transfer_surplus = 0;
170 # before actually transferring a surplus, we will consider
171 # eliminating, and then reconsider with a lower quota
174 my $deferredsurplus = sum map { $_->{Total} } @surpluses;
175 printf "%7s %10s %s\n", 'def.srp', 'total', pr $deferredsurplus;
177 # Look for people to eliminate
178 # We eliminate before trying to transfer surpluses
183 my @elim = equalpiles 'elim?', reverse @sorted;
184 my $elimvote = sum map { $_->{Total} } @elim;
185 my $elimvotetotal = $elimvote * scalar @elim;
187 if (@surpluses and $elimvotetotal > $deferredsurplus) {
188 printf "no-elim, un-defer\n";
192 if ((scalar keys %continuing) - (scalar @elim) < $placesremain) {
193 # eliminate only one then, and try again
194 printf "elim-tie!\n";
195 @elim = historically_prefer -1, @elim;
198 foreach my $s (@elim) {
200 printf "%7s %10s\n", 'ELIM', $c;
201 my $votes = $s->{Votes};
202 votelog $_, "failed to stop $c elimination" foreach @$votes;
203 delete %continuing{$c};
205 push @unsorted, @$votes;
209 $need_to_transfer_surplus = 0;
212 next unless $need_to_transfer_surplus;
214 @surpluses = nsort_by { $_->{Total} }, @surpluses;
215 my @surplusxfer = equalpiles 'xfer?', @surpluses;
216 die unless @surplusxfer;
218 if (@surplusxfer > 1) {
219 @surplusxfer = historically_prefer +1, @surplusxfer;
222 my $s = $surplusxfer[0];
224 printf "%7s %10s\n", 'xfer', $c;
225 my $votes = $s->{Votes};
226 votelog $_, "surplus transferred" foreach @$votes;
227 @surpluses = grep { $_->{Cand} ne $c } @surpluses;
228 push @unsorted, @$votes;