chiark / gitweb /
a58e2505cc5b7f3006732ede785763be54d6b5f9
[appendix-a6.git] / compute
1 #!/usr/bin/perl -w
2 #
3 # parses some input about ballots and computes the result according
4 # to Debian Constitution A.6.
5 #
6 # usage: .../compute [<input files>...]
7 #
8
9
10 use strict;
11 use utf8;
12 use autodie;
13
14 use Data::Printer;
15 use Graph::Directed;
16
17 binmode STDIN, 'encoding(UTF-8)';
18 binmode STDOUT, 'encoding(UTF-8)';
19 binmode STDERR, 'encoding(UTF-8)';
20
21 our @choices;
22 our %choices;
23 our @invotes_v;
24 our @invotes_cc;
25 our $defcho;
26 our $quorum = 0;
27
28 sub addchoice {
29     my $choname = shift @_;
30     my $cho = $choices{$choname} = {
31         @_, Index => (scalar @choices)
32     };
33     push @choices, $choname;
34     return $cho;
35 }
36
37 while (<>) {
38     s/\s+$//;
39     next if m/^\s*\#/;
40     next unless m/\S/;
41     next if m/^\s/;
42     if (m/^([A-Z0-9]+)\s*\=\s*(\S.*)$/) {
43         my ($choname, $desc) = ($1,$2);
44         my $cho = addchoice($choname, Desc => $desc);
45         if ($desc =~ m/\[(\d+):(\d+)\]/) {
46             $cho->{Smaj} = [$1,$2];
47         } elsif ($desc =~ m/\[default\]/) {
48             $defcho = $cho;
49         }
50     } elsif (m/^V:\s+(\S+)\s+(\S.*)/) {
51         push @invotes_v, [ $1, $2 ];
52     } elsif (m/^(\S+)\s*\:\:\s*/) {
53         push @invotes_cc, [ $1, "$'" ];
54     } elsif (m/^quorum = ([0-9.]+)$/) {
55         $quorum = $1+0.0;
56     } elsif (m/^nodefault$/) {
57         $defcho = { Index => -1 }
58     } else {
59         die "invalid input";
60     }
61 }
62
63 our @vab;
64 # $vab[$ia][$ib] = V(A,B)
65 # Actually, it's a list of voters who prefer A to B (A.6(3)(1))
66
67 # Go through the voters and construct V(A,B)
68
69 print "\nParsing \`simple' style ballots\n# devotee-tally-begin\n"
70         if @invotes_cc;
71 # actually, we pre-parse them into @invotes_v
72 # since we want to show them as a tally sheet anyway
73
74 foreach my $iv (@invotes_cc) {
75     $_ = uc $iv->[1];
76     foreach my $chn (m/\b\w+\b/g) {
77         next if $choices{$chn};
78         addchoice($chn, Desc => "($chn from voter ballot)");
79     }
80 }
81
82 foreach my $iv (@invotes_cc) {
83     my ($voter) = $iv->[0];
84     $_ = uc $iv->[1];
85
86     s/\t/ /g;
87     s/\,/ /g;
88     while (s{\(([^()]+)\)}{
89         my $x = $1; $x =~ s/[ =]+/=/g; $x;
90     }ge) { }
91     s/[ =]*=[ =*]/=/g;
92     s/\s+/ /g;
93     print "# normalised $_ ($voter)\n";
94
95     my @ranks = (1000,) x @choices;
96     my $rank = 1;
97     foreach (split /\s+/) {
98         foreach (split /=/) {
99             my $cho = $choices{$_};
100             $cho or die "unknown choice $_ ($voter)";
101             my $ix = $cho->{Index};
102             $ranks[$ix] = $rank;
103         }
104         $rank++;
105     }
106     my $vstr = join '', map {
107         $_ == 1000 ? "-" :
108         $_ < 10 ? (sprintf "%d", $_) :
109         $_ < 36 ? (chr(ord('A') + $_ - 10)) : die
110     } @ranks;
111     print "V: $vstr $voter\n";
112     push @invotes_v, [ $vstr, $voter ];
113 }
114
115 print "# devotee-tally-end\n"
116         if @invotes_cc;
117
118 print "\nDetermining default option\n";
119
120 if ($defcho && $defcho->{Index} > -1) {
121     print "default option was specified: $choices[$defcho->{Index}]\n";
122 } elsif ($defcho) {
123     print "no default option\n";
124 } else {
125     foreach my $try (qw(FD SQ NOTA)) {
126         $defcho = $choices{$try};
127         last if $defcho;
128     }
129     if ($defcho) {
130         print "guessed default option: $choices[$defcho->{Index}]\n";
131     } else {
132         print "could not guess default option, assuming there is none\n";
133     }
134 }
135
136 my $defi = $defcho->{Index};
137 die "FD has smaj?!" if $defcho->{Smaj};
138
139 print "\nParsing devotee tally sheet ballots\n" 
140     if @invotes_v > @invotes_cc;
141
142 foreach my $iv (@invotes_v) {
143     my ($votestr,$voter) = @$iv;
144     eval {
145         length $votestr eq @choices or die "wrong vote vector length";
146         my @vs = split //, $votestr;
147         foreach my $ix (0..$#vs) {
148             my $vchr = $vs[$ix];
149             if ($vchr eq '-') {
150                 $vs[$ix] = 1000;
151             } elsif ($vchr =~ m/[0-9a-z]/) {
152                 $vs[$ix] = ord($vchr);
153             } else {
154                 die "bad vote char";
155             }
156         }
157         foreach my $ia (0..$#vs) {
158             foreach my $ib ($ia+1..$#vs) {
159                 my $va = $vs[$ia];
160                 my $vb = $vs[$ib];
161                 if ($va < $vb) { push @{ $vab[$ia][$ib] }, $voter }
162                 elsif ($vb < $va) { push @{ $vab[$ib][$ia] }, $voter }
163             }
164         }
165     };
166     die "voter $voter $@" if $@;
167 }
168
169 print "\nPreference matrix\n";
170
171 our @ch = map { $choices{$_} } @choices;
172
173 # Print the counts V(A,B)
174 foreach my $iy (-2..$#ch) {
175     foreach my $ix (-2..$#ch) {
176         if ($iy==-1) {
177             if ($ix==-1) {
178                 printf "+";
179             } else {
180                 printf "------";
181             }
182         } elsif ($ix==-1) {
183             printf "|";
184         } elsif ($ix==-2 && $iy==-2) {
185             printf "V(Y,X)";
186         } elsif ($iy==-2) {
187             printf "%5s ", $choices[$ix];
188         } elsif ($ix==-2) {
189             printf "%5s ", $choices[$iy];
190         } else {
191             my $v = \( $vab[$iy][$ix] );
192             $$v ||= [ ];
193             if (@$$v) {
194                 printf "%5d ", (scalar @$$v);
195             } else {
196                 printf "%5s ", "";
197             }
198         }
199     }
200     printf "\n";
201 }
202
203 sub drop ($$) {
204     my ($i,$why) = @_;
205     print "dropping $choices[$i]: $why\n";
206     $ch[$i]{Dropped} = $why;
207 }
208
209 if (defined $defi) {
210     print "\nQuorum A.6(2) (quorum is $quorum)\n";
211
212     foreach my $i (0..$#choices) {
213         next if $ch[$i]{Dropped};
214         next if $i == $defi;
215         my $v = $vab[$i][$defi];
216         next if $v >= $quorum;
217         drop $i, "quorum ($v < $quorum)";
218     }
219
220     print "\nMajority ratio A.6(3)\n";
221
222     foreach my $i (0..$#choices) {
223         next if $ch[$i]{Dropped};
224         next if $i == $defi;
225         my $majr = $ch[$i]{Smaj};
226         $majr ||= [1,1]; # A.6(3)(3)
227         my $vad = scalar @{ $vab[$i][$defi] };
228         my $vda = scalar @{ $vab[$defi][$i] };
229         next if $vad * $majr->[1] > $vda * $majr->[0];
230         drop $i, "majority ratio ($vad:$vda <= $majr->[1]:$majr->[0])";
231     }
232 }
233
234 print "\nDefeats A.6(4)\n";
235
236 my $defeats = Graph::Directed->new;
237
238 sub chremain () {
239     return grep { !$ch[$_]{Dropped} } (0..$#ch);
240 }
241
242 foreach my $ia (chremain()) {
243     $defeats->add_vertex($choices[$ia]);
244     foreach my $ib (chremain()) {
245         my $vab = scalar @{ $vab[$ia][$ib] };
246         my $vba = scalar @{ $vab[$ib][$ia] };
247         next unless $vab > $vba;
248         my $diff = $vab - $vba;
249         print "defeat: $choices[$ia] beats $choices[$ib]",
250             " ($vab > $vba = +$diff)\n";
251         $defeats->add_edge($choices[$ia],$choices[$ib]);
252     }
253 }
254
255 sub chvab ($$) {
256     my ($ca,$cb) = @_;
257     my $v = $vab[ $choices{$ca}{Index} ][ $choices{$cb}{Index} ];
258     return scalar @$v;
259 }
260
261 sub weaker ($$) {
262     # A.6(7)(1)
263     my ($def1,$def2) = @_;
264     my ($ca,$cx) = @$def1;
265     my ($cb,$cy) = @$def2;
266     return 1 if chvab($ca, $cx) < chvab($cb, $cy);
267     return 1 if chvab($ca, $cx) == chvab($cb, $cy)
268               && chvab($cx, $ca) > chvab($cy, $cb);
269     return 0;
270 }
271
272 our $schwartz;
273
274 for (;;) {
275     # loop from A6(5)
276
277     print "defeats graph: $defeats\n";
278
279     print "\nTransitive closure A.6(5)\n";
280
281     my $tdefeats = $defeats->transitive_closure();
282
283     # this makes the debugging output prettier
284     foreach my $ch (@choices) {
285         $tdefeats->delete_edge($ch,$ch);
286     }
287     print "closure graph: $tdefeats\n";
288
289     print "\nSchwartz set A.6(6)\n";
290     
291     $schwartz = $defeats->copy();
292
293     foreach my $ia (chremain()) {
294         foreach my $ib (chremain()) {
295             next if $tdefeats->has_edge($choices[$ia],$choices[$ib]);
296             next if !$tdefeats->has_edge($choices[$ib],$choices[$ia]);
297             print "not in Schwartz set: $choices[$ia] because $choices[$ib]\n";
298             $schwartz->delete_vertex($choices[$ia]);
299             last;
300         }
301     }
302
303     print "set: ", (join " ", $schwartz->vertices()), "\n";
304
305     print "\nDropping weakest defeats A.6(7)\n";
306
307     our @weakest = ();
308
309     foreach my $edge ($schwartz->edges()) {
310 #       print " considering @$edge\n";
311         if (!@weakest) {
312             # no weakest edges yet
313         } elsif (weaker($edge, $weakest[0])) {
314             # this edge is weaker than previous weakest, start new set
315             @weakest = ();
316         } elsif (weaker($weakest[0], $edge)) {
317             # weakest edge is weaker than this one, ignore this one
318             next;
319         } else {
320             # weakest edge is exactly as weak as this one, add this one
321         }
322         push @weakest, $edge;
323     }
324
325     last unless @weakest;
326
327     my $w = $weakest[0];
328     printf "weakest defeats are %d > %d\n", 
329         chvab($w->[0], $w->[1]),
330         chvab($w->[1], $w->[0]);
331     foreach my $weakest (@weakest) {
332         my ($ca,$cb) = @$weakest;
333         print "a weakest defeat is $ca > $cb\n";
334         $defeats->delete_edge($ca,$cb);
335     }
336
337     print "\nDefeats within the Schwartz set, go round again\n";
338 }
339
340 print "no defeats within the Schwartz set\n";
341 print "final schwartz set:\n\n";
342
343 if ($schwartz->vertices() == 1) {
344     print "WINNER IS:\n";
345 } else {
346     print "WINNER IS ONE OF (CASTING VOTE DECIDES):\n";
347 }
348
349 printf "    %-5s %s\n", $_, $choices{$_}{Desc}
350     foreach ($schwartz->vertices());
351
352 print ".\n";
353
354 #p %choices;
355 #p @vab;