X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?p=appendix-a6.git;a=blobdiff_plain;f=stv;h=fbd7b41050b18f535931223c8b7e0d8534c9f095;hp=9e0aceee2dd826ed83a2187795b809d2bb0d17f1;hb=0328306e06758d9fb43cec21611d044eda1acf09;hpb=b673f3ba34855765aca513ee45b84a918eaad099 diff --git a/stv b/stv index 9e0acee..fbd7b41 100755 --- a/stv +++ b/stv @@ -7,6 +7,10 @@ use strict; use utf8; use autodie; use bigrat; +use Carp; +use Data::Dumper; +use List::Util qw(sum0); +use List::MoreUtils qw(nsort_by); # vote is # { Voter => opaque, @@ -19,10 +23,12 @@ our @allvotes; our $places = shift @ARGV; die unless $places eq ($places + 0); +open DEBUG, ">.stv.log" or die $!; + while (<>) { next if m/^\w+$/; m/^(\w+) ([A-Z]+)$/ or die "$_ ?"; - my $prefs = $1; + my $prefs = $2; my $vote = { Voter => $1, Weight => 1.0, @@ -33,7 +39,8 @@ while (<>) { sub pr ($) { my ($f) = @_; - return sprintf "%10.6f=%-10s", $f, $f; + confess unless defined $f; + return sprintf "%10.6f = %-10s", $f, $f; } sub votelog ($$) { @@ -41,67 +48,263 @@ sub votelog ($$) { push @{ $vote->{Log} }, "stage $stage: $m"; } -sub filterout ($$) { - my ($cand, $why) = @_; - foreach my $vote (@allvotes) { - my $oldprefs = $vote->{Prefs}; - my @prefs = grep { $_ ne $cand } $oldprefs; - next if @prefs == @$oldprefs; - votelog $vote, "crossed out candidate $cand: $why"; +our @elected; # $elected[] = $candidate + +our @unsorted = @allvotes; + +our %sorted; +# $sorted{$firstpref}{Votes} = [ $vote, ... ] +# $sorted{$firstpref}{Cand} = $firstpref +# $sorted{$firstpref}{Total} = $totalweight +our @surpluses; # values same as %sorted + +our @exhausted; # votes + +our %continuing; # $continuing{$candidate}=1 + +our @stagerecord; # $stagerecord[]{$candidate} = $total + +sub voteliveprefs ($) { + my ($vote) = @_; + grep { $continuing{$_} } @{ $vote->{Prefs} }; +} + +sub votelogfull ($$) { + my ($vote,$m) = @_; + votelog $vote, $m; + votelog $vote, ("continuing prefs: ". join ' ', voteliveprefs $vote); +} + +foreach my $vote (@allvotes) { + $continuing{$_}=1 foreach @{ $vote->{Prefs} }; +} + +sub equalpiles ($@) { + my ($how, @sorted) = @_; + return () unless @sorted; + my $s = $sorted[0]; + my $eqtotal = $s->{Total}; + my $count = 0; + while ($count < @sorted && $sorted[$count]{Total} == $eqtotal) { + printf "%7s %10s %s\n", $how, $sorted[$count]{Cand}, + pr $sorted[$count]{Total}; + $count++; } + return @sorted[ 0 .. $count-1 ]; +} + +sub historically_prefer ($@) { + my ($signum, @choices) = @_; + + return $choices[0] if @choices < 2; + + my $compare = sub { + foreach my $sr (@stagerecord) { + my $d = $sr->{ $a->{Cand} } <=> $sr->{ $b->{Cand} }; + return $d * $signum if $d; + } + return 0; + }; + + @choices = sort $compare, @choices; + $a = $choices[0]; + my $numequal = 0; + for (;;) { + last unless $numequal >= @choices; + $b = $choices[$numequal]; + last if $compare->(); + } + + die 'random choice unimplemented' if $numequal > 1; + return $choices[0]; +} + +foreach my $c (keys %continuing) { + $sorted{$c} = { + Cand => $c, + Votes => [], + }; } for (;;) { $stage++; - my %sorted; - # $sorted{firstpref}{Votes} = [ $vote, ... ] - # $sorted{firstpref}{Total} = [ $vote, ... ] - for my $vote (@allvotes) { - next unless @{ $vote->{Prefs} }; - my $firstpref = $vote->{Prefs}[0]; - push @{ $sorted{$firstpref}{Votes} }, $vote; + printf "----- stage %d -----\n", $stage; + + print DEBUG "#################### $stage ####################\n", + Data::Dumper->Dump( +[ \@stagerecord, \@elected, \@unsorted, \%sorted, \@surpluses, \%continuing ], +[qw( _@stagerecord _@elected _@unsorted _%sorted _@surpluses _%continuing )] + ); + + my $placesremain = $places - @elected; + + unless ($placesremain > 0) { + printf "Complete.\n"; + last; + } + + while (my $vote = shift @unsorted) { + my ($firstpref) = voteliveprefs $vote; + if (!defined $firstpref) { + votelog $vote, "ballot exhausted"; + push @exhausted, $vote; + } else { + push @{ $sorted{$firstpref}{Votes} }, $vote; + } } foreach my $firstpref (sort keys %sorted) { foreach my $vote (@{ $sorted{$firstpref}{Votes} }) { votelog $vote, "counted $vote->{Weight} for $firstpref"; - $sorted{$firstpref}{Total} += $vote->{Weight}; } } - my @sorted = sort { - $sorted{$b}{Total} <=> $sorted{$a}{Total}; - } keys %sorted; + my @sorted; + my $things_update = sub { + foreach my $s (@surpluses, values %sorted) { + $s->{Total} = sum0 map { $_->{Weight} } @{ $s->{Votes } }; + } + @sorted = nsort_by { -$_->{Total} } values %sorted; + }; + $things_update->(); + + print DEBUG "SORTED\n", Dumper(\@sorted); + + push @stagerecord, { map { ($_->{Cand}, $_->{Total}) } @sorted }; my $totalvalid = 0; - foreach my $firstpref (@sorted) { - $sorted{$firstpref}{Total} += $_->{Weight} - foreach @{ $sorted{$firstpref}{Votes} }; - printf "%10s %s\n", $firstpref, pr $sorted{$firstpref}{Total}; - $totalvalid += $sorted{$firstpref}{Total}; + my $countvalid = sub { + my ($l, $what) = @_; + foreach my $s (@$l) { + printf "%-7s %10s %s\n", $what, $s->{Cand}, pr $s->{Total}; + $totalvalid += $s->{Total}; + } + }; + $countvalid->(\@sorted, '1stpref'); + $countvalid->(\@surpluses, 'surplus'); + + printf "%7s %10s %s\n", 'TOTAL', '-----', pr $totalvalid; + + unless ($totalvalid > 0) { + printf "No more votes!\n"; + last; + } + + my $quota = $totalvalid / ($placesremain + 1); + printf "%7s %10s %s\n", 'quota', '', pr $quota; + + my $need_to_transfer_surplus = 1; + + # Look for people to elect. + # We elect as many as we can, rather than recomputing the (lower) quota + # (ERS rules 5.4.9) + for (;;) { + my $s = $sorted[0]; + my $topvoters = $s->{Total}; + my $surplus = $topvoters - $quota; + last unless $surplus > 0; + + printf "%7s %10s ***************\n", 'ELECTED', $s->{Cand}; + push @elected, $s->{Cand}; + + my $derate = $topvoters / $surplus; + printf "%7s %10s %s\n", 'derate', $s->{Cand}, pr $derate; + + foreach my $vote (@{ $s->{Votes} }) { + votelog $vote, "elected $s->{Cand}, derated $derate"; + $vote->{Weight} /= $derate; + } + push @surpluses, $s; + delete $sorted{ $s->{Cand} }; + delete $continuing{ $s->{Cand} }; + + $things_update->(); + printf "%7s %10s %s\n", 'surplus', $s->{Cand}, pr $s->{Total}; + + $need_to_transfer_surplus = 0; + # before actually transferring a surplus, we will consider + # eliminating, and then reconsider with a lower quota } - printf "%10s %s\n", 'TOTAL', pr $totalvalid; - last unless $totalvalid > 0; + my $deferredsurplus = sum0 map { $_->{Total} } @surpluses; + printf "%18s %s\n", 'deferred surplus', pr $deferredsurplus; - my $quota = $totalvalid / ($places + 1); - printf "\nquota = %10s\n", pr $quota; + # Look for people to eliminate + # We eliminate before trying to transfer surpluses + # ERS 5.2.5 + my $elimvotebefore = 0; + for (;;) { + last unless @sorted; - my $topfirst = $sorted[0]; - my $topvoters = $sorted{$topfirst}{Weight}; - my $surplus = $topvoters - $quota; - if ($surplus >= 0) { - printf "%10s ELECTED\n", $sorted[0]; + if ($elimvotebefore) { + printf "%18s %s\n", 'elimination, sofar', pr $elimvotebefore; + } elsif (@surpluses) { + printf "%18s\n", 'elimination, maybe'; + } else { + printf "%18s\n", 'elimination, starts'; + } + + my @elim = equalpiles 'elim?', reverse @sorted; + my $elimvotenow = sum0 map { $_->{Total} } @elim; + + if (@surpluses || $elimvotebefore) { + # rule 5.2.2 + if (@sorted == @elim) { + printf "%18s\n", 'no-elim (all-equal)'; + last; + } + my $nextup = $sorted[ $#sorted - @elim ]; + printf "%7s %10s %s\n", 'nextup', $nextup->{Cand}, + pr $nextup->{Total}; + my $aheadby = $nextup->{Total} - ($elimvotenow + $elimvotebefore); + unless ($deferredsurplus <= $aheadby) { + # rule 5.2.2 (b) + printf "%18s %s\n", 'no-elim (nextup)', pr $aheadby; + last; + } + } - # transfer the surplus - my $weightmult = $surplus / $topvoters; - foreach my $vote (@{ $sorted{$topfirst}{Votes} }) { - votelog $vote, "part of surplus of weight $weightmult"; - $vote->{Weight} *= $weightmult; + my $elim_tie = + @elim > 1 && + (scalar keys %continuing) - (scalar @elim) < $placesremain; + if ($elim_tie) { + # eliminate only one then, and try again + printf "elim-tie!\n"; + @elim = historically_prefer -1, @elim; } - filterout $topfirst, "candidate was elected"; - } else { - my $elim = $sorted{ $#sorted }; - printf "%10s eliminated\n", $elim; - filterout $elim, "candidate was eliminated"; + + foreach my $s (@elim) { + my $c = $s->{Cand}; + printf "%7s %10s %s\n", 'ELIM', $c, '----------'; + my $votes = $s->{Votes}; + votelogfull $_, "failed to stop $c elimination" foreach @$votes; + $elimvotebefore += $s->{Total}; + delete $continuing{$c}; + delete $sorted{$c}; + push @unsorted, @$votes; + } + + $things_update->(); + $need_to_transfer_surplus = 0; + last if $elim_tie; # no more, then! } + + next unless $need_to_transfer_surplus; + + @surpluses = nsort_by { $_->{Total} } @surpluses; + my @surplusxfer = equalpiles 'xfer?', @surpluses; + die unless @surplusxfer; + + if (@surplusxfer > 1) { + @surplusxfer = historically_prefer +1, @surplusxfer; + } + + my $s = $surplusxfer[0]; + my $c = $s->{Cand}; + printf "%7s %10s\n", 'xfer', $c; + my $votes = $s->{Votes}; + votelogfull $_, "surplus transferred" foreach @$votes; + @surpluses = grep { $_->{Cand} ne $c } @surpluses; + push @unsorted, @$votes; } + +print "done.\n";