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