From 4dbe0e2709b218d561392fe5496109b6bd346651 Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Sun, 21 Aug 2016 17:08:14 +0100 Subject: [PATCH] compute-scottish-stv: wip Signed-off-by: Ian Jackson --- compute-scottish-stv | 150 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 136 insertions(+), 14 deletions(-) diff --git a/compute-scottish-stv b/compute-scottish-stv index 77e00da..bb91c68 100644 --- a/compute-scottish-stv +++ b/compute-scottish-stv @@ -4,22 +4,27 @@ # The Scottish Local Government Elections Order 2007 use strict; +use bigrat; # Data structures: # # vote is # { Voter => opaque, -# Prefs => [ list ], +# Prefs => [ [ CAND, ...], ... ], # Weight => 1.0 } # We edit Prefs as we go # $cands{CAND}{Desc} # $cands{CAND}{Votes} - -our $stage=0; +# $cands{CAND}{Total} +# $cands{CAND}{Continuing} our $seats=0; +our $stage=0; +our $quota; +our %tie; + sub unkopt ($$) { my ($what,$opt) = @_; if ($opt =~ m/^[A-Z]/) { @@ -35,6 +40,12 @@ for (;;) { foreach $_ (split / /, $') { if (m/^_?[Ss]eats=(\d+)/) { $seats = $1; + } elsif (m/^_?[Tt]ie=(.*)\>(.*)$/) { + my @more = split /\,/, $1; + my @less = split /\,/, $2; + my @all = join ',', sort (@more, @less); + $tie{"@all"}{Win} = $more[0] if @more == 1; + $tie{"@all"}{Lose} = $less[0] if @less == 1; } else { unkopt "election", $_; } @@ -65,6 +76,8 @@ for (;;) { } } +$_->{Continuing} = 1 foreach values %cands; + sub sortballots (@) { # Takes each argument, which should be a ballot, sorts # it into $cand{CAND}{Votes} according to first preference. @@ -73,6 +86,7 @@ sub sortballots (@) { # and looks for further preferences. foreach my $v (@_) { my $firstprefs = shift @{ $v->{Prefs} }; + my $w = $v->{Weight}; if (!$firstprefs || !@$firstprefs) { vlog $v, "no more preferences, non transferable"; push @non_transferable, $v; @@ -83,20 +97,128 @@ sub sortballots (@) { foreach my $fpref (@$firstprefs) { my $v2 = { %$v, - Weight => $v->{Weight} / @$firstprefs, + Weight => $w / @$firstprefs, Prefs => [ [ $fpref ], @{ $v->{Prefs} } ], - }; + }; vlog $v, "split for $fpref"; - - - Voter => $ - - my @prefs - my $nprefs = scalar @{ $v->{Prefs} - my @input = @_; - + } + next; + } + my $fp = $firstprefs[0]; + my $c = $cands{$fp}; + if (!$c->{Continuing}) { + vlog $v, "dropping pref $fp, not a continuing candidate"; + sortvallots $v; + next; + } + vlog $v, "sorted into pile for candidate $fp weight $w"; + push @{ $c->{Votes} }, $v; + } +} + +sub prf { + my $fmt = shift; + printf "stage %d: ".$_, $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}; + next unless $c->{Continuing}; + prf "cand %s: %s votes\n", $stage, $cand, $c->{Total}; + $c->{History}[$stage-1] = $c->{Total}; + } +} + +sub computequota () { + my $totalvalid = 0; + $totalvalid += $_->{Total} foreach keys %cands; + $quota = floor($totalvalid / (1 + $seats)); +} -# $cands +sub total_history_cmp () { + my $ha = $cands{a}{History}; + my $hb = $cands{a}{History}; + foreach my $s (reverse 1 .. $stage) { + my $d = $ha->[$s] <=> $hb->[$s]; + next unless $d; + print DEBUG "history cmp $a $b => $d (#$s $ha->[$s] $hb->[$s])\n"; + return $e; + } + return 0; +} + +sortballots @allvotes; for (;;) { + $stage++; + countballots(); + + my @maybe_elect = reverse sort total_history_cmp keys %cands; + my $nelect=0; + for (;;) { + my $nextcand = $maybe_elect[$nelect]; + my $nextc = $cands{$nextcand}; + + # 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], $nextcand) > 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 $elect; + if ($nelect > 1) { + my @all = @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", + $elect, "@all"; + } else { + $elect = $maybe_elect[0]; + prf "electing %s\n"; + } + + prf "*** ELECT %s ***\n", $elect; + $c->{Continuing} = 0; + votelog $_, "helped elect $elect" foreach @{ $c->{Votes} }; + + # SLGEO 48 + my $c = $cands{$elect}; + my $surplus = $c->{Total} - $quota; + + if ($surplus <= 0) { + prf "no surplus\n"; + next; + } + + my $B = $c->{Weight}; + + foreach my $v (@{ $c->{Votes} }) { + my $A = $surplus * $v->{Weight}; + my $F = 100000; + my $xfervalue = floor(($A * $F) / $B) / $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; + sortballots $v; + } + $c->{Votes} = { }; # will crash if we access it again + next; + } + -- 2.30.2