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