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