chiark / gitweb /
bb91c681db0c13c9e69ab23517a744686783e77d
[appendix-a6.git] / compute-scottish-stv
1 #!/usr/bin/perl -w
2
3 # Reference:
4 # The Scottish Local Government Elections Order 2007
5
6 use strict;
7 use bigrat;
8
9 # Data structures:
10 #
11 # vote is
12 #   { Voter => opaque,
13 #     Prefs => [ [ CAND, ...], ... ],
14 #     Weight => 1.0 }
15 # We edit Prefs as we go
16
17 # $cands{CAND}{Desc}
18 # $cands{CAND}{Votes}
19 # $cands{CAND}{Total}
20 # $cands{CAND}{Continuing}
21
22 our $seats=0;
23
24 our $stage=0;
25 our $quota;
26 our %tie;
27
28 sub unkopt ($$) {
29     my ($what,$opt) = @_;
30     if ($opt =~ m/^[A-Z]/) {
31         die "unknown option $_ for $what";
32     } elsif ($opt =~ m/^[a-z]/) {
33         print STDERR "warning (line $.): unknown option $_ for $what\n";
34     }
35 }
36
37 for (;;) {
38     $_ = <>;
39     if (m/^\| /) {
40         foreach $_ (split / /, $') {
41             if (m/^_?[Ss]eats=(\d+)/) {
42                 $seats = $1;
43             } elsif (m/^_?[Tt]ie=(.*)\>(.*)$/) {
44                 my @more = split /\,/, $1;
45                 my @less = split /\,/, $2;
46                 my @all = join ',', sort (@more, @less);
47                 $tie{"@all"}{Win}  = $more[0] if @more == 1;
48                 $tie{"@all"}{Lose} = $less[0] if @less == 1;
49             } else {
50                 unkopt "election", $_;
51             }
52         }
53     } elsif (m/^(\w+) = (\S*) \|/) {
54         my ($cand,$desc) = @_;
55         unkopt "candidate $cand", $_ foreach split / /, $';
56         $cands{$cand}{Desc} = $desc;
57     } elsif (m/^(\w*) :\s*(.*)\s*\|(.*)/) {
58         my ($voter,$prefs,$opts) = ($1,$2,$3);
59         $v = { Voter => $voter };
60         push @{ $v->{Prefs} }, [ $_ =~ m/\w+/g ]
61             foreach split /\s+/, $prefs;
62         foreach $_ (split / /, $opts) {
63             if (m/^_?[Ww]eight=(\d+)/(\d+)$/) {
64                 $v->{Weight} = $1 / $2;
65             } elsif (m/^_?[Ww]eight=([0-9.]+)$/) {
66                 $v->{Weight} = new Math::BigRat $1;
67             } else {
68                 unkopt "voter $v->{Voter}", $_;
69             }
70         }
71         push @allvotes, $v;
72     } elsif (m/^\.$/) {
73         last;
74     } else {
75         die "$_ ?";
76     }
77 }
78
79 $_->{Continuing} = 1 foreach values %cands;
80
81 sub sortballots (@) {
82     # Takes each argument, which should be a ballot, sorts
83     # it into $cand{CAND}{Votes} according to first preference.
84     # Strips that first preference from the ballot.
85     # If the first preference has been eliminated, strips it
86     # and looks for further preferences.
87     foreach my $v (@_) {
88         my $firstprefs = shift @{ $v->{Prefs} };
89         my $w = $v->{Weight};
90         if (!$firstprefs || !@$firstprefs) {
91             vlog $v, "no more preferences, non transferable";
92             push @non_transferable, $v;
93             next;
94         }
95         if (@$firstprefs > 1) {
96             vlog $v, "splitting due to several equal first preferences";
97             foreach my $fpref (@$firstprefs) {
98                 my $v2 = {
99                     %$v,
100                     Weight => $w / @$firstprefs,
101                     Prefs => [ [ $fpref ], @{ $v->{Prefs} } ],
102                 };
103                 vlog $v, "split for $fpref";
104             }
105             next;
106         }
107         my $fp = $firstprefs[0];
108         my $c = $cands{$fp};
109         if (!$c->{Continuing}) {
110             vlog $v, "dropping pref $fp, not a continuing candidate";
111             sortvallots $v;
112             next;
113         }
114         vlog $v, "sorted into pile for candidate $fp weight $w";
115         push @{ $c->{Votes} }, $v;
116     }
117 }
118
119 sub prf {
120     my $fmt = shift;
121     printf "stage %d: ".$_, $stage, @_;
122 }
123
124 sub countballots () {
125     foreach my $c (values %cand) {
126         $c->{Total} = 0;
127         $c->{Total} += $_->{Weight} foreach @{ $c->{Voters} };
128     }
129     foreach my $cand (sort keys %cand) {
130         $c = $cands{$cand};
131         next unless $c->{Continuing};
132         prf "cand %s: %s votes\n", $stage, $cand, $c->{Total};
133         $c->{History}[$stage-1] = $c->{Total};
134     }
135 }
136
137 sub computequota () {
138     my $totalvalid = 0;
139     $totalvalid += $_->{Total} foreach keys %cands;
140     $quota = floor($totalvalid / (1 + $seats));
141 }
142
143 sub total_history_cmp () {
144     my $ha = $cands{a}{History};
145     my $hb = $cands{a}{History};
146     foreach my $s (reverse 1 .. $stage) {
147         my $d = $ha->[$s] <=> $hb->[$s];
148         next unless $d;
149         print DEBUG "history cmp $a $b => $d (#$s $ha->[$s] $hb->[$s])\n";
150         return $e;
151     }
152     return 0;
153 }
154
155 sortballots @allvotes;
156
157 for (;;) {
158     $stage++;
159     countballots();
160     
161     my @maybe_elect = reverse sort total_history_cmp keys %cands;
162     my $nelect=0;
163     for (;;) {
164         my $nextcand = $maybe_elect[$nelect];
165         my $nextc = $cands{$nextcand};
166
167         # We certainly only consider those who meet quota
168         last unless $nextc->{Total} >= $quota;
169         last unless $nextc->{Total} > $quota && $nextel;
170         # ... if equal we can do them one by one, since order
171         # does not matter (SLGEO 49 talks about `two or more ... exceeds')
172
173         last if $nelect &&
174             (total_history_cmp $maybe_elect[0], $nextcand) > 0;
175         # ... only interested in those who compare equal
176         # according ot the history (SLGEO 49(2)); NB our history
177         # includes the current round.
178
179         $nelect++;
180     }
181
182     if ($nelect) {
183         my $elect;
184         if ($nelect > 1) {
185             my @all = @maybe_elect[0 .. $nelect-1];
186             my $elect = $tie{"@all"}{Win};
187             die "need tie break, want winner from @all" unless defined $win;
188             prf "electing %s due to tie break amongst %s\n",
189                 $elect, "@all";
190         } else {
191             $elect = $maybe_elect[0];
192             prf "electing %s\n";
193         }
194
195         prf "*** ELECT %s ***\n", $elect;
196         $c->{Continuing} = 0;
197         votelog $_, "helped elect $elect" foreach @{ $c->{Votes} };
198         
199         # SLGEO 48
200         my $c = $cands{$elect};
201         my $surplus = $c->{Total} - $quota;
202
203         if ($surplus <= 0) {
204             prf "no surplus\n";
205             next;
206         }
207
208         my $B = $c->{Weight};
209
210         foreach my $v (@{ $c->{Votes} }) {
211             my $A = $surplus * $v->{Weight};
212             my $F = 100000;
213             my $xfervalue = floor(($A * $F) / $B) / $f;
214             # SLGEO 48(3): we do arithmetic to 5 d3ecimal places,
215             # but always rounding down
216             votelog $v, "transferring with value $xfervalue (A=$A B=$B)";
217             $v->{Weight} = $xfervalue;
218             sortballots $v;
219         }
220         $c->{Votes} = { }; # will crash if we access it again
221         next;
222     }
223
224