X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?p=appendix-a6.git;a=blobdiff_plain;f=compute-scottish-stv;h=6fde10308b409753d9f877e1df709300d2558fa3;hp=78e62fb56bb7e4651f57b10d1d228a1030c429da;hb=a4f2fc687bb9e16c33a59619acafe2cc21833ee1;hpb=2ca665096a98105791c0d59b9bb0fae765c909b4 diff --git a/compute-scottish-stv b/compute-scottish-stv old mode 100644 new mode 100755 index 78e62fb..6fde103 --- a/compute-scottish-stv +++ b/compute-scottish-stv @@ -4,6 +4,9 @@ # The Scottish Local Government Elections Order 2007 use strict; +use Carp; +use Data::Dumper; +use Math::BigRat; use bigrat; # Data structures: @@ -22,10 +25,25 @@ use bigrat; our $seats=0; +our @allvotes; +our @non_transferable; +our %cands; our $stage=0; our $quota; our %tie; +open DEBUG, ">.compute.log" or die $!; +DEBUG->autoflush(1); + +$SIG{__WARN__} = sub { + print DEBUG Dumper(\%tie, + \@non_transferable, + \%cands, + \@allvotes, + $stage, $quota); + confess $_[0]; +}; + sub unkopt ($$) { my ($what,$opt) = @_; if ($opt =~ m/^[A-Z]/) { @@ -52,16 +70,16 @@ for (;;) { } } } elsif (m/^(\w+) = (\S*) \|/) { - my ($cand,$desc) = @_; + my ($cand,$desc) = ($1,$2); unkopt "candidate $cand", $_ foreach split / /, $'; $cands{$cand}{Desc} = $desc; } elsif (m/^(\w*) :\s*(.*)\s*\|(.*)/) { my ($voter,$prefs,$opts) = ($1,$2,$3); - $v = { Voter => $voter }; + my $v = { Voter => $voter, Prefs => [ ] }; push @{ $v->{Prefs} }, [ $_ =~ m/\w+/g ] foreach split /\s+/, $prefs; foreach $_ (split / /, $opts) { - if (m/^_?[Ww]eight=(\d+)/(\d+)$/) { + if (m/^_?[Ww]eight=(\d+)\/(\d+)$/) { $v->{Weight} = $1 / $2; } elsif (m/^_?[Ww]eight=([0-9.]+)$/) { $v->{Weight} = new Math::BigRat $1; @@ -77,8 +95,16 @@ for (;;) { } } -$cands{$_}{Cand} = $_foreach keys %cands; +$cands{$_}{Cand} = $_ foreach keys %cands; +$_->{Weight} //= 1/1 foreach @allvotes; +$_->{TransferredSurplus} //= [ ] foreach @allvotes; + +sub votelog ($$) { + my ($vote,$m) = @_; + push @{ $vote->{Log} }, "stage $stage: $m"; +} +sub sortballots (@); sub sortballots (@) { # Takes each argument, which should be a ballot, sorts # it into $cand{CAND}{Votes} according to first preference. @@ -89,88 +115,148 @@ sub sortballots (@) { my $firstprefs = shift @{ $v->{Prefs} }; my $w = $v->{Weight}; if (!$firstprefs || !@$firstprefs) { - vlog $v, "no more preferences, non transferable"; + votelog $v, "no more preferences, non transferable"; push @non_transferable, $v; next; } if (@$firstprefs > 1) { - vlog $v, "splitting due to several equal first preferences"; + votelog $v, "splitting due to several equal first preferences"; foreach my $fpref (@$firstprefs) { my $v2 = { %$v, Weight => $w / @$firstprefs, Prefs => [ [ $fpref ], @{ $v->{Prefs} } ], }; - vlog $v, "split for $fpref"; + votelog $v, "split for $fpref"; } next; } - my $fp = $firstprefs[0]; + my $fp = $firstprefs->[0]; my $c = $cands{$fp}; my $noncont = $c->{NonCont}; if ($noncont) { - vlog $v, "dropping pref $fp, $noncont"; - sortvallots $v; + votelog $v, "dropping pref $fp, $noncont"; + sortballots $v; next; } - vlog $v, "sorted into pile for candidate $fp weight $w"; + votelog $v, "sorted into pile for candidate $fp weight $w"; push @{ $c->{Votes} }, $v; } } sub prf { my $fmt = shift; - printf "stage %d: ".$_, $stage, @_; + printf "stage %d: ".$fmt, $stage, @_; } sub countballots () { - foreach my $c (values %cand) { - $c->{Total} = 0; - $c->{Total} += $_->{Weight} foreach @{ $c->{Voters} }; - } - foreach my $cand (sort keys %cand) { - $c = $cands{$cand}; + foreach my $cand (sort keys %cands) { + my $c = $cands{$cand}; next if $c->{NonCont}; - prf "cand %s: %s votes\n", $stage, $cand, $c->{Total}; + $c->{Total} = 0; + $c->{Total} += $_->{Weight} foreach @{ $c->{Votes} }; + prf "candidate %-10s: %10s votes\n", $cand, $c->{Total}; $c->{History}[$stage-1] = $c->{Total}; } } sub computequota () { my $totalvalid = 0; - $totalvalid += $_->{Total} foreach keys %cands; - $quota = floor($totalvalid / (1 + $seats)); + $totalvalid += $_->{Total} foreach values %cands; + $quota = ($totalvalid / (1 + $seats)) -> bfloor(); + prf "quota %10s\n", $quota; } sub total_history_cmp () { my $ha = $a->{History}; my $hb = $b->{History}; - foreach my $s (reverse 1 .. $stage) { + foreach my $s (reverse 0 .. $stage-1) { my $d = $ha->[$s] <=> $hb->[$s]; next unless $d; - print DEBUG "history cmp $a->{Cand} $b->[Cand}" - ." => $d (#$s $ha->[$s] $hb->[$s])\n"; - return $e; + print DEBUG "history cmp $a->{Cand} $b->{Cand}". + " => $d (#$s $ha->[$s] $hb->[$s])\n"; + return $d; } return 0; } +sub continuing () { + grep { !$_->{NonCont} } values %cands; +} + +sub select_best_worst ($$$$) { + my ($wantcand, $wanttiebreak, $signum, $what) = @_; + # $wantcand->($c) = boolish + # $wanttiebreak->($total) = boolish + # Firstly candidates not meeting $wantcand are ignored + # Then we pick the best (worst) candiate by Total (or vote history). + # (SLGEO 49(2) and 51(2). + # If this does not help then totals are equal and we call wanttiebreak. + # If it returns 0 we return alphabetically first CAND. Otherwise + # we tie break. + + my @maybe = grep { $wantcand->($_) } continuing(); + @maybe = sort total_history_cmp @maybe; + @maybe = reverse @maybe if $signum > 0; + + return undef unless @maybe; + + my $nequal = 1; + + for (;;) { + my $nextc = $maybe[$nequal]; + + # Only interested in those who compare equal according to the + # history (SLGEO 49(2)); NB our history includes the current + # round. + + last if $signum*($a = $maybe[0], $b = $nextc, total_history_cmp) > 0; + $nextc++; + } + + if ($nequal > 1 && !$wanttiebreak->($maybe[0]{Total})) { + # ... if equal for election we can do them one by one, since + # order does not matter (SLGEO 49 talks about `two or more + # ... exceeds'). + $nequal = 1; + } + + my $selectcand; + if ($nequal > 1) { + my @all = map { $_->{Cand} } @maybe[0 .. $nequal-1]; + my $tiekey = $signum > 0 ? 'Win' : 'Lose'; + $selectcand = $tie{"@all"}{$tiekey}; + die "need tie break, want $tiekey from @all" + unless defined $selectcand; + prf "$what %s due to tie break amongst %s\n", + $selectcand, "@all"; + } else { + $selectcand = $maybe[0]{Cand}; + prf "$what %s\n", $selectcand; + } + + return $cands{$selectcand}; +} + sub elect_core ($) { my ($c) = @_; prf "*** ELECT %s \`%s' ***\n", $c->{Cand}, $c->{Desc}; $c->{NonCont} = 'Elected'; } +$stage = 1; + sortballots @allvotes; +countballots(); +computequota(); for (;;) { $stage++; - my @continuing = grep { !$_->{NonCont} } values %cands; my $seats_remain = $seats - - grep { $_->{NonCont} eq 'Elected' } values %cands; - if (@continuing <= $seats_remain) { - foreach my $c (@continuing) { + - grep { ($_->{NonCont} // '') eq 'Elected' } values %cands; + if (continuing() <= $seats_remain) { + foreach my $c (continuing()) { prf "electing %s to fill remaining place(s)\n", $c->{Cand}; elect_core $c; } @@ -178,43 +264,15 @@ for (;;) { } countballots(); - - my @maybe_elect = reverse sort total_history_cmp @continuing; - my $nelect=0; - for (;;) { - my $nextc = $maybe_elect[$nelect]; - # We certainly only consider those who meet quota - last unless $nextc->{Total} >= $quota; - last unless $nextc->{Total} > $quota && $nextel; - # ... if equal we can do them one by one, since order - # does not matter (SLGEO 49 talks about `two or more ... exceeds') - - last if $nelect && - (total_history_cmp $maybe_elect[0], $nextc) > 0; - # ... only interested in those who compare equal - # according ot the history (SLGEO 49(2)); NB our history - # includes the current round. - - $nelect++; - } - - if ($nelect) { - my $electcand; - if ($nelect > 1) { - my @all = map { $_->{Cand} } @maybe_elect[0 .. $nelect-1]; - my $elect = $tie{"@all"}{Win}; - die "need tie break, want winner from @all" unless defined $win; - prf "electing %s due to tie break amongst %s\n", - $electcand, "@all"; - } else { - $electcand = $maybe_elect[0]; - prf "electing %s\n"; - } + my $c = select_best_worst + sub { $_->{Total} >= $quota }, + sub { $_ > $quota }, + +1, 'electing'; - my $c = $cands{$electcand}; + if ($c) { elect_core $c; - votelog $_, "helped elect $electcand" foreach @{ $c->{Votes} }; + votelog $_, "helped elect $c->{Cand}" foreach @{ $c->{Votes} }; # SLGEO 48 my $surplus = $c->{Total} - $quota; @@ -224,16 +282,30 @@ for (;;) { next; } - my $B = $c->{Weight}; + my $B = $c->{Total}; + my %tspr; + + prf "surplus %10s\n", $surplus; foreach my $v (@{ $c->{Votes} }) { + my $previously = $v->{TransferredSurplus}; + push @$previously, $c->{Cand}; + my $A = $surplus * $v->{Weight}; my $F = 100000; - my $xfervalue = floor(($A * $F) / $B) / $f; + my $xfervalue = ((($A * $F) / $B) -> bfloor() ) / $F; # SLGEO 48(3): we do arithmetic to 5 d3ecimal places, # but always rounding down votelog $v, "transferring with value $xfervalue (A=$A B=$B)"; $v->{Weight} = $xfervalue; + + if (defined $tspr{"@$previously"}) { + die unless $tspr{"@$previously"} == $xfervalue; + } else { + $tspr{"@$previously"} = $xfervalue; + prf "transfer value of ballots %s: %10s\n", + "@$previously", $xfervalue; + } sortballots $v; } $c->{Votes} = { }; # will crash if we access it again @@ -241,3 +313,18 @@ for (;;) { } # No-one to elect, must eliminate + $c = select_best_worst + sub { 1; }, + sub { 1; }, + -1, 'eliminating'; + + if ($c) { + prf "=== eliminating %s (%s) ===\n", $c->{Cand}, $c->{Desc}; + $c->{NonCont} = 'Eliminated'; + next; + } + + die; +} + +print "done.\n";