diff options
65 files changed, 9886 insertions, 0 deletions
diff --git a/council2007/Votify.pm b/council2007/Votify.pm new file mode 100644 index 0000000..0c5565d --- /dev/null +++ b/council2007/Votify.pm @@ -0,0 +1,683 @@ +# $Id: Votify.pm,v 1.5 2005/05/16 23:58:09 agriffis Exp $ +# +# Copyright 2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# votify.pm: common classes for votify and countify +# + +package Votify; + +use POSIX; +use List::Util; +use strict; + +our ($datadir) = '/home/fox2mike/elections'; +(our $zero = $0) =~ s,.*/,,; + +sub import { + my ($class, $mode) = @_; + $Votify::mode = $mode; +} + +###################################################################### +# OfficialList +###################################################################### + +package OfficialList; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + officials => [], + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/officials-$election") + or die("failed to open officials file"); + chomp(@{$self->{'officials'}} = <F>); + close(F); + + bless $self, $class; + return $self; +} + +sub officials { + my ($self) = @_; + @{$self->{'officials'}}; +} + +###################################################################### +# VoterList +###################################################################### + +package VoterList; + +sub new { + my ($class, $election) = @_; + my (@voterlist, $r); + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/confs-$election", + filename => '', + voters => {}, # confnum => voter + confs => {}, # voter => confnum + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/voters-$election") + or die("failed to open voters file"); + chomp(@voterlist = <F>); + close(F); + + # assign confirmation numbers randomly + for my $v (@voterlist) { + do { $r = int rand 0xffff } while exists $self->{'voters'}{$r}; + $self->{'voters'}{$r} = $v; + $self->{'confs'}{$v} = $r; + } + + unless (keys %{$self->{'voters'}} == keys %{$self->{'confs'}}) { + die("discrepancy deteced in VoterList"); + } + + bless $self, $class; + return $self; +} + +sub confs { + my ($self) = @_; + sort keys %{$self->{'voters'}}; +} + +sub voters { + my ($self) = @_; + sort keys %{$self->{'confs'}}; +} + +sub getvoter { + my ($self, $conf) = @_; + return $self->{'voters'}{$conf}; +} + +sub getconf { + my ($self, $voter) = @_; + return $self->{'confs'}{$voter}; +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c ($self->confs) { + printf F "%04x %s\n", $c, $self->getvoter($c); + } + close F; +} + +###################################################################### +# MasterBallot +###################################################################### + +package MasterBallot; + +use Data::Dumper; + +sub new { + my ($class, $election, $vl) = @_; + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/master-$election", + filename => '', + voterlist => $vl, + ballots => {}, # indexed by conf num + candidates => undef, # indexed by long name + table => undef, # indexed by row+column + }; + + bless $self, $class; + return $self; +} + +sub collect { + my ($self, @voters) = @_; + my ($c, $v, $home, @pwentry); + + for my $v (@voters) { + unless (defined ($c = $self->{'voterlist'}->getconf($v))) { + die "$v does not correspond to any confirmation number"; + } + + @pwentry = getpwnam($v); + unless (@pwentry) { + print STDERR "Warning: unknown user: $v\n"; + next; + } + + $home = $pwentry[7]; + unless (-d $home) { + print STDERR "Warning: no directory: $home\n"; + next; + } + + if (-f "$home/.ballot-$self->{election}-submitted") { + my ($b) = Ballot->new($self->{'election'}); + $b->read("$home/.ballot-$self->{election}-submitted"); + if ($b->verify) { + print STDERR "Errors found in ballot: $v\n"; + next; + } + $self->{'ballots'}{$c} = $b; + } + elsif (-f "$home/.ballot-$self->{election}") { + print STDERR "Warning: $v did not submit their ballot\n"; + } + } +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c (sort keys %{$self->{'ballots'}}) { + printf F "--------- confirmation %04x ---------\n", $c; + print F $self->{'ballots'}{$c}->to_s + } + close F; +} + +sub read { + my ($self, $filename) = @_; + my ($election, $entries) = $self->{'election'}; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + open(F, "<$filename") or die("can't read $filename"); + { local $/ = undef; $entries = <F>; } + for my $e (split /^--------- confirmation /m, $entries) { + next unless $e; # skip the first zero-length record + unless ($e =~ /^([[:xdigit:]]{4}) ---------\n(.*)$/s) { + die "error parsing entry:\n$e"; + } + my ($c, $s, $b) = ($1, $2, Ballot->new($election)); + $b->from_s($s); + $self->{'ballots'}{hex($c)} = $b; + } +} + +sub generate_candidates { + my ($self) = @_; + my ($B, @C, $s); + + # nb: would need to scan all the ballots to support write-ins + $B = Ballot->new($self->{'election'}); + $B->populate; + @C = sort map $_->[0], @{$B->choices}; + for my $c (@C) { + $s = $c; # in case $c is shorter than 5 chars + for (my $i=5; $i<=length($c); $i++) { + $s = substr $c, 0, $i; + print join(" ", grep(/^$s/, @C)), "\n"; + last unless grep(/^$s/, @C) > 1; + } + $self->{'candidates'}{$c} = $s; + } +} + +sub tabulate { + my ($self) = @_; + my (@candidates); # full candidate list + my (%table); # resulting table, row.colum where row defeats column + $self->{'table'} = \%table; + + $self->generate_candidates unless $self->{'candidates'}; + @candidates = keys %{$self->{'candidates'}}; + for my $c1 (@candidates) { + for my $c2 (@candidates) { + $table{"$c1+$c2"} = 0; + } + $table{"$c1+$c1"} = '***'; + } + + # generate the table first; + # walk through the ballots, tallying the rankings expressed by each ballot + for my $b (values %{$self->{'ballots'}}) { + my (@choices, %ranks); + + #print "looking at ballot:\n", $b->to_s, "\n"; + + # first determine the ranking of each candidate. default ranking is + # scalar @candidates. + @choices = @{$b->choices}; + @ranks{@candidates} = (scalar @candidates) x @candidates; + #print "ranks before determining:\n", Dumper(\%ranks); + for (my $i = 0; $i < @choices; $i++) { + @ranks{@{$choices[$i]}} = ($i) x @{$choices[$i]}; + } + #print "ranks after determining:\n", Dumper(\%ranks); + + # second add the results of all the pairwise races into our table + for my $c1 (@candidates) { + for my $c2 (@candidates) { + next if $c1 eq $c2; + $table{"$c1+$c2"}++ if $ranks{$c1} < $ranks{$c2}; + } + } + #print "table after adding:\n"; + #$self->display_table; + } +} + +sub display_table { + my ($self) = @_; + my (@longnames, @shortnames); + my ($minlen, $maxlen, $formatstr) = (0, 4, ''); + + $self->generate_candidates unless $self->{'candidates'}; + @longnames = sort keys %{$self->{'candidates'}}; + @shortnames = sort values %{$self->{'candidates'}}; + $minlen = length scalar keys %{$self->{'ballots'}}; + $minlen = 5 if $minlen < 5; + + # build the format string + for my $s (@shortnames) { + if (length($s) > $minlen) { + $formatstr .= " %" . length($s) . "s"; + } else { + $formatstr .= " %${minlen}s"; + } + } + map { $maxlen = length($_) if length($_) > $maxlen } @longnames; + + # prepend the row header; append newline + $formatstr = "%${maxlen}s" . $formatstr . "\n"; + + # column headers + printf $formatstr, '', @shortnames; + + # rows + for my $l (@longnames) { + printf $formatstr, $l, @{$self->{'table'}}{map "$l+$_", @longnames}; + } +} + +# utility for cssd +sub defeats { + my ($self, $o1, $o2) = @_; + return 0 if $o1 eq $o2; + $self->{'table'}{"$o1+$o2"} > $self->{'table'}{"$o2+$o1"}; +} + +# utility for cssd +sub is_weaker_defeat { + my ($self, $A, $X, $B, $Y) = @_; + die unless $self->defeats($A, $X); + die unless $self->defeats($B, $Y); + return ( + $self->{'table'}{"$A+$X"} < $self->{'table'}{"$B+$Y"} or + ( + $self->{'table'}{"$A+$X"} == $self->{'table'}{"$B+$Y"} and + $self->{'table'}{"$X+$A"} > $self->{'table'}{"$Y+$B"} + ) + ); +} + +sub cssd { + my ($self) = @_; + my (@candidates); + + @candidates = sort keys %{$self->{'candidates'}}; + + while (1) { + my (%transitive_defeats); + my (@active, @plist); + + ###################################################################### + # 5. From the list of [undropped] pairwise defeats, we generate a + # set of transitive defeats. + # 1. An option A transitively defeats an option C if A + # defeats C or if there is some other option B where A + # defeats B AND B transitively defeats C. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + $transitive_defeats{"$o1+$o2"} = 1 if $self->defeats($o1, $o2); + } + } + for my $i (@candidates) { + for my $j (@candidates) { + for my $k (@candidates) { + if (exists $transitive_defeats{"$j+$i"} and + exists $transitive_defeats{"$i+$k"}) + { + $transitive_defeats{"$j+$k"} = 1; + } + } + } + } + + ###################################################################### + # 6. We construct the Schwartz set from the set of transitive + # defeats. + # 1. An option A is in the Schwartz set if for all options B, + # either A transitively defeats B, or B does not + # transitively defeat A. + print "\n"; + A: for my $A (@candidates) { + for my $B (@candidates) { + next if $transitive_defeats{"$A+$B"} or not $transitive_defeats{"$B+$A"}; + # countify marks entries +++ instead of *** when they've already + # been ranked. + if ($self->{'table'}{"$A+$A"} eq '***') { + print "option $A is eliminated ($B trans-defeats $A, and $A does not trans-defeat $B)\n"; + } + next A; + } + push @active, $A; + } + print "the Schwartz set is {", join(", ", @active), "}\n"; + @candidates = @active; + + ###################################################################### + # 7. If there are defeats between options in the Schwartz set, we + # drop the weakest such defeats from the list of pairwise + # defeats, and return to step 5. + # 1. A defeat (A,X) is weaker than a defeat (B,Y) if V(A,X) + # is less than V(B,Y). Also, (A,X) is weaker than (B,Y) if + # V(A,X) is equal to V(B,Y) and V(X,A) is greater than V + # (Y,B). + # 2. A weakest defeat is a defeat that has no other defeat + # weaker than it. There may be more than one such defeat. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + push @plist, [ $o1, $o2 ] if $self->defeats($o1, $o2); + } + } + last unless @plist; + @plist = sort { + return -1 if $self->is_weaker_defeat(@$a, @$b); + return +1 if $self->is_weaker_defeat(@$b, @$a); + return 0; + } @plist; + for my $dx (@plist) { + my ($o1, $o2) = @$dx; + print("$o1+$o2 ", + $self->{'table'}{"$o1+$o2"}, " $o2+$o1 ", + $self->{'table'}{"$o2+$o1"}, "\n"); + } + my ($o1, $o2) = @{$plist[0]}; + $self->{'table'}{"$o1+$o2"} = 0; + $self->{'table'}{"$o2+$o1"} = 0; + } + + ###################################################################### + # 8. If there are no defeats within the Schwartz set, then the + # winner is chosen from the options in the Schwartz set. If + # there is only one such option, it is the winner. If there + # are multiple options, the elector with the casting vote + # chooses which of those options wins. + print "\n"; + if (@candidates > 1) { + print "result: tie between options ", join(", ", @candidates), "\n"; + } else { + print "result: option @candidates wins\n"; + } + + return @candidates; +} + +###################################################################### +# Ballot +###################################################################### + +package Ballot; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + filename => '', + default_filename => $ENV{'HOME'}."/.ballot-$election", + choices => [], + }; + + # Bless me, I'm a ballot! + bless $self, $class; + return $self; +} + +sub from_s { + my ($self, $s) = @_; + my (@choices); + + for (split "\n", $s) { + s/#.*//; + next unless /\S/; + push @choices, [ split(' ', $_) ]; + } + die("No data in string") unless @choices; + + $self->{'choices'} = \@choices; +} + +sub read { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Load the data file + open(F, "<$filename") or die("couldn't open $filename"); + { local $/ = undef; $self->from_s(<F>); } + close(F); +} + +sub populate { + my ($self) = @_; + $self->read("$Votify::datadir/ballot-$self->{election}"); + @{$self->{'choices'}} = List::Util::shuffle(@{$self->{'choices'}}); +} + +sub choices { + my ($self) = @_; + $self->{'choices'}; +} + +sub write { + my ($self, $filename) = @_; + + if ($Votify::mode ne 'user') { + die("we don't write ballots in official mode"); + } + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Don't ever overwrite a ballot + die("File already exists; please remove $filename\n") if -e $filename; + + # Write the user's ballot + open(F, ">$filename") or die "Failed writing $filename"; + print F <<EOT; +# This is a ballot for the $self->{election} election. +# Please rank your choices in order; first choice at the top and last choice at +# the bottom. You can put choices on the same line to indicate no preference +# between them. Any choices you omit from this file are implicitly added at the +# end. +# +# When you're finished editing this, the next step is to verify your ballot +# with: +# +# $Votify::zero --verify $self->{election} +# +# When that passes and you're satisfied, the final step is to submit your vote: +# +# $Votify::zero --submit $self->{election} +# + +EOT + for (@{$self->{'choices'}}) { print F "@$_\n"; } + close(F); +} + +sub verify { + my ($self) = @_; + my (%h, $master, %mh); + my (@dups, @missing, @extra); + my ($errors_found); + + # Load %h from the user's ballot + for my $line (@{$self->{'choices'}}) { + for my $entry (@$line) { + $h{$entry}++; + } + } + + # Load the master ballot into another hash and compare them. + # The master ballots always do one entry per line, making this a little + # easier. + $master = Ballot->new($self->{'election'}); + $master->populate; + %mh = map(($_->[0] => 1), @{$master->{'choices'}}); + + # Check for extra entries (write-ins should be supported in the future) + for (keys %h) { + push @extra, $_ unless exists $mh{$_}; + } + + # Check for duplicate entries + @dups = grep { $h{$_} > 1 } keys %h; + + # Check for missing entries (not necessarily an error) + for (keys %mh) { + push @missing, $_ unless exists $h{$_}; + } + + # Report errors and warnings + if (@extra) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some extra entries that are not part of this election. Sorry, +but write-ins are not (yet) supported. Please remove these from your ballot: + +EOT + print map "\t$_\n", @extra; + print "\n"; + } + $errors_found++; + } + if (@dups) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some duplicate entries. Please resolve these to a single entry +to avoid ambiguities: + +EOT + print map "\t$_\n", @dups; + print "\n"; + } + $errors_found++; + } + if (@{$self->{'choices'}} == 0) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot doesn't contain any entries. You can start over by first removing +the existing ballot, then using --new to generate a new ballot. See --help for +more information. + +EOT + } + $errors_found++; + } + elsif (@missing and $Votify::mode eq 'user') { + print <<EOT; +Your ballot is missing some entries. This is not an error, but note that these +will be implied as a final line, with no preference between them, like this: + +EOT + print "\t", join(" ", @missing), "\n"; + print "\n"; + } + if ($Votify::mode eq 'user' and !$errors_found and + @{$self->{'choices'}} == 1 and + scalar(keys %h) == scalar(keys %mh)) + { + print <<EOT; +Your ballot contains all the candidates on a single line! This means you have +no preference between the candidates. This is not an error, but note that this +is a meaningless ballot that will have no effect on the election. + +EOT + } + + # Stop if there were errors + if ($Votify::mode eq 'user' and $errors_found) { + print("There were errors found in your ballot.\n"); + die("Please correct them and try again.\n\n"); + } + return $errors_found; +} + +sub to_s { + my ($self) = @_; + join '', map "@$_\n", @{$self->{'choices'}}; +} + +1; + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + +Revision 1.3 2005/05/09 23:12:02 agriffis +Add support for registered voters + +Revision 1.2 2005/05/05 23:03:46 agriffis +Fix indentation (and some output as well) + +Revision 1.1 2005/05/05 22:05:34 agriffis +first pass at Gentoo Foundation voting program + +# vim:sw=4 et diff --git a/council2007/ballot-council2007 b/council2007/ballot-council2007 new file mode 100644 index 0000000..299e5e9 --- /dev/null +++ b/council2007/ballot-council2007 @@ -0,0 +1,12 @@ +amne +betelgeuse +christel +dberkholz +dertobi123 +flameeyes +jaervosz +jokey +lu_zero +uberlord +vapier +welp diff --git a/council2007/officials-council2007 b/council2007/officials-council2007 new file mode 100644 index 0000000..fcdc249 --- /dev/null +++ b/council2007/officials-council2007 @@ -0,0 +1,3 @@ +fox2mike +swift +graaff diff --git a/council2007/start-council2007 b/council2007/start-council2007 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/council2007/start-council2007 diff --git a/council2007/stop-council2007 b/council2007/stop-council2007 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/council2007/stop-council2007 diff --git a/council2007/voters-council2007 b/council2007/voters-council2007 new file mode 100644 index 0000000..94c4677 --- /dev/null +++ b/council2007/voters-council2007 @@ -0,0 +1,329 @@ +aballier +achumakov +aetius +agaffney +agorf +agriffis +ali_bush +alin +alonbl +amne +anant +angelos +anpereir +antarus +araujo +armin76 +aross +astinus +augustus +axxo +azarah +bangert +bass +batlogg +battousai +bbj +bcowan +beandog +betelgeuse +bicatali +blackace +cab +calchan +caleb +cam +cardoe +carlo +caster +cedk +centic +chainsaw +chiguire +chrb +christel +chtekk +chutzpah +cla +codeman +codergeek42 +coldwind +compnerd +config +corsair +cparrott +cryos +curtis119 +dams +dang +dberkholz +dcoutts +deathwing00 +dercorny +dertobi123 +desultory +dev-zero +dholm +diox +dirtyepic +djay +dju +dmwaters +dostrow +drac +dragonheart +drizzt +dsd +earthwings +ehmsen +eldad +eradicator +eva +falco +ferdy +flameeyes +flammie +fmccor +fordfrog +formula7 +foser +fox2mike +fuzzyray +fvdpol +g2boojum +genone +genstef +george +gmsoft +gongloo +graaff +grahl +gregkh +griffon26 +grobian +gurligebis +gustavoz +hanno +hansmi +hardave +hattya +hawking +hd_brummy +herbs +hkbst +hlieberman +hoffie +hollow +hparker +humpback +hyakuhei +ian +idani +ikelos +iluxa +jaervosz +jakub +je_fro +jeffw +jer +jforman +jkt +jmbsvicetto +jmglov +joem +johnm +joker +jokey +josejx +joshuabaergen +joslwah +jrinkovs +jsin +jstubbs +jurek +kaiowas +kallamej +kanaka +kang +karltk +kengland +keri +kernelsensei +kevquinn +keytoaster +killerfox +kingtaco +klieber +kloeri +kolmodin +kosmikus +kugelfang +kumba +kutsuya +lack +langthang +latexer +lavajoe +lcars +leio +leonardop +liquidx +lisa +livewire +lizb +lordvan +lu_zero +lucass +mabi +maedhros +maekke +malc +marduk +marienz +marineam +mark_alec +markm +markusle +masterdriverz +matsuu +mattam +mattepiu +mattm +mcummings +mdisney +merlin +metalgod +method +ming +mjolnir +mkay +moloh +mr_bones_ +mrness +muchar +musikc +nakano +neddyseagoon +nelchael +nerdboy +neysx +nichoj +nightmorph +nigoro +nixnut +nixphoeni +nyhm +omp +opfer +pappy +pauldv +pbienst +pclouds +pebenito +peitolm +peper +philantrop +phosphan +phreak +pilla +pingu +pioto +pjp +plate +polvi +psi29a +puggy +pva +pvdabeel +py +pylon +pyrania +pythonhead +r0bertz +r2d2 +r3pek +radek +rajiv +ramereth +rane +ranger +rbrown +rbu +reb +redhatter +remi +ribosome +rl03 +robbat2 +rocket +roger55 +s4t4n +satya +sbriesen +scen +seemant +sekretarz +shadoww +shellsage +shindo +sirseoman +smithj +solar +spb +spock +squash +st3vie +steev +stefaan +stkn +strerror +suka +superlag +swegener +swift +tantive +taviso +tchiwam +tercel +tester +tgall +the_paya +think4urs11 +thoand +thunder +ticho +tocharian +tomk +tove +trapni +troll +trombik +truedfx +tsunam +tupone +twp +uberlord +ulm +usata +vanquirius +vapier +vorlon +voxus +voyageur +weeve +welp +williamh +wltjr +wolf31o2 +wormo +wrobel +wschlich +xmerlin +yoswink +yuval +yvasilev +zaheerm +zeypher +zmedico +zypher +zzam diff --git a/council2008/Votify.pm b/council2008/Votify.pm new file mode 100644 index 0000000..0c5565d --- /dev/null +++ b/council2008/Votify.pm @@ -0,0 +1,683 @@ +# $Id: Votify.pm,v 1.5 2005/05/16 23:58:09 agriffis Exp $ +# +# Copyright 2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# votify.pm: common classes for votify and countify +# + +package Votify; + +use POSIX; +use List::Util; +use strict; + +our ($datadir) = '/home/fox2mike/elections'; +(our $zero = $0) =~ s,.*/,,; + +sub import { + my ($class, $mode) = @_; + $Votify::mode = $mode; +} + +###################################################################### +# OfficialList +###################################################################### + +package OfficialList; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + officials => [], + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/officials-$election") + or die("failed to open officials file"); + chomp(@{$self->{'officials'}} = <F>); + close(F); + + bless $self, $class; + return $self; +} + +sub officials { + my ($self) = @_; + @{$self->{'officials'}}; +} + +###################################################################### +# VoterList +###################################################################### + +package VoterList; + +sub new { + my ($class, $election) = @_; + my (@voterlist, $r); + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/confs-$election", + filename => '', + voters => {}, # confnum => voter + confs => {}, # voter => confnum + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/voters-$election") + or die("failed to open voters file"); + chomp(@voterlist = <F>); + close(F); + + # assign confirmation numbers randomly + for my $v (@voterlist) { + do { $r = int rand 0xffff } while exists $self->{'voters'}{$r}; + $self->{'voters'}{$r} = $v; + $self->{'confs'}{$v} = $r; + } + + unless (keys %{$self->{'voters'}} == keys %{$self->{'confs'}}) { + die("discrepancy deteced in VoterList"); + } + + bless $self, $class; + return $self; +} + +sub confs { + my ($self) = @_; + sort keys %{$self->{'voters'}}; +} + +sub voters { + my ($self) = @_; + sort keys %{$self->{'confs'}}; +} + +sub getvoter { + my ($self, $conf) = @_; + return $self->{'voters'}{$conf}; +} + +sub getconf { + my ($self, $voter) = @_; + return $self->{'confs'}{$voter}; +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c ($self->confs) { + printf F "%04x %s\n", $c, $self->getvoter($c); + } + close F; +} + +###################################################################### +# MasterBallot +###################################################################### + +package MasterBallot; + +use Data::Dumper; + +sub new { + my ($class, $election, $vl) = @_; + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/master-$election", + filename => '', + voterlist => $vl, + ballots => {}, # indexed by conf num + candidates => undef, # indexed by long name + table => undef, # indexed by row+column + }; + + bless $self, $class; + return $self; +} + +sub collect { + my ($self, @voters) = @_; + my ($c, $v, $home, @pwentry); + + for my $v (@voters) { + unless (defined ($c = $self->{'voterlist'}->getconf($v))) { + die "$v does not correspond to any confirmation number"; + } + + @pwentry = getpwnam($v); + unless (@pwentry) { + print STDERR "Warning: unknown user: $v\n"; + next; + } + + $home = $pwentry[7]; + unless (-d $home) { + print STDERR "Warning: no directory: $home\n"; + next; + } + + if (-f "$home/.ballot-$self->{election}-submitted") { + my ($b) = Ballot->new($self->{'election'}); + $b->read("$home/.ballot-$self->{election}-submitted"); + if ($b->verify) { + print STDERR "Errors found in ballot: $v\n"; + next; + } + $self->{'ballots'}{$c} = $b; + } + elsif (-f "$home/.ballot-$self->{election}") { + print STDERR "Warning: $v did not submit their ballot\n"; + } + } +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c (sort keys %{$self->{'ballots'}}) { + printf F "--------- confirmation %04x ---------\n", $c; + print F $self->{'ballots'}{$c}->to_s + } + close F; +} + +sub read { + my ($self, $filename) = @_; + my ($election, $entries) = $self->{'election'}; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + open(F, "<$filename") or die("can't read $filename"); + { local $/ = undef; $entries = <F>; } + for my $e (split /^--------- confirmation /m, $entries) { + next unless $e; # skip the first zero-length record + unless ($e =~ /^([[:xdigit:]]{4}) ---------\n(.*)$/s) { + die "error parsing entry:\n$e"; + } + my ($c, $s, $b) = ($1, $2, Ballot->new($election)); + $b->from_s($s); + $self->{'ballots'}{hex($c)} = $b; + } +} + +sub generate_candidates { + my ($self) = @_; + my ($B, @C, $s); + + # nb: would need to scan all the ballots to support write-ins + $B = Ballot->new($self->{'election'}); + $B->populate; + @C = sort map $_->[0], @{$B->choices}; + for my $c (@C) { + $s = $c; # in case $c is shorter than 5 chars + for (my $i=5; $i<=length($c); $i++) { + $s = substr $c, 0, $i; + print join(" ", grep(/^$s/, @C)), "\n"; + last unless grep(/^$s/, @C) > 1; + } + $self->{'candidates'}{$c} = $s; + } +} + +sub tabulate { + my ($self) = @_; + my (@candidates); # full candidate list + my (%table); # resulting table, row.colum where row defeats column + $self->{'table'} = \%table; + + $self->generate_candidates unless $self->{'candidates'}; + @candidates = keys %{$self->{'candidates'}}; + for my $c1 (@candidates) { + for my $c2 (@candidates) { + $table{"$c1+$c2"} = 0; + } + $table{"$c1+$c1"} = '***'; + } + + # generate the table first; + # walk through the ballots, tallying the rankings expressed by each ballot + for my $b (values %{$self->{'ballots'}}) { + my (@choices, %ranks); + + #print "looking at ballot:\n", $b->to_s, "\n"; + + # first determine the ranking of each candidate. default ranking is + # scalar @candidates. + @choices = @{$b->choices}; + @ranks{@candidates} = (scalar @candidates) x @candidates; + #print "ranks before determining:\n", Dumper(\%ranks); + for (my $i = 0; $i < @choices; $i++) { + @ranks{@{$choices[$i]}} = ($i) x @{$choices[$i]}; + } + #print "ranks after determining:\n", Dumper(\%ranks); + + # second add the results of all the pairwise races into our table + for my $c1 (@candidates) { + for my $c2 (@candidates) { + next if $c1 eq $c2; + $table{"$c1+$c2"}++ if $ranks{$c1} < $ranks{$c2}; + } + } + #print "table after adding:\n"; + #$self->display_table; + } +} + +sub display_table { + my ($self) = @_; + my (@longnames, @shortnames); + my ($minlen, $maxlen, $formatstr) = (0, 4, ''); + + $self->generate_candidates unless $self->{'candidates'}; + @longnames = sort keys %{$self->{'candidates'}}; + @shortnames = sort values %{$self->{'candidates'}}; + $minlen = length scalar keys %{$self->{'ballots'}}; + $minlen = 5 if $minlen < 5; + + # build the format string + for my $s (@shortnames) { + if (length($s) > $minlen) { + $formatstr .= " %" . length($s) . "s"; + } else { + $formatstr .= " %${minlen}s"; + } + } + map { $maxlen = length($_) if length($_) > $maxlen } @longnames; + + # prepend the row header; append newline + $formatstr = "%${maxlen}s" . $formatstr . "\n"; + + # column headers + printf $formatstr, '', @shortnames; + + # rows + for my $l (@longnames) { + printf $formatstr, $l, @{$self->{'table'}}{map "$l+$_", @longnames}; + } +} + +# utility for cssd +sub defeats { + my ($self, $o1, $o2) = @_; + return 0 if $o1 eq $o2; + $self->{'table'}{"$o1+$o2"} > $self->{'table'}{"$o2+$o1"}; +} + +# utility for cssd +sub is_weaker_defeat { + my ($self, $A, $X, $B, $Y) = @_; + die unless $self->defeats($A, $X); + die unless $self->defeats($B, $Y); + return ( + $self->{'table'}{"$A+$X"} < $self->{'table'}{"$B+$Y"} or + ( + $self->{'table'}{"$A+$X"} == $self->{'table'}{"$B+$Y"} and + $self->{'table'}{"$X+$A"} > $self->{'table'}{"$Y+$B"} + ) + ); +} + +sub cssd { + my ($self) = @_; + my (@candidates); + + @candidates = sort keys %{$self->{'candidates'}}; + + while (1) { + my (%transitive_defeats); + my (@active, @plist); + + ###################################################################### + # 5. From the list of [undropped] pairwise defeats, we generate a + # set of transitive defeats. + # 1. An option A transitively defeats an option C if A + # defeats C or if there is some other option B where A + # defeats B AND B transitively defeats C. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + $transitive_defeats{"$o1+$o2"} = 1 if $self->defeats($o1, $o2); + } + } + for my $i (@candidates) { + for my $j (@candidates) { + for my $k (@candidates) { + if (exists $transitive_defeats{"$j+$i"} and + exists $transitive_defeats{"$i+$k"}) + { + $transitive_defeats{"$j+$k"} = 1; + } + } + } + } + + ###################################################################### + # 6. We construct the Schwartz set from the set of transitive + # defeats. + # 1. An option A is in the Schwartz set if for all options B, + # either A transitively defeats B, or B does not + # transitively defeat A. + print "\n"; + A: for my $A (@candidates) { + for my $B (@candidates) { + next if $transitive_defeats{"$A+$B"} or not $transitive_defeats{"$B+$A"}; + # countify marks entries +++ instead of *** when they've already + # been ranked. + if ($self->{'table'}{"$A+$A"} eq '***') { + print "option $A is eliminated ($B trans-defeats $A, and $A does not trans-defeat $B)\n"; + } + next A; + } + push @active, $A; + } + print "the Schwartz set is {", join(", ", @active), "}\n"; + @candidates = @active; + + ###################################################################### + # 7. If there are defeats between options in the Schwartz set, we + # drop the weakest such defeats from the list of pairwise + # defeats, and return to step 5. + # 1. A defeat (A,X) is weaker than a defeat (B,Y) if V(A,X) + # is less than V(B,Y). Also, (A,X) is weaker than (B,Y) if + # V(A,X) is equal to V(B,Y) and V(X,A) is greater than V + # (Y,B). + # 2. A weakest defeat is a defeat that has no other defeat + # weaker than it. There may be more than one such defeat. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + push @plist, [ $o1, $o2 ] if $self->defeats($o1, $o2); + } + } + last unless @plist; + @plist = sort { + return -1 if $self->is_weaker_defeat(@$a, @$b); + return +1 if $self->is_weaker_defeat(@$b, @$a); + return 0; + } @plist; + for my $dx (@plist) { + my ($o1, $o2) = @$dx; + print("$o1+$o2 ", + $self->{'table'}{"$o1+$o2"}, " $o2+$o1 ", + $self->{'table'}{"$o2+$o1"}, "\n"); + } + my ($o1, $o2) = @{$plist[0]}; + $self->{'table'}{"$o1+$o2"} = 0; + $self->{'table'}{"$o2+$o1"} = 0; + } + + ###################################################################### + # 8. If there are no defeats within the Schwartz set, then the + # winner is chosen from the options in the Schwartz set. If + # there is only one such option, it is the winner. If there + # are multiple options, the elector with the casting vote + # chooses which of those options wins. + print "\n"; + if (@candidates > 1) { + print "result: tie between options ", join(", ", @candidates), "\n"; + } else { + print "result: option @candidates wins\n"; + } + + return @candidates; +} + +###################################################################### +# Ballot +###################################################################### + +package Ballot; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + filename => '', + default_filename => $ENV{'HOME'}."/.ballot-$election", + choices => [], + }; + + # Bless me, I'm a ballot! + bless $self, $class; + return $self; +} + +sub from_s { + my ($self, $s) = @_; + my (@choices); + + for (split "\n", $s) { + s/#.*//; + next unless /\S/; + push @choices, [ split(' ', $_) ]; + } + die("No data in string") unless @choices; + + $self->{'choices'} = \@choices; +} + +sub read { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Load the data file + open(F, "<$filename") or die("couldn't open $filename"); + { local $/ = undef; $self->from_s(<F>); } + close(F); +} + +sub populate { + my ($self) = @_; + $self->read("$Votify::datadir/ballot-$self->{election}"); + @{$self->{'choices'}} = List::Util::shuffle(@{$self->{'choices'}}); +} + +sub choices { + my ($self) = @_; + $self->{'choices'}; +} + +sub write { + my ($self, $filename) = @_; + + if ($Votify::mode ne 'user') { + die("we don't write ballots in official mode"); + } + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Don't ever overwrite a ballot + die("File already exists; please remove $filename\n") if -e $filename; + + # Write the user's ballot + open(F, ">$filename") or die "Failed writing $filename"; + print F <<EOT; +# This is a ballot for the $self->{election} election. +# Please rank your choices in order; first choice at the top and last choice at +# the bottom. You can put choices on the same line to indicate no preference +# between them. Any choices you omit from this file are implicitly added at the +# end. +# +# When you're finished editing this, the next step is to verify your ballot +# with: +# +# $Votify::zero --verify $self->{election} +# +# When that passes and you're satisfied, the final step is to submit your vote: +# +# $Votify::zero --submit $self->{election} +# + +EOT + for (@{$self->{'choices'}}) { print F "@$_\n"; } + close(F); +} + +sub verify { + my ($self) = @_; + my (%h, $master, %mh); + my (@dups, @missing, @extra); + my ($errors_found); + + # Load %h from the user's ballot + for my $line (@{$self->{'choices'}}) { + for my $entry (@$line) { + $h{$entry}++; + } + } + + # Load the master ballot into another hash and compare them. + # The master ballots always do one entry per line, making this a little + # easier. + $master = Ballot->new($self->{'election'}); + $master->populate; + %mh = map(($_->[0] => 1), @{$master->{'choices'}}); + + # Check for extra entries (write-ins should be supported in the future) + for (keys %h) { + push @extra, $_ unless exists $mh{$_}; + } + + # Check for duplicate entries + @dups = grep { $h{$_} > 1 } keys %h; + + # Check for missing entries (not necessarily an error) + for (keys %mh) { + push @missing, $_ unless exists $h{$_}; + } + + # Report errors and warnings + if (@extra) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some extra entries that are not part of this election. Sorry, +but write-ins are not (yet) supported. Please remove these from your ballot: + +EOT + print map "\t$_\n", @extra; + print "\n"; + } + $errors_found++; + } + if (@dups) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some duplicate entries. Please resolve these to a single entry +to avoid ambiguities: + +EOT + print map "\t$_\n", @dups; + print "\n"; + } + $errors_found++; + } + if (@{$self->{'choices'}} == 0) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot doesn't contain any entries. You can start over by first removing +the existing ballot, then using --new to generate a new ballot. See --help for +more information. + +EOT + } + $errors_found++; + } + elsif (@missing and $Votify::mode eq 'user') { + print <<EOT; +Your ballot is missing some entries. This is not an error, but note that these +will be implied as a final line, with no preference between them, like this: + +EOT + print "\t", join(" ", @missing), "\n"; + print "\n"; + } + if ($Votify::mode eq 'user' and !$errors_found and + @{$self->{'choices'}} == 1 and + scalar(keys %h) == scalar(keys %mh)) + { + print <<EOT; +Your ballot contains all the candidates on a single line! This means you have +no preference between the candidates. This is not an error, but note that this +is a meaningless ballot that will have no effect on the election. + +EOT + } + + # Stop if there were errors + if ($Votify::mode eq 'user' and $errors_found) { + print("There were errors found in your ballot.\n"); + die("Please correct them and try again.\n\n"); + } + return $errors_found; +} + +sub to_s { + my ($self) = @_; + join '', map "@$_\n", @{$self->{'choices'}}; +} + +1; + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + +Revision 1.3 2005/05/09 23:12:02 agriffis +Add support for registered voters + +Revision 1.2 2005/05/05 23:03:46 agriffis +Fix indentation (and some output as well) + +Revision 1.1 2005/05/05 22:05:34 agriffis +first pass at Gentoo Foundation voting program + +# vim:sw=4 et diff --git a/council2008/ballot-council2008 b/council2008/ballot-council2008 new file mode 100644 index 0000000..fd44753 --- /dev/null +++ b/council2008/ballot-council2008 @@ -0,0 +1,19 @@ +astinus +Betelgeuse +cardoe +dberkholz +dertobi123 +dev-zero +ferdy +Flameeyes +fmccor +Halcy0n +hkBst +jer +Jokey +leio +lu_zero +peper +ulm +welp +zlin diff --git a/council2008/officials-council2008 b/council2008/officials-council2008 new file mode 100644 index 0000000..17a74fc --- /dev/null +++ b/council2008/officials-council2008 @@ -0,0 +1,4 @@ +jmbsvicetto +rane +antarus +root diff --git a/council2008/start-council2008 b/council2008/start-council2008 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/council2008/start-council2008 diff --git a/council2008/stop-council2008 b/council2008/stop-council2008 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/council2008/stop-council2008 diff --git a/council2008/voters-council2008 b/council2008/voters-council2008 new file mode 100644 index 0000000..4845b5f --- /dev/null +++ b/council2008/voters-council2008 @@ -0,0 +1,252 @@ +aballier +aetius +agaffney +agorf +ali_bush +amne +anant +angelos +antarus +araujo +armin76 +astinus +b33fc0d3 +bangert +bass +battousai +bbj +beandog +betelgeuse +bicatali +blackace +bluebird +bunder +cab +calchan +caleb +cam +cardoe +carlo +caster +cedk +chainsaw +chiguire +chtekk +chutzpah +cla +codeman +coldwind +compnerd +corsair +cryos +dams +dang +darkside +dav_it +dberkholz +deathwing00 +dertobi123 +desultory +dev-zero +dirtyepic +djay +dmwaters +dostrow +drac +dragonheart +drizzt +dsd +earthwings +ehmsen +elvanor +eradicator +eva +falco +ferdy +flameeyes +flammie +fmccor +fordfrog +fox2mike +fuzzyray +g2boojum +genone +genstef +gentoofan23 +george +gmsoft +graaff +grahl +gregkh +griffon26 +grobian +grozin +gurligebis +halcy0n +hanno +hattya +haubi +hawking +hd_brummy +hkbst +hoffie +hollow +hparker +humpback +ian +ikelos +iluxa +ingmar +jaervosz +jakub +je_fro +jer +jforman +jkt +jmbsvicetto +jmglov +joker +jokey +josejx +joslwah +jrinkovs +jsbronder +jsin +jurek +kallamej +kanaka +kang +ken69267 +keri +kernelsensei +keytoaster +killerfox +kingtaco +klausman +klieber +kolmodin +kumba +lack +lavajoe +leio +leonardop +livewire +loki_val +lordvan +lu_zero +lucass +mabi +maedhros +maekke +marineam +mark_alec +markm +markusle +matsuu +mattepiu +mbres +mdisney +mduft +mjf +moloh +mpagano +mr_bones_ +mrness +mueli +musikc +neddyseagoon +nelchael +nerdboy +neysx +nichoj +nightmorph +nixnut +nixphoeni +nyhm +omp +opfer +pauldv +pchrist +pclouds +pebenito +peitolm +peper +phosphan +phreak +pilla +pingu +pjp +polvi +pva +pvdabeel +py +pythonhead +r0bertz +radek +rajiv +ramereth +rane +ranger +rbu +redhatter +remi +ribosome +rich0 +ricmm +rl03 +robbat2 +s4t4n +sbriesen +scen +serkan +shadow +shellsage +shindo +sirseoman +smithj +solar +spock +steev +stefaan +stkn +strerror +suka +swegener +swift +tantive +tester +tgall +tgurr +the_paya +think4urs11 +titefleur +tomk +tommy +tove +trapni +truedfx +tsunam +tupone +ulm +vanquirius +vapier +vorlon +voxus +voyageur +weaver +welp +williamh +wltjr +wolf31o2 +wormo +wrobel +wschlich +xmerlin +yngwin +yoswink +yuval +yvasilev +zaheerm +zlin +zmedico +zzam diff --git a/council2008b/Votify.pm b/council2008b/Votify.pm new file mode 100644 index 0000000..0c5565d --- /dev/null +++ b/council2008b/Votify.pm @@ -0,0 +1,683 @@ +# $Id: Votify.pm,v 1.5 2005/05/16 23:58:09 agriffis Exp $ +# +# Copyright 2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# votify.pm: common classes for votify and countify +# + +package Votify; + +use POSIX; +use List::Util; +use strict; + +our ($datadir) = '/home/fox2mike/elections'; +(our $zero = $0) =~ s,.*/,,; + +sub import { + my ($class, $mode) = @_; + $Votify::mode = $mode; +} + +###################################################################### +# OfficialList +###################################################################### + +package OfficialList; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + officials => [], + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/officials-$election") + or die("failed to open officials file"); + chomp(@{$self->{'officials'}} = <F>); + close(F); + + bless $self, $class; + return $self; +} + +sub officials { + my ($self) = @_; + @{$self->{'officials'}}; +} + +###################################################################### +# VoterList +###################################################################### + +package VoterList; + +sub new { + my ($class, $election) = @_; + my (@voterlist, $r); + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/confs-$election", + filename => '', + voters => {}, # confnum => voter + confs => {}, # voter => confnum + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/voters-$election") + or die("failed to open voters file"); + chomp(@voterlist = <F>); + close(F); + + # assign confirmation numbers randomly + for my $v (@voterlist) { + do { $r = int rand 0xffff } while exists $self->{'voters'}{$r}; + $self->{'voters'}{$r} = $v; + $self->{'confs'}{$v} = $r; + } + + unless (keys %{$self->{'voters'}} == keys %{$self->{'confs'}}) { + die("discrepancy deteced in VoterList"); + } + + bless $self, $class; + return $self; +} + +sub confs { + my ($self) = @_; + sort keys %{$self->{'voters'}}; +} + +sub voters { + my ($self) = @_; + sort keys %{$self->{'confs'}}; +} + +sub getvoter { + my ($self, $conf) = @_; + return $self->{'voters'}{$conf}; +} + +sub getconf { + my ($self, $voter) = @_; + return $self->{'confs'}{$voter}; +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c ($self->confs) { + printf F "%04x %s\n", $c, $self->getvoter($c); + } + close F; +} + +###################################################################### +# MasterBallot +###################################################################### + +package MasterBallot; + +use Data::Dumper; + +sub new { + my ($class, $election, $vl) = @_; + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/master-$election", + filename => '', + voterlist => $vl, + ballots => {}, # indexed by conf num + candidates => undef, # indexed by long name + table => undef, # indexed by row+column + }; + + bless $self, $class; + return $self; +} + +sub collect { + my ($self, @voters) = @_; + my ($c, $v, $home, @pwentry); + + for my $v (@voters) { + unless (defined ($c = $self->{'voterlist'}->getconf($v))) { + die "$v does not correspond to any confirmation number"; + } + + @pwentry = getpwnam($v); + unless (@pwentry) { + print STDERR "Warning: unknown user: $v\n"; + next; + } + + $home = $pwentry[7]; + unless (-d $home) { + print STDERR "Warning: no directory: $home\n"; + next; + } + + if (-f "$home/.ballot-$self->{election}-submitted") { + my ($b) = Ballot->new($self->{'election'}); + $b->read("$home/.ballot-$self->{election}-submitted"); + if ($b->verify) { + print STDERR "Errors found in ballot: $v\n"; + next; + } + $self->{'ballots'}{$c} = $b; + } + elsif (-f "$home/.ballot-$self->{election}") { + print STDERR "Warning: $v did not submit their ballot\n"; + } + } +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c (sort keys %{$self->{'ballots'}}) { + printf F "--------- confirmation %04x ---------\n", $c; + print F $self->{'ballots'}{$c}->to_s + } + close F; +} + +sub read { + my ($self, $filename) = @_; + my ($election, $entries) = $self->{'election'}; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + open(F, "<$filename") or die("can't read $filename"); + { local $/ = undef; $entries = <F>; } + for my $e (split /^--------- confirmation /m, $entries) { + next unless $e; # skip the first zero-length record + unless ($e =~ /^([[:xdigit:]]{4}) ---------\n(.*)$/s) { + die "error parsing entry:\n$e"; + } + my ($c, $s, $b) = ($1, $2, Ballot->new($election)); + $b->from_s($s); + $self->{'ballots'}{hex($c)} = $b; + } +} + +sub generate_candidates { + my ($self) = @_; + my ($B, @C, $s); + + # nb: would need to scan all the ballots to support write-ins + $B = Ballot->new($self->{'election'}); + $B->populate; + @C = sort map $_->[0], @{$B->choices}; + for my $c (@C) { + $s = $c; # in case $c is shorter than 5 chars + for (my $i=5; $i<=length($c); $i++) { + $s = substr $c, 0, $i; + print join(" ", grep(/^$s/, @C)), "\n"; + last unless grep(/^$s/, @C) > 1; + } + $self->{'candidates'}{$c} = $s; + } +} + +sub tabulate { + my ($self) = @_; + my (@candidates); # full candidate list + my (%table); # resulting table, row.colum where row defeats column + $self->{'table'} = \%table; + + $self->generate_candidates unless $self->{'candidates'}; + @candidates = keys %{$self->{'candidates'}}; + for my $c1 (@candidates) { + for my $c2 (@candidates) { + $table{"$c1+$c2"} = 0; + } + $table{"$c1+$c1"} = '***'; + } + + # generate the table first; + # walk through the ballots, tallying the rankings expressed by each ballot + for my $b (values %{$self->{'ballots'}}) { + my (@choices, %ranks); + + #print "looking at ballot:\n", $b->to_s, "\n"; + + # first determine the ranking of each candidate. default ranking is + # scalar @candidates. + @choices = @{$b->choices}; + @ranks{@candidates} = (scalar @candidates) x @candidates; + #print "ranks before determining:\n", Dumper(\%ranks); + for (my $i = 0; $i < @choices; $i++) { + @ranks{@{$choices[$i]}} = ($i) x @{$choices[$i]}; + } + #print "ranks after determining:\n", Dumper(\%ranks); + + # second add the results of all the pairwise races into our table + for my $c1 (@candidates) { + for my $c2 (@candidates) { + next if $c1 eq $c2; + $table{"$c1+$c2"}++ if $ranks{$c1} < $ranks{$c2}; + } + } + #print "table after adding:\n"; + #$self->display_table; + } +} + +sub display_table { + my ($self) = @_; + my (@longnames, @shortnames); + my ($minlen, $maxlen, $formatstr) = (0, 4, ''); + + $self->generate_candidates unless $self->{'candidates'}; + @longnames = sort keys %{$self->{'candidates'}}; + @shortnames = sort values %{$self->{'candidates'}}; + $minlen = length scalar keys %{$self->{'ballots'}}; + $minlen = 5 if $minlen < 5; + + # build the format string + for my $s (@shortnames) { + if (length($s) > $minlen) { + $formatstr .= " %" . length($s) . "s"; + } else { + $formatstr .= " %${minlen}s"; + } + } + map { $maxlen = length($_) if length($_) > $maxlen } @longnames; + + # prepend the row header; append newline + $formatstr = "%${maxlen}s" . $formatstr . "\n"; + + # column headers + printf $formatstr, '', @shortnames; + + # rows + for my $l (@longnames) { + printf $formatstr, $l, @{$self->{'table'}}{map "$l+$_", @longnames}; + } +} + +# utility for cssd +sub defeats { + my ($self, $o1, $o2) = @_; + return 0 if $o1 eq $o2; + $self->{'table'}{"$o1+$o2"} > $self->{'table'}{"$o2+$o1"}; +} + +# utility for cssd +sub is_weaker_defeat { + my ($self, $A, $X, $B, $Y) = @_; + die unless $self->defeats($A, $X); + die unless $self->defeats($B, $Y); + return ( + $self->{'table'}{"$A+$X"} < $self->{'table'}{"$B+$Y"} or + ( + $self->{'table'}{"$A+$X"} == $self->{'table'}{"$B+$Y"} and + $self->{'table'}{"$X+$A"} > $self->{'table'}{"$Y+$B"} + ) + ); +} + +sub cssd { + my ($self) = @_; + my (@candidates); + + @candidates = sort keys %{$self->{'candidates'}}; + + while (1) { + my (%transitive_defeats); + my (@active, @plist); + + ###################################################################### + # 5. From the list of [undropped] pairwise defeats, we generate a + # set of transitive defeats. + # 1. An option A transitively defeats an option C if A + # defeats C or if there is some other option B where A + # defeats B AND B transitively defeats C. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + $transitive_defeats{"$o1+$o2"} = 1 if $self->defeats($o1, $o2); + } + } + for my $i (@candidates) { + for my $j (@candidates) { + for my $k (@candidates) { + if (exists $transitive_defeats{"$j+$i"} and + exists $transitive_defeats{"$i+$k"}) + { + $transitive_defeats{"$j+$k"} = 1; + } + } + } + } + + ###################################################################### + # 6. We construct the Schwartz set from the set of transitive + # defeats. + # 1. An option A is in the Schwartz set if for all options B, + # either A transitively defeats B, or B does not + # transitively defeat A. + print "\n"; + A: for my $A (@candidates) { + for my $B (@candidates) { + next if $transitive_defeats{"$A+$B"} or not $transitive_defeats{"$B+$A"}; + # countify marks entries +++ instead of *** when they've already + # been ranked. + if ($self->{'table'}{"$A+$A"} eq '***') { + print "option $A is eliminated ($B trans-defeats $A, and $A does not trans-defeat $B)\n"; + } + next A; + } + push @active, $A; + } + print "the Schwartz set is {", join(", ", @active), "}\n"; + @candidates = @active; + + ###################################################################### + # 7. If there are defeats between options in the Schwartz set, we + # drop the weakest such defeats from the list of pairwise + # defeats, and return to step 5. + # 1. A defeat (A,X) is weaker than a defeat (B,Y) if V(A,X) + # is less than V(B,Y). Also, (A,X) is weaker than (B,Y) if + # V(A,X) is equal to V(B,Y) and V(X,A) is greater than V + # (Y,B). + # 2. A weakest defeat is a defeat that has no other defeat + # weaker than it. There may be more than one such defeat. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + push @plist, [ $o1, $o2 ] if $self->defeats($o1, $o2); + } + } + last unless @plist; + @plist = sort { + return -1 if $self->is_weaker_defeat(@$a, @$b); + return +1 if $self->is_weaker_defeat(@$b, @$a); + return 0; + } @plist; + for my $dx (@plist) { + my ($o1, $o2) = @$dx; + print("$o1+$o2 ", + $self->{'table'}{"$o1+$o2"}, " $o2+$o1 ", + $self->{'table'}{"$o2+$o1"}, "\n"); + } + my ($o1, $o2) = @{$plist[0]}; + $self->{'table'}{"$o1+$o2"} = 0; + $self->{'table'}{"$o2+$o1"} = 0; + } + + ###################################################################### + # 8. If there are no defeats within the Schwartz set, then the + # winner is chosen from the options in the Schwartz set. If + # there is only one such option, it is the winner. If there + # are multiple options, the elector with the casting vote + # chooses which of those options wins. + print "\n"; + if (@candidates > 1) { + print "result: tie between options ", join(", ", @candidates), "\n"; + } else { + print "result: option @candidates wins\n"; + } + + return @candidates; +} + +###################################################################### +# Ballot +###################################################################### + +package Ballot; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + filename => '', + default_filename => $ENV{'HOME'}."/.ballot-$election", + choices => [], + }; + + # Bless me, I'm a ballot! + bless $self, $class; + return $self; +} + +sub from_s { + my ($self, $s) = @_; + my (@choices); + + for (split "\n", $s) { + s/#.*//; + next unless /\S/; + push @choices, [ split(' ', $_) ]; + } + die("No data in string") unless @choices; + + $self->{'choices'} = \@choices; +} + +sub read { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Load the data file + open(F, "<$filename") or die("couldn't open $filename"); + { local $/ = undef; $self->from_s(<F>); } + close(F); +} + +sub populate { + my ($self) = @_; + $self->read("$Votify::datadir/ballot-$self->{election}"); + @{$self->{'choices'}} = List::Util::shuffle(@{$self->{'choices'}}); +} + +sub choices { + my ($self) = @_; + $self->{'choices'}; +} + +sub write { + my ($self, $filename) = @_; + + if ($Votify::mode ne 'user') { + die("we don't write ballots in official mode"); + } + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Don't ever overwrite a ballot + die("File already exists; please remove $filename\n") if -e $filename; + + # Write the user's ballot + open(F, ">$filename") or die "Failed writing $filename"; + print F <<EOT; +# This is a ballot for the $self->{election} election. +# Please rank your choices in order; first choice at the top and last choice at +# the bottom. You can put choices on the same line to indicate no preference +# between them. Any choices you omit from this file are implicitly added at the +# end. +# +# When you're finished editing this, the next step is to verify your ballot +# with: +# +# $Votify::zero --verify $self->{election} +# +# When that passes and you're satisfied, the final step is to submit your vote: +# +# $Votify::zero --submit $self->{election} +# + +EOT + for (@{$self->{'choices'}}) { print F "@$_\n"; } + close(F); +} + +sub verify { + my ($self) = @_; + my (%h, $master, %mh); + my (@dups, @missing, @extra); + my ($errors_found); + + # Load %h from the user's ballot + for my $line (@{$self->{'choices'}}) { + for my $entry (@$line) { + $h{$entry}++; + } + } + + # Load the master ballot into another hash and compare them. + # The master ballots always do one entry per line, making this a little + # easier. + $master = Ballot->new($self->{'election'}); + $master->populate; + %mh = map(($_->[0] => 1), @{$master->{'choices'}}); + + # Check for extra entries (write-ins should be supported in the future) + for (keys %h) { + push @extra, $_ unless exists $mh{$_}; + } + + # Check for duplicate entries + @dups = grep { $h{$_} > 1 } keys %h; + + # Check for missing entries (not necessarily an error) + for (keys %mh) { + push @missing, $_ unless exists $h{$_}; + } + + # Report errors and warnings + if (@extra) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some extra entries that are not part of this election. Sorry, +but write-ins are not (yet) supported. Please remove these from your ballot: + +EOT + print map "\t$_\n", @extra; + print "\n"; + } + $errors_found++; + } + if (@dups) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some duplicate entries. Please resolve these to a single entry +to avoid ambiguities: + +EOT + print map "\t$_\n", @dups; + print "\n"; + } + $errors_found++; + } + if (@{$self->{'choices'}} == 0) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot doesn't contain any entries. You can start over by first removing +the existing ballot, then using --new to generate a new ballot. See --help for +more information. + +EOT + } + $errors_found++; + } + elsif (@missing and $Votify::mode eq 'user') { + print <<EOT; +Your ballot is missing some entries. This is not an error, but note that these +will be implied as a final line, with no preference between them, like this: + +EOT + print "\t", join(" ", @missing), "\n"; + print "\n"; + } + if ($Votify::mode eq 'user' and !$errors_found and + @{$self->{'choices'}} == 1 and + scalar(keys %h) == scalar(keys %mh)) + { + print <<EOT; +Your ballot contains all the candidates on a single line! This means you have +no preference between the candidates. This is not an error, but note that this +is a meaningless ballot that will have no effect on the election. + +EOT + } + + # Stop if there were errors + if ($Votify::mode eq 'user' and $errors_found) { + print("There were errors found in your ballot.\n"); + die("Please correct them and try again.\n\n"); + } + return $errors_found; +} + +sub to_s { + my ($self) = @_; + join '', map "@$_\n", @{$self->{'choices'}}; +} + +1; + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + +Revision 1.3 2005/05/09 23:12:02 agriffis +Add support for registered voters + +Revision 1.2 2005/05/05 23:03:46 agriffis +Fix indentation (and some output as well) + +Revision 1.1 2005/05/05 22:05:34 agriffis +first pass at Gentoo Foundation voting program + +# vim:sw=4 et diff --git a/council2008b/ballot-council2008b b/council2008b/ballot-council2008b new file mode 100644 index 0000000..391fe75 --- /dev/null +++ b/council2008b/ballot-council2008b @@ -0,0 +1,6 @@ +dev-zero +jer +leio +ssuominen +ulm +_reopen_nominations diff --git a/council2008b/officials-council2008b b/council2008b/officials-council2008b new file mode 100644 index 0000000..23ff031 --- /dev/null +++ b/council2008b/officials-council2008b @@ -0,0 +1,5 @@ +robbat2 +jmbsvicetto +antarus +dberkholz +root diff --git a/council2008b/start-council2008b b/council2008b/start-council2008b new file mode 100644 index 0000000..2e10615 --- /dev/null +++ b/council2008b/start-council2008b @@ -0,0 +1 @@ +1227916800 diff --git a/council2008b/stop-council2008b b/council2008b/stop-council2008b new file mode 100644 index 0000000..a4fe570 --- /dev/null +++ b/council2008b/stop-council2008b @@ -0,0 +1 @@ +1228521600 diff --git a/council2008b/voters-council2008b b/council2008b/voters-council2008b new file mode 100644 index 0000000..c6c4253 --- /dev/null +++ b/council2008b/voters-council2008b @@ -0,0 +1,245 @@ +aballier +aetius +agaffney +agorf +ali_bush +amne +anant +angelos +antarus +araujo +armin76 +b33fc0d3 +bangert +bass +battousai +beandog +betelgeuse +bicatali +blackace +bluebird +bunder +calchan +caleb +cam +cardoe +carlo +caster +cedk +chainsaw +chiguire +chtekk +chutzpah +cla +codeman +coldwind +compnerd +corsair +cryos +dams +dang +darkside +dav_it +dberkholz +deathwing00 +dertobi123 +desultory +dev-zero +dirtyepic +djay +dmwaters +dostrow +dragonheart +drizzt +dsd +earthwings +ehmsen +elvanor +eva +falco +fauli +ferdy +flameeyes +flammie +fmccor +fordfrog +ford_prefect +fox2mike +fuzzyray +g2boojum +gengor +genone +genstef +gentoofan23 +george +gmsoft +graaff +grahl +gregkh +griffon26 +grobian +grozin +gurligebis +halcy0n +hanno +hattya +haubi +hawking +hd_brummy +hkbst +hncaldwell +hoffie +hollow +hparker +humpback +i92guboj +ian +ikelos +iluxa +jaervosz +jer +je_fro +jkt +jmbsvicetto +joker +jokey +josejx +joslwah +jrinkovs +jsbronder +jurek +kallamej +kalos +kanaka +ken69267 +keri +kernelsensei +keytoaster +killerfox +kingtaco +klausman +klieber +kolmodin +kumba +lack +lavajoe +leio +livewire +loki_val +lordvan +lu_zero +mabi +maedhros +maekke +marineam +markm +markusle +mark_alec +matsuu +mattepiu +mbres +mdisney +mduft +mescalinum +miknix +mjf +moloh +mpagano +mrness +mr_bones_ +mueli +musikc +neddyseagoon +nelchael +nerdboy +neurogeek +neysx +nichoj +nightmorph +nixnut +nixphoeni +nyhm +omp +patrick +pauldv +pchrist +pebenito +peitolm +peper +phosphan +pilla +pingu +pjp +polvi +pva +pvdabeel +py +pythonhead +quantumsummers +r0bertz +radek +rajiv +ramereth +rane +ranger +rbu +redhatter +remi +ribosome +rich0 +ricmm +rl03 +robbat2 +s4t4n +sbriesen +scarabeus +scen +serkan +shadow +shellsage +shindo +sirseoman +smithj +solar +spock +ssuominen +steev +stefaan +suka +swegener +swift +tantive +tcunha +tester +tgall +tgurr +the_paya +think4urs11 +timebandit +titefleur +tomk +tommy +tove +trapni +truedfx +tsunam +tupone +ulm +vanquirius +vapier +vorlon +voxus +voyageur +weaver +welp +williamh +wormo +wrobel +wschlich +xmerlin +yngwin +yoswink +yuval +yvasilev +zmedico +zzam diff --git a/council200906/Votify.pm b/council200906/Votify.pm new file mode 100644 index 0000000..0c5565d --- /dev/null +++ b/council200906/Votify.pm @@ -0,0 +1,683 @@ +# $Id: Votify.pm,v 1.5 2005/05/16 23:58:09 agriffis Exp $ +# +# Copyright 2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# votify.pm: common classes for votify and countify +# + +package Votify; + +use POSIX; +use List::Util; +use strict; + +our ($datadir) = '/home/fox2mike/elections'; +(our $zero = $0) =~ s,.*/,,; + +sub import { + my ($class, $mode) = @_; + $Votify::mode = $mode; +} + +###################################################################### +# OfficialList +###################################################################### + +package OfficialList; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + officials => [], + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/officials-$election") + or die("failed to open officials file"); + chomp(@{$self->{'officials'}} = <F>); + close(F); + + bless $self, $class; + return $self; +} + +sub officials { + my ($self) = @_; + @{$self->{'officials'}}; +} + +###################################################################### +# VoterList +###################################################################### + +package VoterList; + +sub new { + my ($class, $election) = @_; + my (@voterlist, $r); + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/confs-$election", + filename => '', + voters => {}, # confnum => voter + confs => {}, # voter => confnum + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/voters-$election") + or die("failed to open voters file"); + chomp(@voterlist = <F>); + close(F); + + # assign confirmation numbers randomly + for my $v (@voterlist) { + do { $r = int rand 0xffff } while exists $self->{'voters'}{$r}; + $self->{'voters'}{$r} = $v; + $self->{'confs'}{$v} = $r; + } + + unless (keys %{$self->{'voters'}} == keys %{$self->{'confs'}}) { + die("discrepancy deteced in VoterList"); + } + + bless $self, $class; + return $self; +} + +sub confs { + my ($self) = @_; + sort keys %{$self->{'voters'}}; +} + +sub voters { + my ($self) = @_; + sort keys %{$self->{'confs'}}; +} + +sub getvoter { + my ($self, $conf) = @_; + return $self->{'voters'}{$conf}; +} + +sub getconf { + my ($self, $voter) = @_; + return $self->{'confs'}{$voter}; +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c ($self->confs) { + printf F "%04x %s\n", $c, $self->getvoter($c); + } + close F; +} + +###################################################################### +# MasterBallot +###################################################################### + +package MasterBallot; + +use Data::Dumper; + +sub new { + my ($class, $election, $vl) = @_; + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/master-$election", + filename => '', + voterlist => $vl, + ballots => {}, # indexed by conf num + candidates => undef, # indexed by long name + table => undef, # indexed by row+column + }; + + bless $self, $class; + return $self; +} + +sub collect { + my ($self, @voters) = @_; + my ($c, $v, $home, @pwentry); + + for my $v (@voters) { + unless (defined ($c = $self->{'voterlist'}->getconf($v))) { + die "$v does not correspond to any confirmation number"; + } + + @pwentry = getpwnam($v); + unless (@pwentry) { + print STDERR "Warning: unknown user: $v\n"; + next; + } + + $home = $pwentry[7]; + unless (-d $home) { + print STDERR "Warning: no directory: $home\n"; + next; + } + + if (-f "$home/.ballot-$self->{election}-submitted") { + my ($b) = Ballot->new($self->{'election'}); + $b->read("$home/.ballot-$self->{election}-submitted"); + if ($b->verify) { + print STDERR "Errors found in ballot: $v\n"; + next; + } + $self->{'ballots'}{$c} = $b; + } + elsif (-f "$home/.ballot-$self->{election}") { + print STDERR "Warning: $v did not submit their ballot\n"; + } + } +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c (sort keys %{$self->{'ballots'}}) { + printf F "--------- confirmation %04x ---------\n", $c; + print F $self->{'ballots'}{$c}->to_s + } + close F; +} + +sub read { + my ($self, $filename) = @_; + my ($election, $entries) = $self->{'election'}; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + open(F, "<$filename") or die("can't read $filename"); + { local $/ = undef; $entries = <F>; } + for my $e (split /^--------- confirmation /m, $entries) { + next unless $e; # skip the first zero-length record + unless ($e =~ /^([[:xdigit:]]{4}) ---------\n(.*)$/s) { + die "error parsing entry:\n$e"; + } + my ($c, $s, $b) = ($1, $2, Ballot->new($election)); + $b->from_s($s); + $self->{'ballots'}{hex($c)} = $b; + } +} + +sub generate_candidates { + my ($self) = @_; + my ($B, @C, $s); + + # nb: would need to scan all the ballots to support write-ins + $B = Ballot->new($self->{'election'}); + $B->populate; + @C = sort map $_->[0], @{$B->choices}; + for my $c (@C) { + $s = $c; # in case $c is shorter than 5 chars + for (my $i=5; $i<=length($c); $i++) { + $s = substr $c, 0, $i; + print join(" ", grep(/^$s/, @C)), "\n"; + last unless grep(/^$s/, @C) > 1; + } + $self->{'candidates'}{$c} = $s; + } +} + +sub tabulate { + my ($self) = @_; + my (@candidates); # full candidate list + my (%table); # resulting table, row.colum where row defeats column + $self->{'table'} = \%table; + + $self->generate_candidates unless $self->{'candidates'}; + @candidates = keys %{$self->{'candidates'}}; + for my $c1 (@candidates) { + for my $c2 (@candidates) { + $table{"$c1+$c2"} = 0; + } + $table{"$c1+$c1"} = '***'; + } + + # generate the table first; + # walk through the ballots, tallying the rankings expressed by each ballot + for my $b (values %{$self->{'ballots'}}) { + my (@choices, %ranks); + + #print "looking at ballot:\n", $b->to_s, "\n"; + + # first determine the ranking of each candidate. default ranking is + # scalar @candidates. + @choices = @{$b->choices}; + @ranks{@candidates} = (scalar @candidates) x @candidates; + #print "ranks before determining:\n", Dumper(\%ranks); + for (my $i = 0; $i < @choices; $i++) { + @ranks{@{$choices[$i]}} = ($i) x @{$choices[$i]}; + } + #print "ranks after determining:\n", Dumper(\%ranks); + + # second add the results of all the pairwise races into our table + for my $c1 (@candidates) { + for my $c2 (@candidates) { + next if $c1 eq $c2; + $table{"$c1+$c2"}++ if $ranks{$c1} < $ranks{$c2}; + } + } + #print "table after adding:\n"; + #$self->display_table; + } +} + +sub display_table { + my ($self) = @_; + my (@longnames, @shortnames); + my ($minlen, $maxlen, $formatstr) = (0, 4, ''); + + $self->generate_candidates unless $self->{'candidates'}; + @longnames = sort keys %{$self->{'candidates'}}; + @shortnames = sort values %{$self->{'candidates'}}; + $minlen = length scalar keys %{$self->{'ballots'}}; + $minlen = 5 if $minlen < 5; + + # build the format string + for my $s (@shortnames) { + if (length($s) > $minlen) { + $formatstr .= " %" . length($s) . "s"; + } else { + $formatstr .= " %${minlen}s"; + } + } + map { $maxlen = length($_) if length($_) > $maxlen } @longnames; + + # prepend the row header; append newline + $formatstr = "%${maxlen}s" . $formatstr . "\n"; + + # column headers + printf $formatstr, '', @shortnames; + + # rows + for my $l (@longnames) { + printf $formatstr, $l, @{$self->{'table'}}{map "$l+$_", @longnames}; + } +} + +# utility for cssd +sub defeats { + my ($self, $o1, $o2) = @_; + return 0 if $o1 eq $o2; + $self->{'table'}{"$o1+$o2"} > $self->{'table'}{"$o2+$o1"}; +} + +# utility for cssd +sub is_weaker_defeat { + my ($self, $A, $X, $B, $Y) = @_; + die unless $self->defeats($A, $X); + die unless $self->defeats($B, $Y); + return ( + $self->{'table'}{"$A+$X"} < $self->{'table'}{"$B+$Y"} or + ( + $self->{'table'}{"$A+$X"} == $self->{'table'}{"$B+$Y"} and + $self->{'table'}{"$X+$A"} > $self->{'table'}{"$Y+$B"} + ) + ); +} + +sub cssd { + my ($self) = @_; + my (@candidates); + + @candidates = sort keys %{$self->{'candidates'}}; + + while (1) { + my (%transitive_defeats); + my (@active, @plist); + + ###################################################################### + # 5. From the list of [undropped] pairwise defeats, we generate a + # set of transitive defeats. + # 1. An option A transitively defeats an option C if A + # defeats C or if there is some other option B where A + # defeats B AND B transitively defeats C. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + $transitive_defeats{"$o1+$o2"} = 1 if $self->defeats($o1, $o2); + } + } + for my $i (@candidates) { + for my $j (@candidates) { + for my $k (@candidates) { + if (exists $transitive_defeats{"$j+$i"} and + exists $transitive_defeats{"$i+$k"}) + { + $transitive_defeats{"$j+$k"} = 1; + } + } + } + } + + ###################################################################### + # 6. We construct the Schwartz set from the set of transitive + # defeats. + # 1. An option A is in the Schwartz set if for all options B, + # either A transitively defeats B, or B does not + # transitively defeat A. + print "\n"; + A: for my $A (@candidates) { + for my $B (@candidates) { + next if $transitive_defeats{"$A+$B"} or not $transitive_defeats{"$B+$A"}; + # countify marks entries +++ instead of *** when they've already + # been ranked. + if ($self->{'table'}{"$A+$A"} eq '***') { + print "option $A is eliminated ($B trans-defeats $A, and $A does not trans-defeat $B)\n"; + } + next A; + } + push @active, $A; + } + print "the Schwartz set is {", join(", ", @active), "}\n"; + @candidates = @active; + + ###################################################################### + # 7. If there are defeats between options in the Schwartz set, we + # drop the weakest such defeats from the list of pairwise + # defeats, and return to step 5. + # 1. A defeat (A,X) is weaker than a defeat (B,Y) if V(A,X) + # is less than V(B,Y). Also, (A,X) is weaker than (B,Y) if + # V(A,X) is equal to V(B,Y) and V(X,A) is greater than V + # (Y,B). + # 2. A weakest defeat is a defeat that has no other defeat + # weaker than it. There may be more than one such defeat. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + push @plist, [ $o1, $o2 ] if $self->defeats($o1, $o2); + } + } + last unless @plist; + @plist = sort { + return -1 if $self->is_weaker_defeat(@$a, @$b); + return +1 if $self->is_weaker_defeat(@$b, @$a); + return 0; + } @plist; + for my $dx (@plist) { + my ($o1, $o2) = @$dx; + print("$o1+$o2 ", + $self->{'table'}{"$o1+$o2"}, " $o2+$o1 ", + $self->{'table'}{"$o2+$o1"}, "\n"); + } + my ($o1, $o2) = @{$plist[0]}; + $self->{'table'}{"$o1+$o2"} = 0; + $self->{'table'}{"$o2+$o1"} = 0; + } + + ###################################################################### + # 8. If there are no defeats within the Schwartz set, then the + # winner is chosen from the options in the Schwartz set. If + # there is only one such option, it is the winner. If there + # are multiple options, the elector with the casting vote + # chooses which of those options wins. + print "\n"; + if (@candidates > 1) { + print "result: tie between options ", join(", ", @candidates), "\n"; + } else { + print "result: option @candidates wins\n"; + } + + return @candidates; +} + +###################################################################### +# Ballot +###################################################################### + +package Ballot; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + filename => '', + default_filename => $ENV{'HOME'}."/.ballot-$election", + choices => [], + }; + + # Bless me, I'm a ballot! + bless $self, $class; + return $self; +} + +sub from_s { + my ($self, $s) = @_; + my (@choices); + + for (split "\n", $s) { + s/#.*//; + next unless /\S/; + push @choices, [ split(' ', $_) ]; + } + die("No data in string") unless @choices; + + $self->{'choices'} = \@choices; +} + +sub read { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Load the data file + open(F, "<$filename") or die("couldn't open $filename"); + { local $/ = undef; $self->from_s(<F>); } + close(F); +} + +sub populate { + my ($self) = @_; + $self->read("$Votify::datadir/ballot-$self->{election}"); + @{$self->{'choices'}} = List::Util::shuffle(@{$self->{'choices'}}); +} + +sub choices { + my ($self) = @_; + $self->{'choices'}; +} + +sub write { + my ($self, $filename) = @_; + + if ($Votify::mode ne 'user') { + die("we don't write ballots in official mode"); + } + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Don't ever overwrite a ballot + die("File already exists; please remove $filename\n") if -e $filename; + + # Write the user's ballot + open(F, ">$filename") or die "Failed writing $filename"; + print F <<EOT; +# This is a ballot for the $self->{election} election. +# Please rank your choices in order; first choice at the top and last choice at +# the bottom. You can put choices on the same line to indicate no preference +# between them. Any choices you omit from this file are implicitly added at the +# end. +# +# When you're finished editing this, the next step is to verify your ballot +# with: +# +# $Votify::zero --verify $self->{election} +# +# When that passes and you're satisfied, the final step is to submit your vote: +# +# $Votify::zero --submit $self->{election} +# + +EOT + for (@{$self->{'choices'}}) { print F "@$_\n"; } + close(F); +} + +sub verify { + my ($self) = @_; + my (%h, $master, %mh); + my (@dups, @missing, @extra); + my ($errors_found); + + # Load %h from the user's ballot + for my $line (@{$self->{'choices'}}) { + for my $entry (@$line) { + $h{$entry}++; + } + } + + # Load the master ballot into another hash and compare them. + # The master ballots always do one entry per line, making this a little + # easier. + $master = Ballot->new($self->{'election'}); + $master->populate; + %mh = map(($_->[0] => 1), @{$master->{'choices'}}); + + # Check for extra entries (write-ins should be supported in the future) + for (keys %h) { + push @extra, $_ unless exists $mh{$_}; + } + + # Check for duplicate entries + @dups = grep { $h{$_} > 1 } keys %h; + + # Check for missing entries (not necessarily an error) + for (keys %mh) { + push @missing, $_ unless exists $h{$_}; + } + + # Report errors and warnings + if (@extra) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some extra entries that are not part of this election. Sorry, +but write-ins are not (yet) supported. Please remove these from your ballot: + +EOT + print map "\t$_\n", @extra; + print "\n"; + } + $errors_found++; + } + if (@dups) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some duplicate entries. Please resolve these to a single entry +to avoid ambiguities: + +EOT + print map "\t$_\n", @dups; + print "\n"; + } + $errors_found++; + } + if (@{$self->{'choices'}} == 0) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot doesn't contain any entries. You can start over by first removing +the existing ballot, then using --new to generate a new ballot. See --help for +more information. + +EOT + } + $errors_found++; + } + elsif (@missing and $Votify::mode eq 'user') { + print <<EOT; +Your ballot is missing some entries. This is not an error, but note that these +will be implied as a final line, with no preference between them, like this: + +EOT + print "\t", join(" ", @missing), "\n"; + print "\n"; + } + if ($Votify::mode eq 'user' and !$errors_found and + @{$self->{'choices'}} == 1 and + scalar(keys %h) == scalar(keys %mh)) + { + print <<EOT; +Your ballot contains all the candidates on a single line! This means you have +no preference between the candidates. This is not an error, but note that this +is a meaningless ballot that will have no effect on the election. + +EOT + } + + # Stop if there were errors + if ($Votify::mode eq 'user' and $errors_found) { + print("There were errors found in your ballot.\n"); + die("Please correct them and try again.\n\n"); + } + return $errors_found; +} + +sub to_s { + my ($self) = @_; + join '', map "@$_\n", @{$self->{'choices'}}; +} + +1; + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + +Revision 1.3 2005/05/09 23:12:02 agriffis +Add support for registered voters + +Revision 1.2 2005/05/05 23:03:46 agriffis +Fix indentation (and some output as well) + +Revision 1.1 2005/05/05 22:05:34 agriffis +first pass at Gentoo Foundation voting program + +# vim:sw=4 et diff --git a/council200906/ballot-council200906 b/council200906/ballot-council200906 new file mode 100644 index 0000000..64e401d --- /dev/null +++ b/council200906/ballot-council200906 @@ -0,0 +1,14 @@ +betelgeuse +calchan +dertobi123 +dev-zero +gentoofan23 +leio +lu_zero +patrick +peper +scarabeus +solar +ssuominen +ulm +_reopen_nominations diff --git a/council200906/officials-council200906 b/council200906/officials-council200906 new file mode 100644 index 0000000..bc4323e --- /dev/null +++ b/council200906/officials-council200906 @@ -0,0 +1,4 @@ +jmbsvicetto +rane +neddyseagoon +root diff --git a/council200906/start-council200906 b/council200906/start-council200906 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/council200906/start-council200906 diff --git a/council200906/stop-council200906 b/council200906/stop-council200906 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/council200906/stop-council200906 diff --git a/council200906/voters-council200906 b/council200906/voters-council200906 new file mode 100644 index 0000000..0a667b8 --- /dev/null +++ b/council200906/voters-council200906 @@ -0,0 +1,248 @@ +a3li +aballier +aetius +agaffney +alexxy +ali_bush +amne +angelos +antarus +araujo +arfrever +armin76 +asn +b33fc0d3 +bangert +basic +bass +battousai +beandog +betelgeuse +bicatali +billie +blackace +bluebird +calchan +caleb +cam +cardoe +carlo +caster +cedk +chainsaw +chiguire +chtekk +chutzpah +cla +codeman +coldwind +corsair +cryos +d2_racing +dagger +dams +dang +darkside +dav_it +dberkholz +deathwing00 +dertobi123 +desultory +dev-zero +dirtyepic +djay +dmwaters +dostrow +dragonheart +drizzt +dsd +earthwings +elvanor +eva +falco +fauli +flameeyes +flammie +fmccor +ford_prefect +fordfrog +fox2mike +fuzzyray +g2boojum +gbittorrent +gengor +genstef +gentoofan23 +george +gmsoft +graaff +gregkh +griffon26 +grobian +grozin +gurligebis +halcy0n +hanno +hattya +haubi +hd_brummy +hkbst +hncaldwell +hoffie +hollow +hparker +humpback +hwoarang +i92guboj +ian +idl0r +ikelos +iluxa +jaervosz +je_fro +jer +jkt +jmbsvicetto +joker +jokey +josejx +jrinkovs +jsbronder +jurek +kallamej +kalos +kanaka +ken69267 +keri +kernelsensei +keytoaster +killerfox +kingtaco +klausman +klieber +kolmodin +kumba +lack +lavajoe +leio +livewire +loki_val +lordvan +lu_zero +mabi +maedhros +maekke +marineam +mark_alec +markm +markusle +matsuu +mattepiu +mbres +mdisney +mduft +mescalinum +micm +miknix +mpagano +mr_bones_ +mrness +mrpouet +mueli +musikc +neddyseagoon +nelchael +nerdboy +neurogeek +neysx +nightmorph +nirbheek +nixnut +nixphoeni +nyhm +omp +patrick +pauldv +pchrist +pebenito +peitolm +peper +phosphan +pilla +pjp +polvi +polynomial-c +pva +pvdabeel +py +pythonhead +quantumsummers +r0bertz +radek +rajiv +ramereth +rane +ranger +rbu +redhatter +remi +ribosome +rich0 +ricmm +rl03 +robbat2 +s4t4n +sbriesen +scarabeus +scen +serkan +shadow +shindo +sirseoman +smithj +solar +spock +ssuominen +steev +stefaan +suka +swegener +tacotest +tampakrap +tantive +tcunha +tester +tgall +tgurr +the_paya +think4urs11 +timebandit +titefleur +tomk +tommy +tove +trapni +truedfx +tsunam +tupone +ulm +vadimk +vanquirius +vapier +volkmar +vorlon +voxus +voyageur +weaver +welp +williamh +wormo +wrobel +wschlich +xmerlin +yngwin +yoswink +yuval +yvasilev +zmedico +zzam diff --git a/council200912/Votify.pm b/council200912/Votify.pm new file mode 100644 index 0000000..0c5565d --- /dev/null +++ b/council200912/Votify.pm @@ -0,0 +1,683 @@ +# $Id: Votify.pm,v 1.5 2005/05/16 23:58:09 agriffis Exp $ +# +# Copyright 2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# votify.pm: common classes for votify and countify +# + +package Votify; + +use POSIX; +use List::Util; +use strict; + +our ($datadir) = '/home/fox2mike/elections'; +(our $zero = $0) =~ s,.*/,,; + +sub import { + my ($class, $mode) = @_; + $Votify::mode = $mode; +} + +###################################################################### +# OfficialList +###################################################################### + +package OfficialList; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + officials => [], + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/officials-$election") + or die("failed to open officials file"); + chomp(@{$self->{'officials'}} = <F>); + close(F); + + bless $self, $class; + return $self; +} + +sub officials { + my ($self) = @_; + @{$self->{'officials'}}; +} + +###################################################################### +# VoterList +###################################################################### + +package VoterList; + +sub new { + my ($class, $election) = @_; + my (@voterlist, $r); + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/confs-$election", + filename => '', + voters => {}, # confnum => voter + confs => {}, # voter => confnum + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/voters-$election") + or die("failed to open voters file"); + chomp(@voterlist = <F>); + close(F); + + # assign confirmation numbers randomly + for my $v (@voterlist) { + do { $r = int rand 0xffff } while exists $self->{'voters'}{$r}; + $self->{'voters'}{$r} = $v; + $self->{'confs'}{$v} = $r; + } + + unless (keys %{$self->{'voters'}} == keys %{$self->{'confs'}}) { + die("discrepancy deteced in VoterList"); + } + + bless $self, $class; + return $self; +} + +sub confs { + my ($self) = @_; + sort keys %{$self->{'voters'}}; +} + +sub voters { + my ($self) = @_; + sort keys %{$self->{'confs'}}; +} + +sub getvoter { + my ($self, $conf) = @_; + return $self->{'voters'}{$conf}; +} + +sub getconf { + my ($self, $voter) = @_; + return $self->{'confs'}{$voter}; +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c ($self->confs) { + printf F "%04x %s\n", $c, $self->getvoter($c); + } + close F; +} + +###################################################################### +# MasterBallot +###################################################################### + +package MasterBallot; + +use Data::Dumper; + +sub new { + my ($class, $election, $vl) = @_; + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/master-$election", + filename => '', + voterlist => $vl, + ballots => {}, # indexed by conf num + candidates => undef, # indexed by long name + table => undef, # indexed by row+column + }; + + bless $self, $class; + return $self; +} + +sub collect { + my ($self, @voters) = @_; + my ($c, $v, $home, @pwentry); + + for my $v (@voters) { + unless (defined ($c = $self->{'voterlist'}->getconf($v))) { + die "$v does not correspond to any confirmation number"; + } + + @pwentry = getpwnam($v); + unless (@pwentry) { + print STDERR "Warning: unknown user: $v\n"; + next; + } + + $home = $pwentry[7]; + unless (-d $home) { + print STDERR "Warning: no directory: $home\n"; + next; + } + + if (-f "$home/.ballot-$self->{election}-submitted") { + my ($b) = Ballot->new($self->{'election'}); + $b->read("$home/.ballot-$self->{election}-submitted"); + if ($b->verify) { + print STDERR "Errors found in ballot: $v\n"; + next; + } + $self->{'ballots'}{$c} = $b; + } + elsif (-f "$home/.ballot-$self->{election}") { + print STDERR "Warning: $v did not submit their ballot\n"; + } + } +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c (sort keys %{$self->{'ballots'}}) { + printf F "--------- confirmation %04x ---------\n", $c; + print F $self->{'ballots'}{$c}->to_s + } + close F; +} + +sub read { + my ($self, $filename) = @_; + my ($election, $entries) = $self->{'election'}; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + open(F, "<$filename") or die("can't read $filename"); + { local $/ = undef; $entries = <F>; } + for my $e (split /^--------- confirmation /m, $entries) { + next unless $e; # skip the first zero-length record + unless ($e =~ /^([[:xdigit:]]{4}) ---------\n(.*)$/s) { + die "error parsing entry:\n$e"; + } + my ($c, $s, $b) = ($1, $2, Ballot->new($election)); + $b->from_s($s); + $self->{'ballots'}{hex($c)} = $b; + } +} + +sub generate_candidates { + my ($self) = @_; + my ($B, @C, $s); + + # nb: would need to scan all the ballots to support write-ins + $B = Ballot->new($self->{'election'}); + $B->populate; + @C = sort map $_->[0], @{$B->choices}; + for my $c (@C) { + $s = $c; # in case $c is shorter than 5 chars + for (my $i=5; $i<=length($c); $i++) { + $s = substr $c, 0, $i; + print join(" ", grep(/^$s/, @C)), "\n"; + last unless grep(/^$s/, @C) > 1; + } + $self->{'candidates'}{$c} = $s; + } +} + +sub tabulate { + my ($self) = @_; + my (@candidates); # full candidate list + my (%table); # resulting table, row.colum where row defeats column + $self->{'table'} = \%table; + + $self->generate_candidates unless $self->{'candidates'}; + @candidates = keys %{$self->{'candidates'}}; + for my $c1 (@candidates) { + for my $c2 (@candidates) { + $table{"$c1+$c2"} = 0; + } + $table{"$c1+$c1"} = '***'; + } + + # generate the table first; + # walk through the ballots, tallying the rankings expressed by each ballot + for my $b (values %{$self->{'ballots'}}) { + my (@choices, %ranks); + + #print "looking at ballot:\n", $b->to_s, "\n"; + + # first determine the ranking of each candidate. default ranking is + # scalar @candidates. + @choices = @{$b->choices}; + @ranks{@candidates} = (scalar @candidates) x @candidates; + #print "ranks before determining:\n", Dumper(\%ranks); + for (my $i = 0; $i < @choices; $i++) { + @ranks{@{$choices[$i]}} = ($i) x @{$choices[$i]}; + } + #print "ranks after determining:\n", Dumper(\%ranks); + + # second add the results of all the pairwise races into our table + for my $c1 (@candidates) { + for my $c2 (@candidates) { + next if $c1 eq $c2; + $table{"$c1+$c2"}++ if $ranks{$c1} < $ranks{$c2}; + } + } + #print "table after adding:\n"; + #$self->display_table; + } +} + +sub display_table { + my ($self) = @_; + my (@longnames, @shortnames); + my ($minlen, $maxlen, $formatstr) = (0, 4, ''); + + $self->generate_candidates unless $self->{'candidates'}; + @longnames = sort keys %{$self->{'candidates'}}; + @shortnames = sort values %{$self->{'candidates'}}; + $minlen = length scalar keys %{$self->{'ballots'}}; + $minlen = 5 if $minlen < 5; + + # build the format string + for my $s (@shortnames) { + if (length($s) > $minlen) { + $formatstr .= " %" . length($s) . "s"; + } else { + $formatstr .= " %${minlen}s"; + } + } + map { $maxlen = length($_) if length($_) > $maxlen } @longnames; + + # prepend the row header; append newline + $formatstr = "%${maxlen}s" . $formatstr . "\n"; + + # column headers + printf $formatstr, '', @shortnames; + + # rows + for my $l (@longnames) { + printf $formatstr, $l, @{$self->{'table'}}{map "$l+$_", @longnames}; + } +} + +# utility for cssd +sub defeats { + my ($self, $o1, $o2) = @_; + return 0 if $o1 eq $o2; + $self->{'table'}{"$o1+$o2"} > $self->{'table'}{"$o2+$o1"}; +} + +# utility for cssd +sub is_weaker_defeat { + my ($self, $A, $X, $B, $Y) = @_; + die unless $self->defeats($A, $X); + die unless $self->defeats($B, $Y); + return ( + $self->{'table'}{"$A+$X"} < $self->{'table'}{"$B+$Y"} or + ( + $self->{'table'}{"$A+$X"} == $self->{'table'}{"$B+$Y"} and + $self->{'table'}{"$X+$A"} > $self->{'table'}{"$Y+$B"} + ) + ); +} + +sub cssd { + my ($self) = @_; + my (@candidates); + + @candidates = sort keys %{$self->{'candidates'}}; + + while (1) { + my (%transitive_defeats); + my (@active, @plist); + + ###################################################################### + # 5. From the list of [undropped] pairwise defeats, we generate a + # set of transitive defeats. + # 1. An option A transitively defeats an option C if A + # defeats C or if there is some other option B where A + # defeats B AND B transitively defeats C. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + $transitive_defeats{"$o1+$o2"} = 1 if $self->defeats($o1, $o2); + } + } + for my $i (@candidates) { + for my $j (@candidates) { + for my $k (@candidates) { + if (exists $transitive_defeats{"$j+$i"} and + exists $transitive_defeats{"$i+$k"}) + { + $transitive_defeats{"$j+$k"} = 1; + } + } + } + } + + ###################################################################### + # 6. We construct the Schwartz set from the set of transitive + # defeats. + # 1. An option A is in the Schwartz set if for all options B, + # either A transitively defeats B, or B does not + # transitively defeat A. + print "\n"; + A: for my $A (@candidates) { + for my $B (@candidates) { + next if $transitive_defeats{"$A+$B"} or not $transitive_defeats{"$B+$A"}; + # countify marks entries +++ instead of *** when they've already + # been ranked. + if ($self->{'table'}{"$A+$A"} eq '***') { + print "option $A is eliminated ($B trans-defeats $A, and $A does not trans-defeat $B)\n"; + } + next A; + } + push @active, $A; + } + print "the Schwartz set is {", join(", ", @active), "}\n"; + @candidates = @active; + + ###################################################################### + # 7. If there are defeats between options in the Schwartz set, we + # drop the weakest such defeats from the list of pairwise + # defeats, and return to step 5. + # 1. A defeat (A,X) is weaker than a defeat (B,Y) if V(A,X) + # is less than V(B,Y). Also, (A,X) is weaker than (B,Y) if + # V(A,X) is equal to V(B,Y) and V(X,A) is greater than V + # (Y,B). + # 2. A weakest defeat is a defeat that has no other defeat + # weaker than it. There may be more than one such defeat. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + push @plist, [ $o1, $o2 ] if $self->defeats($o1, $o2); + } + } + last unless @plist; + @plist = sort { + return -1 if $self->is_weaker_defeat(@$a, @$b); + return +1 if $self->is_weaker_defeat(@$b, @$a); + return 0; + } @plist; + for my $dx (@plist) { + my ($o1, $o2) = @$dx; + print("$o1+$o2 ", + $self->{'table'}{"$o1+$o2"}, " $o2+$o1 ", + $self->{'table'}{"$o2+$o1"}, "\n"); + } + my ($o1, $o2) = @{$plist[0]}; + $self->{'table'}{"$o1+$o2"} = 0; + $self->{'table'}{"$o2+$o1"} = 0; + } + + ###################################################################### + # 8. If there are no defeats within the Schwartz set, then the + # winner is chosen from the options in the Schwartz set. If + # there is only one such option, it is the winner. If there + # are multiple options, the elector with the casting vote + # chooses which of those options wins. + print "\n"; + if (@candidates > 1) { + print "result: tie between options ", join(", ", @candidates), "\n"; + } else { + print "result: option @candidates wins\n"; + } + + return @candidates; +} + +###################################################################### +# Ballot +###################################################################### + +package Ballot; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + filename => '', + default_filename => $ENV{'HOME'}."/.ballot-$election", + choices => [], + }; + + # Bless me, I'm a ballot! + bless $self, $class; + return $self; +} + +sub from_s { + my ($self, $s) = @_; + my (@choices); + + for (split "\n", $s) { + s/#.*//; + next unless /\S/; + push @choices, [ split(' ', $_) ]; + } + die("No data in string") unless @choices; + + $self->{'choices'} = \@choices; +} + +sub read { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Load the data file + open(F, "<$filename") or die("couldn't open $filename"); + { local $/ = undef; $self->from_s(<F>); } + close(F); +} + +sub populate { + my ($self) = @_; + $self->read("$Votify::datadir/ballot-$self->{election}"); + @{$self->{'choices'}} = List::Util::shuffle(@{$self->{'choices'}}); +} + +sub choices { + my ($self) = @_; + $self->{'choices'}; +} + +sub write { + my ($self, $filename) = @_; + + if ($Votify::mode ne 'user') { + die("we don't write ballots in official mode"); + } + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Don't ever overwrite a ballot + die("File already exists; please remove $filename\n") if -e $filename; + + # Write the user's ballot + open(F, ">$filename") or die "Failed writing $filename"; + print F <<EOT; +# This is a ballot for the $self->{election} election. +# Please rank your choices in order; first choice at the top and last choice at +# the bottom. You can put choices on the same line to indicate no preference +# between them. Any choices you omit from this file are implicitly added at the +# end. +# +# When you're finished editing this, the next step is to verify your ballot +# with: +# +# $Votify::zero --verify $self->{election} +# +# When that passes and you're satisfied, the final step is to submit your vote: +# +# $Votify::zero --submit $self->{election} +# + +EOT + for (@{$self->{'choices'}}) { print F "@$_\n"; } + close(F); +} + +sub verify { + my ($self) = @_; + my (%h, $master, %mh); + my (@dups, @missing, @extra); + my ($errors_found); + + # Load %h from the user's ballot + for my $line (@{$self->{'choices'}}) { + for my $entry (@$line) { + $h{$entry}++; + } + } + + # Load the master ballot into another hash and compare them. + # The master ballots always do one entry per line, making this a little + # easier. + $master = Ballot->new($self->{'election'}); + $master->populate; + %mh = map(($_->[0] => 1), @{$master->{'choices'}}); + + # Check for extra entries (write-ins should be supported in the future) + for (keys %h) { + push @extra, $_ unless exists $mh{$_}; + } + + # Check for duplicate entries + @dups = grep { $h{$_} > 1 } keys %h; + + # Check for missing entries (not necessarily an error) + for (keys %mh) { + push @missing, $_ unless exists $h{$_}; + } + + # Report errors and warnings + if (@extra) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some extra entries that are not part of this election. Sorry, +but write-ins are not (yet) supported. Please remove these from your ballot: + +EOT + print map "\t$_\n", @extra; + print "\n"; + } + $errors_found++; + } + if (@dups) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some duplicate entries. Please resolve these to a single entry +to avoid ambiguities: + +EOT + print map "\t$_\n", @dups; + print "\n"; + } + $errors_found++; + } + if (@{$self->{'choices'}} == 0) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot doesn't contain any entries. You can start over by first removing +the existing ballot, then using --new to generate a new ballot. See --help for +more information. + +EOT + } + $errors_found++; + } + elsif (@missing and $Votify::mode eq 'user') { + print <<EOT; +Your ballot is missing some entries. This is not an error, but note that these +will be implied as a final line, with no preference between them, like this: + +EOT + print "\t", join(" ", @missing), "\n"; + print "\n"; + } + if ($Votify::mode eq 'user' and !$errors_found and + @{$self->{'choices'}} == 1 and + scalar(keys %h) == scalar(keys %mh)) + { + print <<EOT; +Your ballot contains all the candidates on a single line! This means you have +no preference between the candidates. This is not an error, but note that this +is a meaningless ballot that will have no effect on the election. + +EOT + } + + # Stop if there were errors + if ($Votify::mode eq 'user' and $errors_found) { + print("There were errors found in your ballot.\n"); + die("Please correct them and try again.\n\n"); + } + return $errors_found; +} + +sub to_s { + my ($self) = @_; + join '', map "@$_\n", @{$self->{'choices'}}; +} + +1; + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + +Revision 1.3 2005/05/09 23:12:02 agriffis +Add support for registered voters + +Revision 1.2 2005/05/05 23:03:46 agriffis +Fix indentation (and some output as well) + +Revision 1.1 2005/05/05 22:05:34 agriffis +first pass at Gentoo Foundation voting program + +# vim:sw=4 et diff --git a/council200912/ballot-council200912 b/council200912/ballot-council200912 new file mode 100644 index 0000000..0471032 --- /dev/null +++ b/council200912/ballot-council200912 @@ -0,0 +1,6 @@ +dev-zero +patrick +scarabeus +tanderson +wired +_reopen_nominations diff --git a/council200912/council2009-devs-list b/council200912/council2009-devs-list new file mode 100644 index 0000000..5c41357 --- /dev/null +++ b/council200912/council2009-devs-list @@ -0,0 +1,260 @@ +a3li +aballier +abcd +aetius +agaffney +alexxy +ali_bush +amne +anarchy +angelos +antarus +araujo +arfrever +armin76 +asn +asym +ayoy +b33fc0d3 +bangert +basic +bass +battousai +beandog +betelgeuse +bicatali +billie +blackace +bluebird +calchan +cam +cardoe +carlo +caster +cedk +chainsaw +chiguire +chtekk +chutzpah +cla +codeman +coldwind +corsair +craig +cryos +d2_racing +dabbott +dagger +dams +dang +darkside +dav_it +dberkholz +deathwing00 +dertobi123 +desultory +dev-zero +dirtyepic +djay +djc +dmwaters +dostrow +dragonheart +drizzt +dsd +earthwings +elvanor +eva +falco +fauli +flameeyes +flammie +fmccor +ford_prefect +fordfrog +fox2mike +fuzzyray +g2boojum +gbittorrent +gengor +genstef +george +gmsoft +graaff +gregkh +griffon26 +grobian +grozin +gurligebis +halcy0n +hanno +hattya +haubi +hd_brummy +hkbst +hncaldwell +hoffie +hollow +hparker +humpback +hwoarang +i92guboj +ian +idl0r +ikelos +iluxa +jaervosz +je_fro +jer +jkt +jmbsvicetto +joker +jokey +josejx +jrinkovs +jsbronder +jurek +kallamej +kanaka +ken69267 +keri +kernelsensei +keytoaster +killerfox +kingtaco +klausman +klieber +kolmodin +lack +lavajoe +leio +livewire +loki_val +lordvan +lu_zero +lxnay +mabi +maedhros +maekke +marineam +mark_alec +markm +markusle +matsuu +mattepiu +mbres +mdisney +mduft +mescalinum +micm +miknix +mpagano +mr_bones_ +mrness +mrpouet +mueli +musikc +nathanzachary +neddyseagoon +nelchael +nerdboy +neurogeek +neysx +nightmorph +nirbheek +nixnut +nixphoeni +nyhm +omp +pacho +patrick +pauldv +pchrist +pebenito +peitolm +peper +phajdan.jr +phosphan +pilla +pjp +polvi +polynomial-c +pva +pvdabeel +py +pythonhead +quantumsummers +r0bertz +radek +rajiv +ramereth +rane +ranger +rbu +redhatter +remi +ribosome +rich0 +ricmm +rl03 +robbat2 +s4t4n +sbriesen +scarabeus +scen +serkan +shadow +shindo +sirseoman +smithj +solar +spatz +sping +spock +ssuominen +steev +stefaan +suka +swegener +tacotest +tampakrap +tanderson +tantive +tcunha +tester +tgall +tgurr +the_paya +think4urs11 +timebandit +titefleur +tomk +tommy +tove +trapni +truedfx +tsunam +tupone +ulm +vadimk +vanquirius +vapier +volkmar +vorlon +vostorga +voxus +voyageur +weaver +welp +williamh +wired +wormo +wrobel +wschlich +xmerlin +yngwin +yoswink +yuval +yvasilev +zmedico +zzam diff --git a/council200912/council2009-devs-list-unsorted b/council200912/council2009-devs-list-unsorted new file mode 100644 index 0000000..add6664 --- /dev/null +++ b/council200912/council2009-devs-list-unsorted @@ -0,0 +1,260 @@ +deathwing00 +polvi +jkt +dostrow +halcy0n +dberkholz +cam +xmerlin +cryos +mrness +carlo +griffon26 +nixphoeni +kanaka +rajiv +lordvan +robbat2 +zzam +pythonhead +fuzzyray +chiguire +wormo +vanquirius +gurligebis +dang +hparker +trapni +hollow +jaervosz +cardoe +wschlich +genstef +tgall +ribosome +lu_zero +williamh +matsuu +swegener +voxus +g2boojum +gregkh +josejx +ian +bass +vapier +chainsaw +marineam +yoswink +ranger +rl03 +corsair +battousai +anarchy +dams +flammie +tomk +george +pvdabeel +amne +tove +sbriesen +s4t4n +blackace +iluxa +jrinkovs +phosphan +pauldv +joker +dragonheart +hanno +humpback +killerfox +dsd +gmsoft +pjp +redhatter +dertobi123 +hattya +pebenito +suka +peitolm +radek +fmccor +truedfx +flameeyes +araujo +mr_bones_ +neysx +codeman +agaffney +tester +tantive +solar +livewire +nerdboy +vorlon +spock +smithj +kallamej +maedhros +grobian +rane +pilla +stefaan +steev +nelchael +markm +chtekk +markusle +nixnut +patrick +jer +tsunam +hd_brummy +earthwings +pva +wrobel +sirseoman +yvasilev +chutzpah +shadow +jokey +mark_alec +antarus +zmedico +djay +tupone +nightmorph +kernelsensei +gbittorrent +keri +calchan +ikelos +bangert +beandog +neddyseagoon +mattepiu +the_paya +kolmodin +falco +yuval +leio +tacotest +jmbsvicetto +caster +nyhm +mdisney +lack +dev-zero +je_fro +aballier +drizzt +omp +mabi +jurek +peper +dirtyepic +fauli +shindo +graaff +cedk +remi +welp +rbu +fordfrog +armin76 +hkbst +bicatali +desultory +ssuominen +aetius +voyageur +think4urs11 +ali_bush +keytoaster +scen +ulm +musikc +cla +r0bertz +lavajoe +angelos +dmwaters +eva +coldwind +py +mbres +hoffie +fox2mike +dav_it +maekke +haubi +klieber +betelgeuse +kingtaco +ramereth +tgurr +mpagano +titefleur +jsbronder +rich0 +elvanor +yngwin +ricmm +ken69267 +klausman +tommy +mduft +weaver +grozin +mueli +darkside +serkan +loki_val +pchrist +bluebird +tanderson +b33fc0d3 +ford_prefect +neurogeek +gengor +quantumsummers +timebandit +nathanzachary +hncaldwell +miknix +mescalinum +scarabeus +tcunha +i92guboj +basic +alexxy +a3li +d2_racing +tampakrap +hwoarang +micm +nirbheek +asn +idl0r +vadimk +arfrever +polynomial-c +volkmar +dagger +billie +mrpouet +vostorga +dabbott +wired +abcd +ayoy +spatz +djc +lxnay +sping +asym +craig +phajdan.jr +pacho diff --git a/council200912/election-details b/council200912/election-details new file mode 100644 index 0000000..97e0809 --- /dev/null +++ b/council200912/election-details @@ -0,0 +1,5 @@ +name: council200912 +dates: 02/01 to 15/01 +officials: jmbsvicetto, NeddySeagoon (missing 3rd official) +voters: http://www.gentoo.org/proj/en/elections/council/2009/voters-council200912.txt +ballot: http://www.gentoo.org/proj/en/elections/council/2009/ballot-council200912.txt diff --git a/council200912/get-devs-list b/council200912/get-devs-list new file mode 100644 index 0000000..e5144b2 --- /dev/null +++ b/council200912/get-devs-list @@ -0,0 +1,2 @@ +ldapsearch '(&(objectClass=gentooDev)(gentooStatus=active)(!(gentooJoin=undefined)))' -Z uid -LLL | grep "^uid" | sed -e "s/uid: //" > council2009-devs-list-unsorted +sort council2009-devs-list-unsorted > council2009-devs-list diff --git a/council200912/officials-council200912 b/council200912/officials-council200912 new file mode 100644 index 0000000..2b209d9 --- /dev/null +++ b/council200912/officials-council200912 @@ -0,0 +1,5 @@ +jmbsvicetto +neddyseagoon +fox2mike +robbat2 +antarus diff --git a/council200912/start-council200912 b/council200912/start-council200912 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/council200912/start-council200912 diff --git a/council200912/stop-council200912 b/council200912/stop-council200912 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/council200912/stop-council200912 diff --git a/council200912/voters-council200912 b/council200912/voters-council200912 new file mode 100644 index 0000000..5c41357 --- /dev/null +++ b/council200912/voters-council200912 @@ -0,0 +1,260 @@ +a3li +aballier +abcd +aetius +agaffney +alexxy +ali_bush +amne +anarchy +angelos +antarus +araujo +arfrever +armin76 +asn +asym +ayoy +b33fc0d3 +bangert +basic +bass +battousai +beandog +betelgeuse +bicatali +billie +blackace +bluebird +calchan +cam +cardoe +carlo +caster +cedk +chainsaw +chiguire +chtekk +chutzpah +cla +codeman +coldwind +corsair +craig +cryos +d2_racing +dabbott +dagger +dams +dang +darkside +dav_it +dberkholz +deathwing00 +dertobi123 +desultory +dev-zero +dirtyepic +djay +djc +dmwaters +dostrow +dragonheart +drizzt +dsd +earthwings +elvanor +eva +falco +fauli +flameeyes +flammie +fmccor +ford_prefect +fordfrog +fox2mike +fuzzyray +g2boojum +gbittorrent +gengor +genstef +george +gmsoft +graaff +gregkh +griffon26 +grobian +grozin +gurligebis +halcy0n +hanno +hattya +haubi +hd_brummy +hkbst +hncaldwell +hoffie +hollow +hparker +humpback +hwoarang +i92guboj +ian +idl0r +ikelos +iluxa +jaervosz +je_fro +jer +jkt +jmbsvicetto +joker +jokey +josejx +jrinkovs +jsbronder +jurek +kallamej +kanaka +ken69267 +keri +kernelsensei +keytoaster +killerfox +kingtaco +klausman +klieber +kolmodin +lack +lavajoe +leio +livewire +loki_val +lordvan +lu_zero +lxnay +mabi +maedhros +maekke +marineam +mark_alec +markm +markusle +matsuu +mattepiu +mbres +mdisney +mduft +mescalinum +micm +miknix +mpagano +mr_bones_ +mrness +mrpouet +mueli +musikc +nathanzachary +neddyseagoon +nelchael +nerdboy +neurogeek +neysx +nightmorph +nirbheek +nixnut +nixphoeni +nyhm +omp +pacho +patrick +pauldv +pchrist +pebenito +peitolm +peper +phajdan.jr +phosphan +pilla +pjp +polvi +polynomial-c +pva +pvdabeel +py +pythonhead +quantumsummers +r0bertz +radek +rajiv +ramereth +rane +ranger +rbu +redhatter +remi +ribosome +rich0 +ricmm +rl03 +robbat2 +s4t4n +sbriesen +scarabeus +scen +serkan +shadow +shindo +sirseoman +smithj +solar +spatz +sping +spock +ssuominen +steev +stefaan +suka +swegener +tacotest +tampakrap +tanderson +tantive +tcunha +tester +tgall +tgurr +the_paya +think4urs11 +timebandit +titefleur +tomk +tommy +tove +trapni +truedfx +tsunam +tupone +ulm +vadimk +vanquirius +vapier +volkmar +vorlon +vostorga +voxus +voyageur +weaver +welp +williamh +wired +wormo +wrobel +wschlich +xmerlin +yngwin +yoswink +yuval +yvasilev +zmedico +zzam diff --git a/council201006/Votify.pm b/council201006/Votify.pm new file mode 100644 index 0000000..73bc287 --- /dev/null +++ b/council201006/Votify.pm @@ -0,0 +1,683 @@ +# $Id: Votify.pm,v 1.5 2005/05/16 23:58:09 agriffis Exp $ +# +# Copyright 2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# votify.pm: common classes for votify and countify +# + +package Votify; + +use POSIX; +use List::Util; +use strict; + +our ($datadir) = '/etc/elections/current'; +(our $zero = $0) =~ s,.*/,,; + +sub import { + my ($class, $mode) = @_; + $Votify::mode = $mode; +} + +###################################################################### +# OfficialList +###################################################################### + +package OfficialList; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + officials => [], + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/officials-$election") + or die("failed to open officials file"); + chomp(@{$self->{'officials'}} = <F>); + close(F); + + bless $self, $class; + return $self; +} + +sub officials { + my ($self) = @_; + @{$self->{'officials'}}; +} + +###################################################################### +# VoterList +###################################################################### + +package VoterList; + +sub new { + my ($class, $election) = @_; + my (@voterlist, $r); + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/confs-$election", + filename => '', + voters => {}, # confnum => voter + confs => {}, # voter => confnum + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/voters-$election") + or die("failed to open voters file"); + chomp(@voterlist = <F>); + close(F); + + # assign confirmation numbers randomly + for my $v (@voterlist) { + do { $r = int rand 0xffff } while exists $self->{'voters'}{$r}; + $self->{'voters'}{$r} = $v; + $self->{'confs'}{$v} = $r; + } + + unless (keys %{$self->{'voters'}} == keys %{$self->{'confs'}}) { + die("discrepancy deteced in VoterList"); + } + + bless $self, $class; + return $self; +} + +sub confs { + my ($self) = @_; + sort keys %{$self->{'voters'}}; +} + +sub voters { + my ($self) = @_; + sort keys %{$self->{'confs'}}; +} + +sub getvoter { + my ($self, $conf) = @_; + return $self->{'voters'}{$conf}; +} + +sub getconf { + my ($self, $voter) = @_; + return $self->{'confs'}{$voter}; +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c ($self->confs) { + printf F "%04x %s\n", $c, $self->getvoter($c); + } + close F; +} + +###################################################################### +# MasterBallot +###################################################################### + +package MasterBallot; + +use Data::Dumper; + +sub new { + my ($class, $election, $vl) = @_; + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/master-$election", + filename => '', + voterlist => $vl, + ballots => {}, # indexed by conf num + candidates => undef, # indexed by long name + table => undef, # indexed by row+column + }; + + bless $self, $class; + return $self; +} + +sub collect { + my ($self, @voters) = @_; + my ($c, $v, $home, @pwentry); + + for my $v (@voters) { + unless (defined ($c = $self->{'voterlist'}->getconf($v))) { + die "$v does not correspond to any confirmation number"; + } + + @pwentry = getpwnam($v); + unless (@pwentry) { + print STDERR "Warning: unknown user: $v\n"; + next; + } + + $home = $pwentry[7]; + unless (-d $home) { + print STDERR "Warning: no directory: $home\n"; + next; + } + + if (-f "$home/.ballot-$self->{election}-submitted") { + my ($b) = Ballot->new($self->{'election'}); + $b->read("$home/.ballot-$self->{election}-submitted"); + if ($b->verify) { + print STDERR "Errors found in ballot: $v\n"; + next; + } + $self->{'ballots'}{$c} = $b; + } + elsif (-f "$home/.ballot-$self->{election}") { + print STDERR "Warning: $v did not submit their ballot\n"; + } + } +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c (sort keys %{$self->{'ballots'}}) { + printf F "--------- confirmation %04x ---------\n", $c; + print F $self->{'ballots'}{$c}->to_s + } + close F; +} + +sub read { + my ($self, $filename) = @_; + my ($election, $entries) = $self->{'election'}; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + open(F, "<$filename") or die("can't read $filename"); + { local $/ = undef; $entries = <F>; } + for my $e (split /^--------- confirmation /m, $entries) { + next unless $e; # skip the first zero-length record + unless ($e =~ /^([[:xdigit:]]{4}) ---------\n(.*)$/s) { + die "error parsing entry:\n$e"; + } + my ($c, $s, $b) = ($1, $2, Ballot->new($election)); + $b->from_s($s); + $self->{'ballots'}{hex($c)} = $b; + } +} + +sub generate_candidates { + my ($self) = @_; + my ($B, @C, $s); + + # nb: would need to scan all the ballots to support write-ins + $B = Ballot->new($self->{'election'}); + $B->populate; + @C = sort map $_->[0], @{$B->choices}; + for my $c (@C) { + $s = $c; # in case $c is shorter than 5 chars + for (my $i=5; $i<=length($c); $i++) { + $s = substr $c, 0, $i; + print join(" ", grep(/^$s/, @C)), "\n"; + last unless grep(/^$s/, @C) > 1; + } + $self->{'candidates'}{$c} = $s; + } +} + +sub tabulate { + my ($self) = @_; + my (@candidates); # full candidate list + my (%table); # resulting table, row.colum where row defeats column + $self->{'table'} = \%table; + + $self->generate_candidates unless $self->{'candidates'}; + @candidates = keys %{$self->{'candidates'}}; + for my $c1 (@candidates) { + for my $c2 (@candidates) { + $table{"$c1+$c2"} = 0; + } + $table{"$c1+$c1"} = '***'; + } + + # generate the table first; + # walk through the ballots, tallying the rankings expressed by each ballot + for my $b (values %{$self->{'ballots'}}) { + my (@choices, %ranks); + + #print "looking at ballot:\n", $b->to_s, "\n"; + + # first determine the ranking of each candidate. default ranking is + # scalar @candidates. + @choices = @{$b->choices}; + @ranks{@candidates} = (scalar @candidates) x @candidates; + #print "ranks before determining:\n", Dumper(\%ranks); + for (my $i = 0; $i < @choices; $i++) { + @ranks{@{$choices[$i]}} = ($i) x @{$choices[$i]}; + } + #print "ranks after determining:\n", Dumper(\%ranks); + + # second add the results of all the pairwise races into our table + for my $c1 (@candidates) { + for my $c2 (@candidates) { + next if $c1 eq $c2; + $table{"$c1+$c2"}++ if $ranks{$c1} < $ranks{$c2}; + } + } + #print "table after adding:\n"; + #$self->display_table; + } +} + +sub display_table { + my ($self) = @_; + my (@longnames, @shortnames); + my ($minlen, $maxlen, $formatstr) = (0, 4, ''); + + $self->generate_candidates unless $self->{'candidates'}; + @longnames = sort keys %{$self->{'candidates'}}; + @shortnames = sort values %{$self->{'candidates'}}; + $minlen = length scalar keys %{$self->{'ballots'}}; + $minlen = 5 if $minlen < 5; + + # build the format string + for my $s (@shortnames) { + if (length($s) > $minlen) { + $formatstr .= " %" . length($s) . "s"; + } else { + $formatstr .= " %${minlen}s"; + } + } + map { $maxlen = length($_) if length($_) > $maxlen } @longnames; + + # prepend the row header; append newline + $formatstr = "%${maxlen}s" . $formatstr . "\n"; + + # column headers + printf $formatstr, '', @shortnames; + + # rows + for my $l (@longnames) { + printf $formatstr, $l, @{$self->{'table'}}{map "$l+$_", @longnames}; + } +} + +# utility for cssd +sub defeats { + my ($self, $o1, $o2) = @_; + return 0 if $o1 eq $o2; + $self->{'table'}{"$o1+$o2"} > $self->{'table'}{"$o2+$o1"}; +} + +# utility for cssd +sub is_weaker_defeat { + my ($self, $A, $X, $B, $Y) = @_; + die unless $self->defeats($A, $X); + die unless $self->defeats($B, $Y); + return ( + $self->{'table'}{"$A+$X"} < $self->{'table'}{"$B+$Y"} or + ( + $self->{'table'}{"$A+$X"} == $self->{'table'}{"$B+$Y"} and + $self->{'table'}{"$X+$A"} > $self->{'table'}{"$Y+$B"} + ) + ); +} + +sub cssd { + my ($self) = @_; + my (@candidates); + + @candidates = sort keys %{$self->{'candidates'}}; + + while (1) { + my (%transitive_defeats); + my (@active, @plist); + + ###################################################################### + # 5. From the list of [undropped] pairwise defeats, we generate a + # set of transitive defeats. + # 1. An option A transitively defeats an option C if A + # defeats C or if there is some other option B where A + # defeats B AND B transitively defeats C. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + $transitive_defeats{"$o1+$o2"} = 1 if $self->defeats($o1, $o2); + } + } + for my $i (@candidates) { + for my $j (@candidates) { + for my $k (@candidates) { + if (exists $transitive_defeats{"$j+$i"} and + exists $transitive_defeats{"$i+$k"}) + { + $transitive_defeats{"$j+$k"} = 1; + } + } + } + } + + ###################################################################### + # 6. We construct the Schwartz set from the set of transitive + # defeats. + # 1. An option A is in the Schwartz set if for all options B, + # either A transitively defeats B, or B does not + # transitively defeat A. + print "\n"; + A: for my $A (@candidates) { + for my $B (@candidates) { + next if $transitive_defeats{"$A+$B"} or not $transitive_defeats{"$B+$A"}; + # countify marks entries +++ instead of *** when they've already + # been ranked. + if ($self->{'table'}{"$A+$A"} eq '***') { + print "option $A is eliminated ($B trans-defeats $A, and $A does not trans-defeat $B)\n"; + } + next A; + } + push @active, $A; + } + print "the Schwartz set is {", join(", ", @active), "}\n"; + @candidates = @active; + + ###################################################################### + # 7. If there are defeats between options in the Schwartz set, we + # drop the weakest such defeats from the list of pairwise + # defeats, and return to step 5. + # 1. A defeat (A,X) is weaker than a defeat (B,Y) if V(A,X) + # is less than V(B,Y). Also, (A,X) is weaker than (B,Y) if + # V(A,X) is equal to V(B,Y) and V(X,A) is greater than V + # (Y,B). + # 2. A weakest defeat is a defeat that has no other defeat + # weaker than it. There may be more than one such defeat. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + push @plist, [ $o1, $o2 ] if $self->defeats($o1, $o2); + } + } + last unless @plist; + @plist = sort { + return -1 if $self->is_weaker_defeat(@$a, @$b); + return +1 if $self->is_weaker_defeat(@$b, @$a); + return 0; + } @plist; + for my $dx (@plist) { + my ($o1, $o2) = @$dx; + print("$o1+$o2 ", + $self->{'table'}{"$o1+$o2"}, " $o2+$o1 ", + $self->{'table'}{"$o2+$o1"}, "\n"); + } + my ($o1, $o2) = @{$plist[0]}; + $self->{'table'}{"$o1+$o2"} = 0; + $self->{'table'}{"$o2+$o1"} = 0; + } + + ###################################################################### + # 8. If there are no defeats within the Schwartz set, then the + # winner is chosen from the options in the Schwartz set. If + # there is only one such option, it is the winner. If there + # are multiple options, the elector with the casting vote + # chooses which of those options wins. + print "\n"; + if (@candidates > 1) { + print "result: tie between options ", join(", ", @candidates), "\n"; + } else { + print "result: option @candidates wins\n"; + } + + return @candidates; +} + +###################################################################### +# Ballot +###################################################################### + +package Ballot; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + filename => '', + default_filename => $ENV{'HOME'}."/.ballot-$election", + choices => [], + }; + + # Bless me, I'm a ballot! + bless $self, $class; + return $self; +} + +sub from_s { + my ($self, $s) = @_; + my (@choices); + + for (split "\n", $s) { + s/#.*//; + next unless /\S/; + push @choices, [ split(' ', $_) ]; + } + die("No data in string") unless @choices; + + $self->{'choices'} = \@choices; +} + +sub read { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Load the data file + open(F, "<$filename") or die("couldn't open $filename"); + { local $/ = undef; $self->from_s(<F>); } + close(F); +} + +sub populate { + my ($self) = @_; + $self->read("$Votify::datadir/ballot-$self->{election}"); + @{$self->{'choices'}} = List::Util::shuffle(@{$self->{'choices'}}); +} + +sub choices { + my ($self) = @_; + $self->{'choices'}; +} + +sub write { + my ($self, $filename) = @_; + + if ($Votify::mode ne 'user') { + die("we don't write ballots in official mode"); + } + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Don't ever overwrite a ballot + die("File already exists; please remove $filename\n") if -e $filename; + + # Write the user's ballot + open(F, ">$filename") or die "Failed writing $filename"; + print F <<EOT; +# This is a ballot for the $self->{election} election. +# Please rank your choices in order; first choice at the top and last choice at +# the bottom. You can put choices on the same line to indicate no preference +# between them. Any choices you omit from this file are implicitly added at the +# end. +# +# When you're finished editing this, the next step is to verify your ballot +# with: +# +# $Votify::zero --verify $self->{election} +# +# When that passes and you're satisfied, the final step is to submit your vote: +# +# $Votify::zero --submit $self->{election} +# + +EOT + for (@{$self->{'choices'}}) { print F "@$_\n"; } + close(F); +} + +sub verify { + my ($self) = @_; + my (%h, $master, %mh); + my (@dups, @missing, @extra); + my ($errors_found); + + # Load %h from the user's ballot + for my $line (@{$self->{'choices'}}) { + for my $entry (@$line) { + $h{$entry}++; + } + } + + # Load the master ballot into another hash and compare them. + # The master ballots always do one entry per line, making this a little + # easier. + $master = Ballot->new($self->{'election'}); + $master->populate; + %mh = map(($_->[0] => 1), @{$master->{'choices'}}); + + # Check for extra entries (write-ins should be supported in the future) + for (keys %h) { + push @extra, $_ unless exists $mh{$_}; + } + + # Check for duplicate entries + @dups = grep { $h{$_} > 1 } keys %h; + + # Check for missing entries (not necessarily an error) + for (keys %mh) { + push @missing, $_ unless exists $h{$_}; + } + + # Report errors and warnings + if (@extra) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some extra entries that are not part of this election. Sorry, +but write-ins are not (yet) supported. Please remove these from your ballot: + +EOT + print map "\t$_\n", @extra; + print "\n"; + } + $errors_found++; + } + if (@dups) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some duplicate entries. Please resolve these to a single entry +to avoid ambiguities: + +EOT + print map "\t$_\n", @dups; + print "\n"; + } + $errors_found++; + } + if (@{$self->{'choices'}} == 0) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot doesn't contain any entries. You can start over by first removing +the existing ballot, then using --new to generate a new ballot. See --help for +more information. + +EOT + } + $errors_found++; + } + elsif (@missing and $Votify::mode eq 'user') { + print <<EOT; +Your ballot is missing some entries. This is not an error, but note that these +will be implied as a final line, with no preference between them, like this: + +EOT + print "\t", join(" ", @missing), "\n"; + print "\n"; + } + if ($Votify::mode eq 'user' and !$errors_found and + @{$self->{'choices'}} == 1 and + scalar(keys %h) == scalar(keys %mh)) + { + print <<EOT; +Your ballot contains all the candidates on a single line! This means you have +no preference between the candidates. This is not an error, but note that this +is a meaningless ballot that will have no effect on the election. + +EOT + } + + # Stop if there were errors + if ($Votify::mode eq 'user' and $errors_found) { + print("There were errors found in your ballot.\n"); + die("Please correct them and try again.\n\n"); + } + return $errors_found; +} + +sub to_s { + my ($self) = @_; + join '', map "@$_\n", @{$self->{'choices'}}; +} + +1; + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + +Revision 1.3 2005/05/09 23:12:02 agriffis +Add support for registered voters + +Revision 1.2 2005/05/05 23:03:46 agriffis +Fix indentation (and some output as well) + +Revision 1.1 2005/05/05 22:05:34 agriffis +first pass at Gentoo Foundation voting program + +# vim:sw=4 et diff --git a/council201006/ballot-council201006 b/council201006/ballot-council201006 new file mode 100644 index 0000000..e8d662b --- /dev/null +++ b/council201006/ballot-council201006 @@ -0,0 +1 @@ +_reopen_nominations diff --git a/council201006/council201006-devs-list b/council201006/council201006-devs-list new file mode 100644 index 0000000..532d454 --- /dev/null +++ b/council201006/council201006-devs-list @@ -0,0 +1,270 @@ +a3li +aballier +abcd +aetius +agaffney +alexxy +ali_bush +amne +anarchy +angelos +antarus +araujo +arfrever +armin76 +asn +asym +ayoy +b33fc0d3 +bangert +basic +bass +battousai +beandog +betelgeuse +bicatali +billie +blackace +bluebird +calchan +caleb +cam +cardoe +carlo +caster +cedk +chainsaw +chiguire +chiiph +chithanh +chtekk +chutzpah +cla +codeman +coldwind +corsair +craig +cryos +d2_racing +dabbott +dagger +dams +dang +darkside +dav_it +dberkholz +deathwing00 +dertobi123 +desultory +dev-zero +dirtyepic +djay +djc +dmwaters +dostrow +dragonheart +drizzt +dsd +earthwings +elvanor +eva +falco +fauli +ferringb +flameeyes +flammie +fmccor +ford_prefect +fordfrog +fox2mike +fuzzyray +g2boojum +gengor +genstef +george +gmsoft +graaff +gregkh +griffon26 +grobian +grozin +gurligebis +halcy0n +hanno +hattya +haubi +hd_brummy +hkbst +hncaldwell +hoffie +hollow +hparker +humpback +hwoarang +i92guboj +ian +idl0r +ikelos +iluxa +jaervosz +je_fro +jer +jkt +jlec +jmbsvicetto +joker +jokey +josejx +jrinkovs +jsbronder +jurek +kallamej +kanaka +ken69267 +keri +kernelsensei +keytoaster +killerfox +kingtaco +klausman +klieber +kolmodin +kumba +lack +lavajoe +leio +livewire +loki_val +lordvan +lu_zero +lxnay +mabi +maedhros +maekke +marineam +mark_alec +markm +markusle +matsuu +mattepiu +mbres +mdisney +mduft +mescalinum +micm +miknix +mpagano +mr_bones_ +mrness +mrpouet +mueli +musikc +nathanzachary +neddyseagoon +nelchael +nerdboy +neurogeek +neysx +nightmorph +nimiux +nirbheek +nixnut +nixphoeni +nyhm +omp +pacho +patrick +pauldv +pchrist +pebenito +peitolm +peper +phajdan.jr +phosphan +pilla +pjp +polvi +polynomial-c +pva +pvdabeel +py +pythonhead +quantumsummers +r0bertz +radek +rajiv +ramereth +rane +ranger +rbu +reavertm +redhatter +remi +ribosome +rich0 +ricmm +rl03 +robbat2 +s4t4n +sbriesen +scarabeus +scen +serkan +shadow +shindo +sirseoman +smithj +sochotnicky +solar +spatz +sping +spock +ssuominen +steev +stefaan +suka +swegener +tampakrap +tanderson +tantive +tcunha +tester +tgall +tgurr +the_paya +think4urs11 +timebandit +titefleur +tomjbe +tomk +tommy +tove +trapni +truedfx +tsunam +tupone +ulm +vadimk +vanquirius +vapier +volkmar +vorlon +vostorga +voxus +voyageur +weaver +welp +williamh +wired +wormo +wrobel +wschlich +xarthisius +xmerlin +yngwin +yoswink +yuval +yvasilev +zmedico +zorry +zzam diff --git a/council201006/election-details b/council201006/election-details new file mode 100644 index 0000000..5ce723c --- /dev/null +++ b/council201006/election-details @@ -0,0 +1,6 @@ +name: council201006 +startDate: 2010-06-20 00:00:00 UTC +endDate: 2010-07-03 23:59:00 UTC +officials: neddyseagoon, ulm, tove, robbat2 +voters: http://www.gentoo.org/proj/en/elections/council/2010/voters-council201006.txt +ballot: http://www.gentoo.org/proj/en/elections/council/2010/ballot-council201006.txt diff --git a/council201006/get-devs-list b/council201006/get-devs-list new file mode 100644 index 0000000..ce27f5c --- /dev/null +++ b/council201006/get-devs-list @@ -0,0 +1,2 @@ +ENAME=$(awk '/^name:/{print $2}' election-details) +ldapsearch '(&(objectClass=gentooDev)(gentooStatus=active)(!(gentooAccess=infra-system.group)))' -Z uid -LLL -S uid | awk '/^uid:/ {print $2}' >${ENAME}-devs-list diff --git a/council201006/officials-council201006 b/council201006/officials-council201006 new file mode 100644 index 0000000..c7f7376 --- /dev/null +++ b/council201006/officials-council201006 @@ -0,0 +1,4 @@ +neddyseagoon +ulm +tove +robbat2 diff --git a/council201006/start-council201006 b/council201006/start-council201006 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/council201006/start-council201006 diff --git a/council201006/stop-council201006 b/council201006/stop-council201006 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/council201006/stop-council201006 diff --git a/council201006/voters-council201006 b/council201006/voters-council201006 new file mode 100644 index 0000000..ab66a82 --- /dev/null +++ b/council201006/voters-council201006 @@ -0,0 +1,270 @@ +a3li +aballier +abcd +aetius +agaffney +alexxy +ali_bush +amne +anarchy +angelos +antarus +araujo +arfrever +armin76 +asn +asym +ayoy +b33fc0d3 +bangert +basic +bass +battousai +beandog +betelgeuse +bicatali +billie +blackace +bluebird +calchan +caleb +cam +cardoe +carlo +caster +cedk +chainsaw +chiguire +chiiph +chithanh +chtekk +chutzpah +cla +codeman +coldwind +corsair +craig +cryos +d2_racing +dabbott +dagger +dams +dang +darkside +dav_it +dberkholz +deathwing00 +dertobi123 +desultory +dev-zero +dirtyepic +djay +djc +dmwaters +dostrow +dragonheart +drizzt +dsd +earthwings +elvanor +eva +falco +fauli +ferringb +flameeyes +flammie +fmccor +fordfrog +ford_prefect +fox2mike +fuzzyray +g2boojum +gengor +genstef +george +gmsoft +graaff +gregkh +griffon26 +grobian +grozin +gurligebis +halcy0n +hanno +hattya +haubi +hd_brummy +hkbst +hncaldwell +hoffie +hollow +hparker +humpback +hwoarang +i92guboj +ian +idl0r +ikelos +iluxa +jaervosz +je_fro +jer +jkt +jlec +jmbsvicetto +joker +jokey +josejx +jrinkovs +jsbronder +jurek +kallamej +kanaka +ken69267 +keri +kernelsensei +keytoaster +killerfox +kingtaco +klausman +klieber +kolmodin +kumba +lack +lavajoe +leio +livewire +loki_val +lordvan +lu_zero +lxnay +mabi +maedhros +maekke +marineam +mark_alec +markm +markusle +matsuu +mattepiu +mbres +mdisney +mduft +mescalinum +micm +miknix +mpagano +mr_bones_ +mrness +mrpouet +mueli +musikc +nathanzachary +neddyseagoon +nelchael +nerdboy +neurogeek +neysx +nightmorph +nimiux +nirbheek +nixnut +nixphoeni +nyhm +omp +pacho +patrick +pauldv +pchrist +pebenito +peitolm +peper +phajdan.jr +phosphan +pilla +pjp +polvi +polynomial-c +pva +pvdabeel +py +pythonhead +quantumsummers +r0bertz +radek +rajiv +ramereth +rane +ranger +rbu +reavertm +redhatter +remi +ribosome +rich0 +ricmm +rl03 +robbat2 +s4t4n +sbriesen +scarabeus +scen +serkan +shadow +shindo +sirseoman +smithj +sochotnicky +solar +spatz +sping +spock +ssuominen +steev +stefaan +suka +swegener +tampakrap +tanderson +tantive +tcunha +tester +tgall +tgurr +the_paya +think4urs11 +timebandit +titefleur +tomjbe +tomk +tommy +tove +trapni +truedfx +tsunam +tupone +ulm +vadimk +vanquirius +vapier +volkmar +vorlon +vostorga +voxus +voyageur +weaver +welp +williamh +wired +wormo +wrobel +wschlich +xarthisius +xmerlin +yngwin +yoswink +yuval +yvasilev +zmedico +zorry +zzam diff --git a/countify b/countify new file mode 100755 index 0000000..e089584 --- /dev/null +++ b/countify @@ -0,0 +1,141 @@ +#!/usr/bin/perl -w +# $Id: countify,v 1.3 2005/05/16 18:10:46 agriffis Exp $ +# +# Copyright 2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# countify: collect, tabulate and announce ballot results +# + +#BEGIN { push @INC, (getpwnam 'fox2mike')[7].'/elections' } +BEGIN { push @INC, '/etc/elections/current' } + +use POSIX; +use Getopt::Long; +use List::Util; +use Votify 'official'; +use strict; + +###################################################################### +# Global vars +###################################################################### + +(my $zero = $0) =~ s,.*/,,; +(my $version = '$Revision: 1.3 $') =~ s/.*?(\d.*\d).*/$zero version $1\n/; +my %opt; +my $usage = <<EOT; + +usage: $zero <command> <election> + +where <command> is one of: + + --collect Collect the submitted ballots from home directories + --rank Show ranking based on master ballot + --help Show this help message + --version Show version information + +and <election> is one of the elections currently in-progress. The following +elections are currently open: + + trustees2005 + +EOT + +###################################################################### +# Main +###################################################################### + +package main; + +# Make sure umask is secure before we do anything +umask 077; + +# Parse the options on the cmdline. Put the short versions first in +# each optionstring so that the hash keys are created using the short +# versions. For example, use 'q|qar', not 'qar|q'. +my ($result) = GetOptions( + \%opt, + 'collect', # collect the submitted ballots from home directories + 'rank', # show the ranking based on a master ballot + 'help', # help message + 'version', # version information +); +if ($opt{'help'} or not %opt) { print STDERR $usage; exit 0 } +if ($opt{'version'}) { print STDERR $version; exit 0 } +die "$zero: only one command allowed; use --help for help\n" if 1 < keys %opt; +die "$zero: election required; use --help for help\n" unless @ARGV == 1; + +my ($election) = $ARGV[0]; +my ($vl) = VoterList->new($election); + +if ($opt{'collect'}) { + my ($ol) = OfficialList->new($election); + my ($master) = MasterBallot->new($election, $vl); + $master->collect($vl->voters); + for my $o ($ol->officials) { + my ($uid, $home) = (getpwnam $o)[2,7]; + mkdir "$home/results-$election"; + $master->write("$home/results-$election/master-$election"); + $vl->write("$home/results-$election/confs-$election"); + chown $uid, -1, "$home/results-$election", + "$home/results-$election/master-$election", + "$home/results-$election/confs-$election"; + } + exit 0; +} + +if ($opt{'rank'}) { + my ($master) = MasterBallot->new($election, $vl); + my (@candidates, @winner, @ranked, @ranks); + $master->read("$ENV{HOME}/results-$election/master-$election"); + $master->generate_candidates(); + @candidates = sort keys %{$master->{'candidates'}}; + + while (1) { + $master->tabulate(); + + # this is a hack :-( + #print "\n"; + for my $r (@ranked) { + #print "$r is already ranked\n"; + for my $c (@candidates) { + $master->{'table'}{"$r+$c"} = -1; + } + $master->{'table'}{"$r+$r"} = '+++'; + } + + # now display the table + print "\n"; + $master->display_table(); + + @winner = $master->cssd(); + if (@winner == @candidates) { + print "\n*** No additional winners to add to the list ***\n"; + last; + } + + push @ranks, [ @winner ]; + push @ranked, @winner; + if (@ranked == @candidates) { + print "\n*** Finished ranking candidates ***\n"; + last; + } + + print "\n*** Running another pass to find the next winners... ***\n"; + } + + print "\nFinal ranked list:\n"; + print map "@$_\n", @ranks; +} + +__END__ + +$Log: countify,v $ +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + + +# vim:sw=4 et @@ -0,0 +1 @@ +council201006
\ No newline at end of file diff --git a/foundation-referendum-2009-01/Votify.pm b/foundation-referendum-2009-01/Votify.pm new file mode 100644 index 0000000..0c5565d --- /dev/null +++ b/foundation-referendum-2009-01/Votify.pm @@ -0,0 +1,683 @@ +# $Id: Votify.pm,v 1.5 2005/05/16 23:58:09 agriffis Exp $ +# +# Copyright 2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# votify.pm: common classes for votify and countify +# + +package Votify; + +use POSIX; +use List::Util; +use strict; + +our ($datadir) = '/home/fox2mike/elections'; +(our $zero = $0) =~ s,.*/,,; + +sub import { + my ($class, $mode) = @_; + $Votify::mode = $mode; +} + +###################################################################### +# OfficialList +###################################################################### + +package OfficialList; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + officials => [], + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/officials-$election") + or die("failed to open officials file"); + chomp(@{$self->{'officials'}} = <F>); + close(F); + + bless $self, $class; + return $self; +} + +sub officials { + my ($self) = @_; + @{$self->{'officials'}}; +} + +###################################################################### +# VoterList +###################################################################### + +package VoterList; + +sub new { + my ($class, $election) = @_; + my (@voterlist, $r); + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/confs-$election", + filename => '', + voters => {}, # confnum => voter + confs => {}, # voter => confnum + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/voters-$election") + or die("failed to open voters file"); + chomp(@voterlist = <F>); + close(F); + + # assign confirmation numbers randomly + for my $v (@voterlist) { + do { $r = int rand 0xffff } while exists $self->{'voters'}{$r}; + $self->{'voters'}{$r} = $v; + $self->{'confs'}{$v} = $r; + } + + unless (keys %{$self->{'voters'}} == keys %{$self->{'confs'}}) { + die("discrepancy deteced in VoterList"); + } + + bless $self, $class; + return $self; +} + +sub confs { + my ($self) = @_; + sort keys %{$self->{'voters'}}; +} + +sub voters { + my ($self) = @_; + sort keys %{$self->{'confs'}}; +} + +sub getvoter { + my ($self, $conf) = @_; + return $self->{'voters'}{$conf}; +} + +sub getconf { + my ($self, $voter) = @_; + return $self->{'confs'}{$voter}; +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c ($self->confs) { + printf F "%04x %s\n", $c, $self->getvoter($c); + } + close F; +} + +###################################################################### +# MasterBallot +###################################################################### + +package MasterBallot; + +use Data::Dumper; + +sub new { + my ($class, $election, $vl) = @_; + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/master-$election", + filename => '', + voterlist => $vl, + ballots => {}, # indexed by conf num + candidates => undef, # indexed by long name + table => undef, # indexed by row+column + }; + + bless $self, $class; + return $self; +} + +sub collect { + my ($self, @voters) = @_; + my ($c, $v, $home, @pwentry); + + for my $v (@voters) { + unless (defined ($c = $self->{'voterlist'}->getconf($v))) { + die "$v does not correspond to any confirmation number"; + } + + @pwentry = getpwnam($v); + unless (@pwentry) { + print STDERR "Warning: unknown user: $v\n"; + next; + } + + $home = $pwentry[7]; + unless (-d $home) { + print STDERR "Warning: no directory: $home\n"; + next; + } + + if (-f "$home/.ballot-$self->{election}-submitted") { + my ($b) = Ballot->new($self->{'election'}); + $b->read("$home/.ballot-$self->{election}-submitted"); + if ($b->verify) { + print STDERR "Errors found in ballot: $v\n"; + next; + } + $self->{'ballots'}{$c} = $b; + } + elsif (-f "$home/.ballot-$self->{election}") { + print STDERR "Warning: $v did not submit their ballot\n"; + } + } +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c (sort keys %{$self->{'ballots'}}) { + printf F "--------- confirmation %04x ---------\n", $c; + print F $self->{'ballots'}{$c}->to_s + } + close F; +} + +sub read { + my ($self, $filename) = @_; + my ($election, $entries) = $self->{'election'}; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + open(F, "<$filename") or die("can't read $filename"); + { local $/ = undef; $entries = <F>; } + for my $e (split /^--------- confirmation /m, $entries) { + next unless $e; # skip the first zero-length record + unless ($e =~ /^([[:xdigit:]]{4}) ---------\n(.*)$/s) { + die "error parsing entry:\n$e"; + } + my ($c, $s, $b) = ($1, $2, Ballot->new($election)); + $b->from_s($s); + $self->{'ballots'}{hex($c)} = $b; + } +} + +sub generate_candidates { + my ($self) = @_; + my ($B, @C, $s); + + # nb: would need to scan all the ballots to support write-ins + $B = Ballot->new($self->{'election'}); + $B->populate; + @C = sort map $_->[0], @{$B->choices}; + for my $c (@C) { + $s = $c; # in case $c is shorter than 5 chars + for (my $i=5; $i<=length($c); $i++) { + $s = substr $c, 0, $i; + print join(" ", grep(/^$s/, @C)), "\n"; + last unless grep(/^$s/, @C) > 1; + } + $self->{'candidates'}{$c} = $s; + } +} + +sub tabulate { + my ($self) = @_; + my (@candidates); # full candidate list + my (%table); # resulting table, row.colum where row defeats column + $self->{'table'} = \%table; + + $self->generate_candidates unless $self->{'candidates'}; + @candidates = keys %{$self->{'candidates'}}; + for my $c1 (@candidates) { + for my $c2 (@candidates) { + $table{"$c1+$c2"} = 0; + } + $table{"$c1+$c1"} = '***'; + } + + # generate the table first; + # walk through the ballots, tallying the rankings expressed by each ballot + for my $b (values %{$self->{'ballots'}}) { + my (@choices, %ranks); + + #print "looking at ballot:\n", $b->to_s, "\n"; + + # first determine the ranking of each candidate. default ranking is + # scalar @candidates. + @choices = @{$b->choices}; + @ranks{@candidates} = (scalar @candidates) x @candidates; + #print "ranks before determining:\n", Dumper(\%ranks); + for (my $i = 0; $i < @choices; $i++) { + @ranks{@{$choices[$i]}} = ($i) x @{$choices[$i]}; + } + #print "ranks after determining:\n", Dumper(\%ranks); + + # second add the results of all the pairwise races into our table + for my $c1 (@candidates) { + for my $c2 (@candidates) { + next if $c1 eq $c2; + $table{"$c1+$c2"}++ if $ranks{$c1} < $ranks{$c2}; + } + } + #print "table after adding:\n"; + #$self->display_table; + } +} + +sub display_table { + my ($self) = @_; + my (@longnames, @shortnames); + my ($minlen, $maxlen, $formatstr) = (0, 4, ''); + + $self->generate_candidates unless $self->{'candidates'}; + @longnames = sort keys %{$self->{'candidates'}}; + @shortnames = sort values %{$self->{'candidates'}}; + $minlen = length scalar keys %{$self->{'ballots'}}; + $minlen = 5 if $minlen < 5; + + # build the format string + for my $s (@shortnames) { + if (length($s) > $minlen) { + $formatstr .= " %" . length($s) . "s"; + } else { + $formatstr .= " %${minlen}s"; + } + } + map { $maxlen = length($_) if length($_) > $maxlen } @longnames; + + # prepend the row header; append newline + $formatstr = "%${maxlen}s" . $formatstr . "\n"; + + # column headers + printf $formatstr, '', @shortnames; + + # rows + for my $l (@longnames) { + printf $formatstr, $l, @{$self->{'table'}}{map "$l+$_", @longnames}; + } +} + +# utility for cssd +sub defeats { + my ($self, $o1, $o2) = @_; + return 0 if $o1 eq $o2; + $self->{'table'}{"$o1+$o2"} > $self->{'table'}{"$o2+$o1"}; +} + +# utility for cssd +sub is_weaker_defeat { + my ($self, $A, $X, $B, $Y) = @_; + die unless $self->defeats($A, $X); + die unless $self->defeats($B, $Y); + return ( + $self->{'table'}{"$A+$X"} < $self->{'table'}{"$B+$Y"} or + ( + $self->{'table'}{"$A+$X"} == $self->{'table'}{"$B+$Y"} and + $self->{'table'}{"$X+$A"} > $self->{'table'}{"$Y+$B"} + ) + ); +} + +sub cssd { + my ($self) = @_; + my (@candidates); + + @candidates = sort keys %{$self->{'candidates'}}; + + while (1) { + my (%transitive_defeats); + my (@active, @plist); + + ###################################################################### + # 5. From the list of [undropped] pairwise defeats, we generate a + # set of transitive defeats. + # 1. An option A transitively defeats an option C if A + # defeats C or if there is some other option B where A + # defeats B AND B transitively defeats C. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + $transitive_defeats{"$o1+$o2"} = 1 if $self->defeats($o1, $o2); + } + } + for my $i (@candidates) { + for my $j (@candidates) { + for my $k (@candidates) { + if (exists $transitive_defeats{"$j+$i"} and + exists $transitive_defeats{"$i+$k"}) + { + $transitive_defeats{"$j+$k"} = 1; + } + } + } + } + + ###################################################################### + # 6. We construct the Schwartz set from the set of transitive + # defeats. + # 1. An option A is in the Schwartz set if for all options B, + # either A transitively defeats B, or B does not + # transitively defeat A. + print "\n"; + A: for my $A (@candidates) { + for my $B (@candidates) { + next if $transitive_defeats{"$A+$B"} or not $transitive_defeats{"$B+$A"}; + # countify marks entries +++ instead of *** when they've already + # been ranked. + if ($self->{'table'}{"$A+$A"} eq '***') { + print "option $A is eliminated ($B trans-defeats $A, and $A does not trans-defeat $B)\n"; + } + next A; + } + push @active, $A; + } + print "the Schwartz set is {", join(", ", @active), "}\n"; + @candidates = @active; + + ###################################################################### + # 7. If there are defeats between options in the Schwartz set, we + # drop the weakest such defeats from the list of pairwise + # defeats, and return to step 5. + # 1. A defeat (A,X) is weaker than a defeat (B,Y) if V(A,X) + # is less than V(B,Y). Also, (A,X) is weaker than (B,Y) if + # V(A,X) is equal to V(B,Y) and V(X,A) is greater than V + # (Y,B). + # 2. A weakest defeat is a defeat that has no other defeat + # weaker than it. There may be more than one such defeat. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + push @plist, [ $o1, $o2 ] if $self->defeats($o1, $o2); + } + } + last unless @plist; + @plist = sort { + return -1 if $self->is_weaker_defeat(@$a, @$b); + return +1 if $self->is_weaker_defeat(@$b, @$a); + return 0; + } @plist; + for my $dx (@plist) { + my ($o1, $o2) = @$dx; + print("$o1+$o2 ", + $self->{'table'}{"$o1+$o2"}, " $o2+$o1 ", + $self->{'table'}{"$o2+$o1"}, "\n"); + } + my ($o1, $o2) = @{$plist[0]}; + $self->{'table'}{"$o1+$o2"} = 0; + $self->{'table'}{"$o2+$o1"} = 0; + } + + ###################################################################### + # 8. If there are no defeats within the Schwartz set, then the + # winner is chosen from the options in the Schwartz set. If + # there is only one such option, it is the winner. If there + # are multiple options, the elector with the casting vote + # chooses which of those options wins. + print "\n"; + if (@candidates > 1) { + print "result: tie between options ", join(", ", @candidates), "\n"; + } else { + print "result: option @candidates wins\n"; + } + + return @candidates; +} + +###################################################################### +# Ballot +###################################################################### + +package Ballot; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + filename => '', + default_filename => $ENV{'HOME'}."/.ballot-$election", + choices => [], + }; + + # Bless me, I'm a ballot! + bless $self, $class; + return $self; +} + +sub from_s { + my ($self, $s) = @_; + my (@choices); + + for (split "\n", $s) { + s/#.*//; + next unless /\S/; + push @choices, [ split(' ', $_) ]; + } + die("No data in string") unless @choices; + + $self->{'choices'} = \@choices; +} + +sub read { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Load the data file + open(F, "<$filename") or die("couldn't open $filename"); + { local $/ = undef; $self->from_s(<F>); } + close(F); +} + +sub populate { + my ($self) = @_; + $self->read("$Votify::datadir/ballot-$self->{election}"); + @{$self->{'choices'}} = List::Util::shuffle(@{$self->{'choices'}}); +} + +sub choices { + my ($self) = @_; + $self->{'choices'}; +} + +sub write { + my ($self, $filename) = @_; + + if ($Votify::mode ne 'user') { + die("we don't write ballots in official mode"); + } + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Don't ever overwrite a ballot + die("File already exists; please remove $filename\n") if -e $filename; + + # Write the user's ballot + open(F, ">$filename") or die "Failed writing $filename"; + print F <<EOT; +# This is a ballot for the $self->{election} election. +# Please rank your choices in order; first choice at the top and last choice at +# the bottom. You can put choices on the same line to indicate no preference +# between them. Any choices you omit from this file are implicitly added at the +# end. +# +# When you're finished editing this, the next step is to verify your ballot +# with: +# +# $Votify::zero --verify $self->{election} +# +# When that passes and you're satisfied, the final step is to submit your vote: +# +# $Votify::zero --submit $self->{election} +# + +EOT + for (@{$self->{'choices'}}) { print F "@$_\n"; } + close(F); +} + +sub verify { + my ($self) = @_; + my (%h, $master, %mh); + my (@dups, @missing, @extra); + my ($errors_found); + + # Load %h from the user's ballot + for my $line (@{$self->{'choices'}}) { + for my $entry (@$line) { + $h{$entry}++; + } + } + + # Load the master ballot into another hash and compare them. + # The master ballots always do one entry per line, making this a little + # easier. + $master = Ballot->new($self->{'election'}); + $master->populate; + %mh = map(($_->[0] => 1), @{$master->{'choices'}}); + + # Check for extra entries (write-ins should be supported in the future) + for (keys %h) { + push @extra, $_ unless exists $mh{$_}; + } + + # Check for duplicate entries + @dups = grep { $h{$_} > 1 } keys %h; + + # Check for missing entries (not necessarily an error) + for (keys %mh) { + push @missing, $_ unless exists $h{$_}; + } + + # Report errors and warnings + if (@extra) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some extra entries that are not part of this election. Sorry, +but write-ins are not (yet) supported. Please remove these from your ballot: + +EOT + print map "\t$_\n", @extra; + print "\n"; + } + $errors_found++; + } + if (@dups) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some duplicate entries. Please resolve these to a single entry +to avoid ambiguities: + +EOT + print map "\t$_\n", @dups; + print "\n"; + } + $errors_found++; + } + if (@{$self->{'choices'}} == 0) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot doesn't contain any entries. You can start over by first removing +the existing ballot, then using --new to generate a new ballot. See --help for +more information. + +EOT + } + $errors_found++; + } + elsif (@missing and $Votify::mode eq 'user') { + print <<EOT; +Your ballot is missing some entries. This is not an error, but note that these +will be implied as a final line, with no preference between them, like this: + +EOT + print "\t", join(" ", @missing), "\n"; + print "\n"; + } + if ($Votify::mode eq 'user' and !$errors_found and + @{$self->{'choices'}} == 1 and + scalar(keys %h) == scalar(keys %mh)) + { + print <<EOT; +Your ballot contains all the candidates on a single line! This means you have +no preference between the candidates. This is not an error, but note that this +is a meaningless ballot that will have no effect on the election. + +EOT + } + + # Stop if there were errors + if ($Votify::mode eq 'user' and $errors_found) { + print("There were errors found in your ballot.\n"); + die("Please correct them and try again.\n\n"); + } + return $errors_found; +} + +sub to_s { + my ($self) = @_; + join '', map "@$_\n", @{$self->{'choices'}}; +} + +1; + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + +Revision 1.3 2005/05/09 23:12:02 agriffis +Add support for registered voters + +Revision 1.2 2005/05/05 23:03:46 agriffis +Fix indentation (and some output as well) + +Revision 1.1 2005/05/05 22:05:34 agriffis +first pass at Gentoo Foundation voting program + +# vim:sw=4 et diff --git a/foundation-referendum-2009-01/ballot-foundation-referendum-2009-01 b/foundation-referendum-2009-01/ballot-foundation-referendum-2009-01 new file mode 100644 index 0000000..3ce46e2 --- /dev/null +++ b/foundation-referendum-2009-01/ballot-foundation-referendum-2009-01 @@ -0,0 +1,2 @@ +no +yes diff --git a/foundation-referendum-2009-01/officials-foundation-referendum-2009-01 b/foundation-referendum-2009-01/officials-foundation-referendum-2009-01 new file mode 100644 index 0000000..46a4cec --- /dev/null +++ b/foundation-referendum-2009-01/officials-foundation-referendum-2009-01 @@ -0,0 +1,4 @@ +patrick +jmbsvicetto +antarus +root diff --git a/foundation-referendum-2009-01/start-foundation-referendum-2009-01 b/foundation-referendum-2009-01/start-foundation-referendum-2009-01 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/foundation-referendum-2009-01/start-foundation-referendum-2009-01 diff --git a/foundation-referendum-2009-01/stop-foundation-referendum-2009-01 b/foundation-referendum-2009-01/stop-foundation-referendum-2009-01 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/foundation-referendum-2009-01/stop-foundation-referendum-2009-01 diff --git a/foundation-referendum-2009-01/voters-foundation-referendum-2009-01 b/foundation-referendum-2009-01/voters-foundation-referendum-2009-01 new file mode 100644 index 0000000..5b0dbc1 --- /dev/null +++ b/foundation-referendum-2009-01/voters-foundation-referendum-2009-01 @@ -0,0 +1,211 @@ +a3li +aballier +abcd +agaffney +agriffis +ali_bush +amne +anant +anpereir +antarus +araujo +armin76 +astinus +axxo +azarah +bangert +bass +batlogg +bcowan +beandog +beejay +bennyc +betelgeuse +bicatali +blackace +blubber +bluebird +calchan +cam +cardoe +carlo +carpaski +caster +cedk +chainsaw +chiguire +ciaranm +clefebvre.62 +coldwind +compnerd +corsair +cryos +cshields +cybersystem +dabbott +dang +darkside +dberkholz +deathwing00 +dertobi123 +desultory +dev-zero +dholm +dirtyepic +djay +dju +dmwaters +dostrow +dragonheart +drizzt +drobbins +dsd +edit21 +eradicator +fauli +ferdy +ferringb +flameeyes +flammie +ford_prefect +fox2mike +fuzzyray +g2boojum +genone +genstef +geoman +george +gerrynjr +graaff +grahl +gregkh +grobian +gustavoz +halcy0n +hansmi +hattya +hollow +hparker +ian +iggy +iluxa +jaervosz +jakub +je_fro +jer +jforman +jkt +jmbsvicetto +johnm +joker +joshuabaergen +jrinkovs +jstubbs +kaiowas +kallamej +kanaka +karltk +keri +killerfox +klieber +koon +kugelfang +kumba +lack +langthang +lanius +latexer +leonardop +liquidx +lu_zero +mabi +maedhros +malc +mark_alec +markusle +mcummings +moloh +mpagano +mr_bones_ +mrness +musikc +nakano +neddyseagoon +nelchael +nerdboy +neysx +nichoj +nightmorph +nixnut +nixphoeni +npmccallum +nyhm +omp +patrick +pauldv +pfeifer +phreak +pilla +pjp +port001 +pva +pvdabeel +pylon +pythonhead +quantumsummers +r2d2 +radek +rajiv +ramereth +rane +ranger +rbrown +rbu +remi +ribosome +rich0 +rl03 +robbat2 +roger55 +ronisbr +rphillips +seemant +shadow +so +solar +spb +spock +steev +stefaan +stkn +stuart +swift +tampakrap +tanderson +tantive +taviso +tester +tgall +the_paya +thunder +ticho +tigger +tove +tsunam +tupone +twp +ulm +usata +vapier +vivo +volkmar +vorlon +weeve +weirdedout +wltjr +wormo +yoswink +yvasilev +zhen +zypher +zzam diff --git a/foundation-referendum-2009-01/voters-foundation-referendum-2009-01-init b/foundation-referendum-2009-01/voters-foundation-referendum-2009-01-init new file mode 100644 index 0000000..7150063 --- /dev/null +++ b/foundation-referendum-2009-01/voters-foundation-referendum-2009-01-init @@ -0,0 +1,210 @@ +a3li Alex Legler 0xF3C06469 +aballier Alexis Ballier 0x160F534A +abcd Jonathan Callen 0x8D2840EA +agaffney Andrew Gaffney 0x6A2D77EB +agriffis Aron Griffis 0x20104EB0 +ali_bush Alistair Bush 0xE894970B +amne Wernfried Haas 0x16E5A780 +anant Anant Narayanan 0xB99B4111 +anpereir Andrés Pereira 0x76B74DAE +antarus Alec Warner 0xA00637F4 +araujo Luis Francisco Araujo 0xA2C0A9F1 +armin76 Raúl Porcel 0xF6AD3240 +astinus Alex Howells 0xB188A23A +axxo Thomas Matthijs 0x682A3231 +azarah Martin Schlemmer 0xA68960B6 +bangert Thilo Bangert 0x80390277 +bass Jose Alberto Suarez Lopez 0xF326EC24 +batlogg Jodok Batlogg 0x0E57868B +bcowan Brad Teaford Cowan 0xB1F16A56 +beandog Steve Dibb 0x96F8EB58 +beejay Benjamin Judas 0xC31DEDD8 +bennyc Benny Chuang 0xDB8FF979 +betelgeuse Petteri Räty 0x8182B0B4 +bicatali Sebastien Fabbro 0x13CB1360 +blackace Nicholas D. Wolfwood 0x0A5F7D12 +blubber Tiemo Kieft 0xE3E9E3A6 +bluebird Friedrich Oslage 0xF989EFA5 +calchan Denis Dupeyron 0xB16C047F +cam Camille Huot 0x6949514A +cardoe Doug Klima 0x179106D0 +carlo Carsten Lohrke 0xF18B496F +carpaski Nicholas Jones 0xB31DFA34 +caster Vlastimil Babka 0x4E61DE84 +cedk Cédric Krier 0x4EA4FF31 +chainsaw Tony Vroon 0xB5058F9A +chiguire John Christian Stoddart 0x21B3F464 +ciaranm Ciaran McCreesh 0x352D5E11 +clefebvre.62 Christophe LEFEBVRE +coldwind Santiago M. Mola 0xAAD203B5 +compnerd Saleem Abdulrasool 0x2D5645E5 +corsair Markus Rothe 0x04ABFC77 +cryos Marcus D. Hanwell 0x7D4117BD +cshields Corey Shields 0xA3041453 +cybersystem Sascha Schwabbauer 0x84EA0C8F +dabbott David Abbott 0xF01C4ACC +dang Daniel Gryniewicz 0x5D119EB1 +darkside Jeremy Olexa 0x60F8A50F +dberkholz Donnie Berkholz 0xB4B5AEDB +deathwing00 Ioannis Aslanidis 0x47F370A0 +dertobi123 Tobias Scherbaum 0x30C0F005 +desultory Dean Stephens 0xA8C5D9DB +dev-zero Tiziano Müller 0xAE9C1E30 +dholm David Holm 0x0228A97D +dirtyepic Ryan Hill 0xF9A40662 +dju Julien Allanos 0x3CA91F19 +dmwaters Deedra M. Waters 0xE4C635CF +dostrow Daniel Ostrow 0x237C2E05 +dragonheart Daniel Black 0x76677097 +drizzt Timothy Redaelli 0xEB8D3145 +drobbins Daniel Robbins 0xE372614F +dsd Daniel Drake 0x3795694A +edit21 James Phipps B1811D4F +eradicator Jeremy Huddleston 0x5FA03115 +ferdy Fernando J. Pereda 0x60BD28D4 +ferringb Brian Harring 0x8037554D +flameeyes Diego Pettenò 0x69875563 +flammie Flammie Pirinen 0x07469AA9 +ford_prefect Arun Raghavan 0x29C3E2EC +fox2mike Shyam Mani 0xFDD0E345 +fuzzyray Paul Varner 0x6B402757 +g2boojum Grant Goodyear 0xE0F65B76 +genone Marius Mauch 0x933B48D7 +genstef Stefan Schweizer 0xE115F4DB +gentoofan23 Thomas Anderson 0xA071C079 +geoman Stephen Becker 0xB393C856 +george George Shapovalov 0x0AD58A7F +gerrynjr Gerald J. Normandin Jr. 0xC1DBDF81 +graaff Hans de Graaff 0xFB0878BB +grahl Jan Hendrik Grahl 0x83A7B496 +gregkh Greg Kroah-Hartman 0xDB2DFB29 +grobian Fabian Groffen 0x1FCCCC42 +gustavoz Gustavo Zacarias 0x10A7FC90 +halcy0n Mark Loeser 0x458BAE84 +hansmi Michael Hanselmann 0x4A51BBEA +hattya Akinori Hattori 0xEC917A6D +hollow Benedikt Boehm 0xB5FAF161 +hparker Homer Parker 0x1D524429 +ian Christian Hartmann 0x692A4865 +iggy Brian Jackson 0x45C583E6 +iluxa Ilya Volynets 0x5166BA5A +jaervosz Sune Kloppenborg Jeppesen 0xC1CEEAB9 +jakub Jakub Moc 0xCEBA3D9E +je_fro Jeffrey Gardner 0x4A5D8F23 +jer Jeroen Roovers 0xA792A613 +jforman Jeffrey Forman 0x1950DDD4 +jkt Jan Kundrát 0x44722517 +jmbsvicetto Jorge Manuel B. S. Vicetto 0xF544C802 +johnm John Mylchreest 0x9C745515 +joker Christian Birchinger 0x608F6AEB +joshuabaergen Joshua Baergen 0x6221BC43 +jrinkovs Joe Rinkovsky 0x86E1E83A +jstubbs Jason Stubbs 0xC93F64FE +kaiowas Petre Rodan 0x055DEF31 +kallamej Anders Hellgren 0x65FE446E +kanaka Joel Martin 0xD0BD70B8 +karltk Karl Trygve Kalleberg 0xDCDE7E08 +keri Keri Harris 0x756D6F67 +killerfox René Nussbaumer 0x354BFD9F +klieber Kurt Lieber 0x27ED2046 +koon Thierry Carrez 0xB6A55F4F +kugelfang Danny van Dyk 0xDAED53A2 +kumba Joshua Kinard 0x339E0EE2 +lack Jim Ramsay 0x4E952AE8 +langthang Tuan Van 0x6476B2FD +lanius Heinrich Wendel 0x5B5E7771 +latexer Peter Johanson 0x6EFA3917 +leonardop Leonardo Boshell 0x3AB00DC4 +liquidx Alastair Tse 0x390714F6 +lu_zero Luca Barbato 0x84E90E34 +mabi Matti Bickel 0x4849EC6C +maedhros Jonathan Coome 0xA46E5974 +malc Malcolm Lashley 0x11C3441D +mark_alec Mark Kowarsky 0xEC3A5C32 +markusle Markus Dittrich 0x08FD678F +mcummings Michael Cummings 0x9E7F4E2E +moloh Michal Kurgan 0x6D452422 +mpagano Mike Pagano 0xB576E4E3 +mr_bones_ Michael Sterrett 0x22B40A2C +mrness Alin Năstac 0xE9D60F4D +musikc Christina Fullam 0xC6BA4161 +nakano Masatomo Nakano 0x6C61958A +neddyseagoon Roy Bamford 0x0D7A312C +nelchael Krzysiek Pawlik 0xBC555551 +nerdboy Steve Arnold 0x47D46D61 +neysx Xavier Neys 0x7A090902 +nichoj Joshua Nichols 0x8F856A5C +nightmorph Joshua Saddler 0x132C5725 +nixnut Gysbert Wassenaar 0x0DFD78A3 +nixphoeni Joe Sapp 0x721E3B48 +npmccallum Nathaniel McCallum 0x209B2B1E +nyhm Tristan Heaven 0x61B3014B +omp David Shakaryan 0x4B8FE14B +opfer Christian Faulhammer 0x2B859DE3 +patrick Patrick Lauer 0xE52864CE +pauldv Paul de Vrieze 0x18D615DB +pfeifer Jay Pfeifer 0xFD058638 +phreak Christian Heim 0x9A9F68E6 +pilla Mauricio Lima Pilla 0x441C1EB7 +pjp Peter Penkala 0x06627F19 +port001 Ian Leitch 0x60174462 +pva Peter Volkov 0x45926AB3 +pvdabeel Pieter van den Abeele 0xF238673E +pylon Lars Weiler 0x195165F4 +pythonhead Rob Cakebread 0x96BA679B +quantumsummers Matthew Summers 0x08789D46 +r2d2 Robert Paskowitz 0xE0C8678A +radek Radoslaw Stachowiak 0x500EC195 +rajiv Rajiv Aaron Manglani 0x302A3876 +ramereth Lance Albertson 0x27F4B742 +rane Łukasz Damentko 0x7C60CD3A +ranger Brent Baude 0xA212AD7A +rbrown Richard Brown 0xC41D5454 +rbu Robert Buchholz 0xFA61D6CA +remi Remi Cardona 0x901AB08A +ribosome Olivier Fisette 0x8B703AAA +rich0 Richard Freeman 0xA6665569 +rl03 Renat Lumpau 0xC6A838DA +robbat2 Robin H. Johnson 0x34884E85 +roger55 Roger Miliker 0x14E02B9C +ronisbr Ronan Arraes Jardim Chagas 0x0BDF14AA +rphillips Ryan Phillips 0x9A11D435 +seemant Seemant Kulleen 0x3458780E +shadow Damian Kuras 0x846B8E57 +so Stefano Pacella 0xB5330A9B +solar Ned Ludd 0x1E0A730C +spb Stephen Bennett 0x652088B8 +spock Michael Januszewski 0xA3947BE6 +steev Stephen Klimaszewski 0x5C4F6F68 +stefaan Stefaan De Roeck 0x4E0995E3 +stkn Stefan Knoblich 0xAE2B614A +stuart Stuart Herbert 0xF9AFC57C +swift Sven Vermeulen 0xCDBA2FDB +tampakrap Theo Chatzimichos 0x57DC0078 +tantive Michael Imhof 0x7EDC47B7 +taviso Tavis Ormandy 0x2690FD71 +tester Olivier Crête 0x3C7462D4 +tgall Tom Gall 0xA0568509 +the_paya Javier Villavicencio 0x09F2FA06 +thunder Damian Florczyk 0x92344129 +ticho Andrej Kacian 0x7CD93FE2 +tigger Rob Holland 0xD91B4729 +tove Torsten Veller 0x9C67CD96 +tsunam Joshua Jackson 0x9860FC4B +tupone Alfredo Tupone 0x7CF66216 +twp Tom William Payne 0xA18BCCFD +ulm Ulrich Müller 0x8222EEEC +usata Mamoru Komachi 0xACE78E88 +vapier Mike Frysinger 0xE837F581 +vivo Francesco Riosa 0xB54F32E0 +volkmar Mounir Lamouri 0xF2D5A6BE +vorlon Matthias Geerdsen 0xB16A5183 +weeve Jason Wever 0x58A8AB6F +weirdedout Noel Saliba C6EECFEF8C +wltjr William Thomson 0xCCD92F26 +wormo Stephanie J. Lockwood-Childs 0xB16E355F +yoswink José Luis Rivero 0xBFC72A72 +yvasilev Yuri Vasilevski 0x28938E36 +zhen John P. Davis 0xE28141BB +zypher Marc Hildebrand 0x03556F0E +zzam Matthias Schwarzott 0x7BD574E7 diff --git a/trustees2008/Votify.pm b/trustees2008/Votify.pm new file mode 100644 index 0000000..0c5565d --- /dev/null +++ b/trustees2008/Votify.pm @@ -0,0 +1,683 @@ +# $Id: Votify.pm,v 1.5 2005/05/16 23:58:09 agriffis Exp $ +# +# Copyright 2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# votify.pm: common classes for votify and countify +# + +package Votify; + +use POSIX; +use List::Util; +use strict; + +our ($datadir) = '/home/fox2mike/elections'; +(our $zero = $0) =~ s,.*/,,; + +sub import { + my ($class, $mode) = @_; + $Votify::mode = $mode; +} + +###################################################################### +# OfficialList +###################################################################### + +package OfficialList; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + officials => [], + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/officials-$election") + or die("failed to open officials file"); + chomp(@{$self->{'officials'}} = <F>); + close(F); + + bless $self, $class; + return $self; +} + +sub officials { + my ($self) = @_; + @{$self->{'officials'}}; +} + +###################################################################### +# VoterList +###################################################################### + +package VoterList; + +sub new { + my ($class, $election) = @_; + my (@voterlist, $r); + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/confs-$election", + filename => '', + voters => {}, # confnum => voter + confs => {}, # voter => confnum + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/voters-$election") + or die("failed to open voters file"); + chomp(@voterlist = <F>); + close(F); + + # assign confirmation numbers randomly + for my $v (@voterlist) { + do { $r = int rand 0xffff } while exists $self->{'voters'}{$r}; + $self->{'voters'}{$r} = $v; + $self->{'confs'}{$v} = $r; + } + + unless (keys %{$self->{'voters'}} == keys %{$self->{'confs'}}) { + die("discrepancy deteced in VoterList"); + } + + bless $self, $class; + return $self; +} + +sub confs { + my ($self) = @_; + sort keys %{$self->{'voters'}}; +} + +sub voters { + my ($self) = @_; + sort keys %{$self->{'confs'}}; +} + +sub getvoter { + my ($self, $conf) = @_; + return $self->{'voters'}{$conf}; +} + +sub getconf { + my ($self, $voter) = @_; + return $self->{'confs'}{$voter}; +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c ($self->confs) { + printf F "%04x %s\n", $c, $self->getvoter($c); + } + close F; +} + +###################################################################### +# MasterBallot +###################################################################### + +package MasterBallot; + +use Data::Dumper; + +sub new { + my ($class, $election, $vl) = @_; + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/master-$election", + filename => '', + voterlist => $vl, + ballots => {}, # indexed by conf num + candidates => undef, # indexed by long name + table => undef, # indexed by row+column + }; + + bless $self, $class; + return $self; +} + +sub collect { + my ($self, @voters) = @_; + my ($c, $v, $home, @pwentry); + + for my $v (@voters) { + unless (defined ($c = $self->{'voterlist'}->getconf($v))) { + die "$v does not correspond to any confirmation number"; + } + + @pwentry = getpwnam($v); + unless (@pwentry) { + print STDERR "Warning: unknown user: $v\n"; + next; + } + + $home = $pwentry[7]; + unless (-d $home) { + print STDERR "Warning: no directory: $home\n"; + next; + } + + if (-f "$home/.ballot-$self->{election}-submitted") { + my ($b) = Ballot->new($self->{'election'}); + $b->read("$home/.ballot-$self->{election}-submitted"); + if ($b->verify) { + print STDERR "Errors found in ballot: $v\n"; + next; + } + $self->{'ballots'}{$c} = $b; + } + elsif (-f "$home/.ballot-$self->{election}") { + print STDERR "Warning: $v did not submit their ballot\n"; + } + } +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c (sort keys %{$self->{'ballots'}}) { + printf F "--------- confirmation %04x ---------\n", $c; + print F $self->{'ballots'}{$c}->to_s + } + close F; +} + +sub read { + my ($self, $filename) = @_; + my ($election, $entries) = $self->{'election'}; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + open(F, "<$filename") or die("can't read $filename"); + { local $/ = undef; $entries = <F>; } + for my $e (split /^--------- confirmation /m, $entries) { + next unless $e; # skip the first zero-length record + unless ($e =~ /^([[:xdigit:]]{4}) ---------\n(.*)$/s) { + die "error parsing entry:\n$e"; + } + my ($c, $s, $b) = ($1, $2, Ballot->new($election)); + $b->from_s($s); + $self->{'ballots'}{hex($c)} = $b; + } +} + +sub generate_candidates { + my ($self) = @_; + my ($B, @C, $s); + + # nb: would need to scan all the ballots to support write-ins + $B = Ballot->new($self->{'election'}); + $B->populate; + @C = sort map $_->[0], @{$B->choices}; + for my $c (@C) { + $s = $c; # in case $c is shorter than 5 chars + for (my $i=5; $i<=length($c); $i++) { + $s = substr $c, 0, $i; + print join(" ", grep(/^$s/, @C)), "\n"; + last unless grep(/^$s/, @C) > 1; + } + $self->{'candidates'}{$c} = $s; + } +} + +sub tabulate { + my ($self) = @_; + my (@candidates); # full candidate list + my (%table); # resulting table, row.colum where row defeats column + $self->{'table'} = \%table; + + $self->generate_candidates unless $self->{'candidates'}; + @candidates = keys %{$self->{'candidates'}}; + for my $c1 (@candidates) { + for my $c2 (@candidates) { + $table{"$c1+$c2"} = 0; + } + $table{"$c1+$c1"} = '***'; + } + + # generate the table first; + # walk through the ballots, tallying the rankings expressed by each ballot + for my $b (values %{$self->{'ballots'}}) { + my (@choices, %ranks); + + #print "looking at ballot:\n", $b->to_s, "\n"; + + # first determine the ranking of each candidate. default ranking is + # scalar @candidates. + @choices = @{$b->choices}; + @ranks{@candidates} = (scalar @candidates) x @candidates; + #print "ranks before determining:\n", Dumper(\%ranks); + for (my $i = 0; $i < @choices; $i++) { + @ranks{@{$choices[$i]}} = ($i) x @{$choices[$i]}; + } + #print "ranks after determining:\n", Dumper(\%ranks); + + # second add the results of all the pairwise races into our table + for my $c1 (@candidates) { + for my $c2 (@candidates) { + next if $c1 eq $c2; + $table{"$c1+$c2"}++ if $ranks{$c1} < $ranks{$c2}; + } + } + #print "table after adding:\n"; + #$self->display_table; + } +} + +sub display_table { + my ($self) = @_; + my (@longnames, @shortnames); + my ($minlen, $maxlen, $formatstr) = (0, 4, ''); + + $self->generate_candidates unless $self->{'candidates'}; + @longnames = sort keys %{$self->{'candidates'}}; + @shortnames = sort values %{$self->{'candidates'}}; + $minlen = length scalar keys %{$self->{'ballots'}}; + $minlen = 5 if $minlen < 5; + + # build the format string + for my $s (@shortnames) { + if (length($s) > $minlen) { + $formatstr .= " %" . length($s) . "s"; + } else { + $formatstr .= " %${minlen}s"; + } + } + map { $maxlen = length($_) if length($_) > $maxlen } @longnames; + + # prepend the row header; append newline + $formatstr = "%${maxlen}s" . $formatstr . "\n"; + + # column headers + printf $formatstr, '', @shortnames; + + # rows + for my $l (@longnames) { + printf $formatstr, $l, @{$self->{'table'}}{map "$l+$_", @longnames}; + } +} + +# utility for cssd +sub defeats { + my ($self, $o1, $o2) = @_; + return 0 if $o1 eq $o2; + $self->{'table'}{"$o1+$o2"} > $self->{'table'}{"$o2+$o1"}; +} + +# utility for cssd +sub is_weaker_defeat { + my ($self, $A, $X, $B, $Y) = @_; + die unless $self->defeats($A, $X); + die unless $self->defeats($B, $Y); + return ( + $self->{'table'}{"$A+$X"} < $self->{'table'}{"$B+$Y"} or + ( + $self->{'table'}{"$A+$X"} == $self->{'table'}{"$B+$Y"} and + $self->{'table'}{"$X+$A"} > $self->{'table'}{"$Y+$B"} + ) + ); +} + +sub cssd { + my ($self) = @_; + my (@candidates); + + @candidates = sort keys %{$self->{'candidates'}}; + + while (1) { + my (%transitive_defeats); + my (@active, @plist); + + ###################################################################### + # 5. From the list of [undropped] pairwise defeats, we generate a + # set of transitive defeats. + # 1. An option A transitively defeats an option C if A + # defeats C or if there is some other option B where A + # defeats B AND B transitively defeats C. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + $transitive_defeats{"$o1+$o2"} = 1 if $self->defeats($o1, $o2); + } + } + for my $i (@candidates) { + for my $j (@candidates) { + for my $k (@candidates) { + if (exists $transitive_defeats{"$j+$i"} and + exists $transitive_defeats{"$i+$k"}) + { + $transitive_defeats{"$j+$k"} = 1; + } + } + } + } + + ###################################################################### + # 6. We construct the Schwartz set from the set of transitive + # defeats. + # 1. An option A is in the Schwartz set if for all options B, + # either A transitively defeats B, or B does not + # transitively defeat A. + print "\n"; + A: for my $A (@candidates) { + for my $B (@candidates) { + next if $transitive_defeats{"$A+$B"} or not $transitive_defeats{"$B+$A"}; + # countify marks entries +++ instead of *** when they've already + # been ranked. + if ($self->{'table'}{"$A+$A"} eq '***') { + print "option $A is eliminated ($B trans-defeats $A, and $A does not trans-defeat $B)\n"; + } + next A; + } + push @active, $A; + } + print "the Schwartz set is {", join(", ", @active), "}\n"; + @candidates = @active; + + ###################################################################### + # 7. If there are defeats between options in the Schwartz set, we + # drop the weakest such defeats from the list of pairwise + # defeats, and return to step 5. + # 1. A defeat (A,X) is weaker than a defeat (B,Y) if V(A,X) + # is less than V(B,Y). Also, (A,X) is weaker than (B,Y) if + # V(A,X) is equal to V(B,Y) and V(X,A) is greater than V + # (Y,B). + # 2. A weakest defeat is a defeat that has no other defeat + # weaker than it. There may be more than one such defeat. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + push @plist, [ $o1, $o2 ] if $self->defeats($o1, $o2); + } + } + last unless @plist; + @plist = sort { + return -1 if $self->is_weaker_defeat(@$a, @$b); + return +1 if $self->is_weaker_defeat(@$b, @$a); + return 0; + } @plist; + for my $dx (@plist) { + my ($o1, $o2) = @$dx; + print("$o1+$o2 ", + $self->{'table'}{"$o1+$o2"}, " $o2+$o1 ", + $self->{'table'}{"$o2+$o1"}, "\n"); + } + my ($o1, $o2) = @{$plist[0]}; + $self->{'table'}{"$o1+$o2"} = 0; + $self->{'table'}{"$o2+$o1"} = 0; + } + + ###################################################################### + # 8. If there are no defeats within the Schwartz set, then the + # winner is chosen from the options in the Schwartz set. If + # there is only one such option, it is the winner. If there + # are multiple options, the elector with the casting vote + # chooses which of those options wins. + print "\n"; + if (@candidates > 1) { + print "result: tie between options ", join(", ", @candidates), "\n"; + } else { + print "result: option @candidates wins\n"; + } + + return @candidates; +} + +###################################################################### +# Ballot +###################################################################### + +package Ballot; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + filename => '', + default_filename => $ENV{'HOME'}."/.ballot-$election", + choices => [], + }; + + # Bless me, I'm a ballot! + bless $self, $class; + return $self; +} + +sub from_s { + my ($self, $s) = @_; + my (@choices); + + for (split "\n", $s) { + s/#.*//; + next unless /\S/; + push @choices, [ split(' ', $_) ]; + } + die("No data in string") unless @choices; + + $self->{'choices'} = \@choices; +} + +sub read { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Load the data file + open(F, "<$filename") or die("couldn't open $filename"); + { local $/ = undef; $self->from_s(<F>); } + close(F); +} + +sub populate { + my ($self) = @_; + $self->read("$Votify::datadir/ballot-$self->{election}"); + @{$self->{'choices'}} = List::Util::shuffle(@{$self->{'choices'}}); +} + +sub choices { + my ($self) = @_; + $self->{'choices'}; +} + +sub write { + my ($self, $filename) = @_; + + if ($Votify::mode ne 'user') { + die("we don't write ballots in official mode"); + } + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Don't ever overwrite a ballot + die("File already exists; please remove $filename\n") if -e $filename; + + # Write the user's ballot + open(F, ">$filename") or die "Failed writing $filename"; + print F <<EOT; +# This is a ballot for the $self->{election} election. +# Please rank your choices in order; first choice at the top and last choice at +# the bottom. You can put choices on the same line to indicate no preference +# between them. Any choices you omit from this file are implicitly added at the +# end. +# +# When you're finished editing this, the next step is to verify your ballot +# with: +# +# $Votify::zero --verify $self->{election} +# +# When that passes and you're satisfied, the final step is to submit your vote: +# +# $Votify::zero --submit $self->{election} +# + +EOT + for (@{$self->{'choices'}}) { print F "@$_\n"; } + close(F); +} + +sub verify { + my ($self) = @_; + my (%h, $master, %mh); + my (@dups, @missing, @extra); + my ($errors_found); + + # Load %h from the user's ballot + for my $line (@{$self->{'choices'}}) { + for my $entry (@$line) { + $h{$entry}++; + } + } + + # Load the master ballot into another hash and compare them. + # The master ballots always do one entry per line, making this a little + # easier. + $master = Ballot->new($self->{'election'}); + $master->populate; + %mh = map(($_->[0] => 1), @{$master->{'choices'}}); + + # Check for extra entries (write-ins should be supported in the future) + for (keys %h) { + push @extra, $_ unless exists $mh{$_}; + } + + # Check for duplicate entries + @dups = grep { $h{$_} > 1 } keys %h; + + # Check for missing entries (not necessarily an error) + for (keys %mh) { + push @missing, $_ unless exists $h{$_}; + } + + # Report errors and warnings + if (@extra) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some extra entries that are not part of this election. Sorry, +but write-ins are not (yet) supported. Please remove these from your ballot: + +EOT + print map "\t$_\n", @extra; + print "\n"; + } + $errors_found++; + } + if (@dups) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some duplicate entries. Please resolve these to a single entry +to avoid ambiguities: + +EOT + print map "\t$_\n", @dups; + print "\n"; + } + $errors_found++; + } + if (@{$self->{'choices'}} == 0) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot doesn't contain any entries. You can start over by first removing +the existing ballot, then using --new to generate a new ballot. See --help for +more information. + +EOT + } + $errors_found++; + } + elsif (@missing and $Votify::mode eq 'user') { + print <<EOT; +Your ballot is missing some entries. This is not an error, but note that these +will be implied as a final line, with no preference between them, like this: + +EOT + print "\t", join(" ", @missing), "\n"; + print "\n"; + } + if ($Votify::mode eq 'user' and !$errors_found and + @{$self->{'choices'}} == 1 and + scalar(keys %h) == scalar(keys %mh)) + { + print <<EOT; +Your ballot contains all the candidates on a single line! This means you have +no preference between the candidates. This is not an error, but note that this +is a meaningless ballot that will have no effect on the election. + +EOT + } + + # Stop if there were errors + if ($Votify::mode eq 'user' and $errors_found) { + print("There were errors found in your ballot.\n"); + die("Please correct them and try again.\n\n"); + } + return $errors_found; +} + +sub to_s { + my ($self) = @_; + join '', map "@$_\n", @{$self->{'choices'}}; +} + +1; + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + +Revision 1.3 2005/05/09 23:12:02 agriffis +Add support for registered voters + +Revision 1.2 2005/05/05 23:03:46 agriffis +Fix indentation (and some output as well) + +Revision 1.1 2005/05/05 22:05:34 agriffis +first pass at Gentoo Foundation voting program + +# vim:sw=4 et diff --git a/trustees2008/ballot-trustees2008 b/trustees2008/ballot-trustees2008 new file mode 100644 index 0000000..22f8b37 --- /dev/null +++ b/trustees2008/ballot-trustees2008 @@ -0,0 +1,8 @@ + wltjr + NeddySeagoon + tgall + patrick + tsunam + je_fro + fmccor + jakub diff --git a/trustees2008/officials-trustees2008 b/trustees2008/officials-trustees2008 new file mode 100644 index 0000000..b0bec0c --- /dev/null +++ b/trustees2008/officials-trustees2008 @@ -0,0 +1,4 @@ +jmbsvicetto +rane +rich0 +root diff --git a/trustees2008/start-trustees2008 b/trustees2008/start-trustees2008 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/trustees2008/start-trustees2008 diff --git a/trustees2008/stop-trustees2008 b/trustees2008/stop-trustees2008 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/trustees2008/stop-trustees2008 diff --git a/trustees2008/voters-trustees2008 b/trustees2008/voters-trustees2008 new file mode 100644 index 0000000..75edc58 --- /dev/null +++ b/trustees2008/voters-trustees2008 @@ -0,0 +1,295 @@ +aballier +agaffney +agriffis +alin +alonbl +amne +anant +anpereir +antarus +araujo +armin76 +aross +astinus +axxo +azarah +bangert +bass +batlogg +battousai +bbj +bcowan +beandog +beejay +bennyc +betelgeuse +bicatali +blackace +blubber +calchan +cam +cardoe +carlo +carpaski +caster +cedk +centic +chainsaw +chiguire +chrb +christel +chtekk +chutzpah +ciaranm +codeman +compnerd +corsair +cryos +cshields +cybersystem +dams +dang +dberkholz +dcoutts +deathwing00 +dercorny +dertobi123 +desultory +dev-zero +dholm +dirtyepic +djay +dju +dmwaters +dostrow +drac +dragonheart +drizzt +drobbins +dsd +earthwings +ehmsen +eldad +eradicator +falco +ferdy +ferringb +flameeyes +flammie +fmccor +fordfrog +fox2mike +fuzzyray +g2boojum +genone +genstef +geoman +george +gerrynjr +gmsoft +graaff +grahl +gregkh +griffon26 +grobian +gurligebis +gustavoz +halcy0n +hanno +hansmi +hattya +hd_brummy +hkbst +hlieberman +hollow +hparker +humpback +hyakuhei +ian +iggy +ikelos +iluxa +jaervosz +jakub +je_fro +jer +jforman +jkt +jmbsvicetto +jmglov +johnm +joker +jokey +josejx +joshuabaergen +joslwah +jrinkovs +jstubbs +jurek +kaiowas +kallamej +kanaka +kang +karltk +kengland +keri +kernelsensei +kevquinn +killerfox +kingtaco +klieber +kloeri +kolmodin +koon +kosmikus +kugelfang +kumba +kutsuya +lack +langthang +lanius +latexer +lcars +leio +leonardop +liquidx +livewire +lordvan +lucass +lu_zero +mabi +maedhros +malc +marienz +marineam +mark_alec +markm +markusle +masterdriverz +matsuu +mattepiu +mcummings +mdisney +method +mjolnir +mkay +moloh +mr_bones_ +mrness +nakano +neddyseagoon +nelchael +nerdboy +neysx +nichoj +nightmorph +nixnut +nixphoeni +npmccallum +nyhm +omp +opfer +pappy +patrick +pauldv +pbienst +pclouds +pebenito +peitolm +peper +pfeifer +phosphan +phreak +pilla +pingu +pjp +polvi +port001 +psi29a +pva +pvdabeel +pylon +pythonhead +r2d2 +r3pek +radek +rajiv +ramereth +rane +ranger +rbrown +rbu +reb +redhatter +remi +ribosome +rl03 +robbat2 +rocket +roger55 +rphillips +s4t4n +satya +sbriesen +seemant +sejo +sekretarz +shadow +shellsage +shindo +sirseoman +smithj +so +solar +spb +spock +spyderous +steev +stefaan +stkn +strerror +stuart +suka +swegener +swift +tantive +taviso +tchiwam +tester +tgall +the_paya +thoand +thunder +ticho +tigger +tomk +tove +trapni +troll +truedfx +tsunam +tupone +twp +usata +vanquirius +vapier +vivo +vorlon +voxus +weeve +welp +williamh +wltjr +wolf31o2 +wormo +wrobel +wschlich +xmerlin +yoswink +yuval +yvasilev +zaheerm +zhen +zmedico +zypher +zzam diff --git a/trustees2009/Votify.pm b/trustees2009/Votify.pm new file mode 100644 index 0000000..0c5565d --- /dev/null +++ b/trustees2009/Votify.pm @@ -0,0 +1,683 @@ +# $Id: Votify.pm,v 1.5 2005/05/16 23:58:09 agriffis Exp $ +# +# Copyright 2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# votify.pm: common classes for votify and countify +# + +package Votify; + +use POSIX; +use List::Util; +use strict; + +our ($datadir) = '/home/fox2mike/elections'; +(our $zero = $0) =~ s,.*/,,; + +sub import { + my ($class, $mode) = @_; + $Votify::mode = $mode; +} + +###################################################################### +# OfficialList +###################################################################### + +package OfficialList; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + officials => [], + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/officials-$election") + or die("failed to open officials file"); + chomp(@{$self->{'officials'}} = <F>); + close(F); + + bless $self, $class; + return $self; +} + +sub officials { + my ($self) = @_; + @{$self->{'officials'}}; +} + +###################################################################### +# VoterList +###################################################################### + +package VoterList; + +sub new { + my ($class, $election) = @_; + my (@voterlist, $r); + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/confs-$election", + filename => '', + voters => {}, # confnum => voter + confs => {}, # voter => confnum + }; + + # no point in waiting to load + open(F, "<$Votify::datadir/voters-$election") + or die("failed to open voters file"); + chomp(@voterlist = <F>); + close(F); + + # assign confirmation numbers randomly + for my $v (@voterlist) { + do { $r = int rand 0xffff } while exists $self->{'voters'}{$r}; + $self->{'voters'}{$r} = $v; + $self->{'confs'}{$v} = $r; + } + + unless (keys %{$self->{'voters'}} == keys %{$self->{'confs'}}) { + die("discrepancy deteced in VoterList"); + } + + bless $self, $class; + return $self; +} + +sub confs { + my ($self) = @_; + sort keys %{$self->{'voters'}}; +} + +sub voters { + my ($self) = @_; + sort keys %{$self->{'confs'}}; +} + +sub getvoter { + my ($self, $conf) = @_; + return $self->{'voters'}{$conf}; +} + +sub getconf { + my ($self, $voter) = @_; + return $self->{'confs'}{$voter}; +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c ($self->confs) { + printf F "%04x %s\n", $c, $self->getvoter($c); + } + close F; +} + +###################################################################### +# MasterBallot +###################################################################### + +package MasterBallot; + +use Data::Dumper; + +sub new { + my ($class, $election, $vl) = @_; + my ($self) = { + election => $election, + default_filename => "$Votify::datadir/master-$election", + filename => '', + voterlist => $vl, + ballots => {}, # indexed by conf num + candidates => undef, # indexed by long name + table => undef, # indexed by row+column + }; + + bless $self, $class; + return $self; +} + +sub collect { + my ($self, @voters) = @_; + my ($c, $v, $home, @pwentry); + + for my $v (@voters) { + unless (defined ($c = $self->{'voterlist'}->getconf($v))) { + die "$v does not correspond to any confirmation number"; + } + + @pwentry = getpwnam($v); + unless (@pwentry) { + print STDERR "Warning: unknown user: $v\n"; + next; + } + + $home = $pwentry[7]; + unless (-d $home) { + print STDERR "Warning: no directory: $home\n"; + next; + } + + if (-f "$home/.ballot-$self->{election}-submitted") { + my ($b) = Ballot->new($self->{'election'}); + $b->read("$home/.ballot-$self->{election}-submitted"); + if ($b->verify) { + print STDERR "Errors found in ballot: $v\n"; + next; + } + $self->{'ballots'}{$c} = $b; + } + elsif (-f "$home/.ballot-$self->{election}") { + print STDERR "Warning: $v did not submit their ballot\n"; + } + } +} + +sub write { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + if (-f $filename) { + die "$filename already exists; please remove it first"; + } + + open(F, ">$filename") or die("can't write to $filename"); + for my $c (sort keys %{$self->{'ballots'}}) { + printf F "--------- confirmation %04x ---------\n", $c; + print F $self->{'ballots'}{$c}->to_s + } + close F; +} + +sub read { + my ($self, $filename) = @_; + my ($election, $entries) = $self->{'election'}; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + open(F, "<$filename") or die("can't read $filename"); + { local $/ = undef; $entries = <F>; } + for my $e (split /^--------- confirmation /m, $entries) { + next unless $e; # skip the first zero-length record + unless ($e =~ /^([[:xdigit:]]{4}) ---------\n(.*)$/s) { + die "error parsing entry:\n$e"; + } + my ($c, $s, $b) = ($1, $2, Ballot->new($election)); + $b->from_s($s); + $self->{'ballots'}{hex($c)} = $b; + } +} + +sub generate_candidates { + my ($self) = @_; + my ($B, @C, $s); + + # nb: would need to scan all the ballots to support write-ins + $B = Ballot->new($self->{'election'}); + $B->populate; + @C = sort map $_->[0], @{$B->choices}; + for my $c (@C) { + $s = $c; # in case $c is shorter than 5 chars + for (my $i=5; $i<=length($c); $i++) { + $s = substr $c, 0, $i; + print join(" ", grep(/^$s/, @C)), "\n"; + last unless grep(/^$s/, @C) > 1; + } + $self->{'candidates'}{$c} = $s; + } +} + +sub tabulate { + my ($self) = @_; + my (@candidates); # full candidate list + my (%table); # resulting table, row.colum where row defeats column + $self->{'table'} = \%table; + + $self->generate_candidates unless $self->{'candidates'}; + @candidates = keys %{$self->{'candidates'}}; + for my $c1 (@candidates) { + for my $c2 (@candidates) { + $table{"$c1+$c2"} = 0; + } + $table{"$c1+$c1"} = '***'; + } + + # generate the table first; + # walk through the ballots, tallying the rankings expressed by each ballot + for my $b (values %{$self->{'ballots'}}) { + my (@choices, %ranks); + + #print "looking at ballot:\n", $b->to_s, "\n"; + + # first determine the ranking of each candidate. default ranking is + # scalar @candidates. + @choices = @{$b->choices}; + @ranks{@candidates} = (scalar @candidates) x @candidates; + #print "ranks before determining:\n", Dumper(\%ranks); + for (my $i = 0; $i < @choices; $i++) { + @ranks{@{$choices[$i]}} = ($i) x @{$choices[$i]}; + } + #print "ranks after determining:\n", Dumper(\%ranks); + + # second add the results of all the pairwise races into our table + for my $c1 (@candidates) { + for my $c2 (@candidates) { + next if $c1 eq $c2; + $table{"$c1+$c2"}++ if $ranks{$c1} < $ranks{$c2}; + } + } + #print "table after adding:\n"; + #$self->display_table; + } +} + +sub display_table { + my ($self) = @_; + my (@longnames, @shortnames); + my ($minlen, $maxlen, $formatstr) = (0, 4, ''); + + $self->generate_candidates unless $self->{'candidates'}; + @longnames = sort keys %{$self->{'candidates'}}; + @shortnames = sort values %{$self->{'candidates'}}; + $minlen = length scalar keys %{$self->{'ballots'}}; + $minlen = 5 if $minlen < 5; + + # build the format string + for my $s (@shortnames) { + if (length($s) > $minlen) { + $formatstr .= " %" . length($s) . "s"; + } else { + $formatstr .= " %${minlen}s"; + } + } + map { $maxlen = length($_) if length($_) > $maxlen } @longnames; + + # prepend the row header; append newline + $formatstr = "%${maxlen}s" . $formatstr . "\n"; + + # column headers + printf $formatstr, '', @shortnames; + + # rows + for my $l (@longnames) { + printf $formatstr, $l, @{$self->{'table'}}{map "$l+$_", @longnames}; + } +} + +# utility for cssd +sub defeats { + my ($self, $o1, $o2) = @_; + return 0 if $o1 eq $o2; + $self->{'table'}{"$o1+$o2"} > $self->{'table'}{"$o2+$o1"}; +} + +# utility for cssd +sub is_weaker_defeat { + my ($self, $A, $X, $B, $Y) = @_; + die unless $self->defeats($A, $X); + die unless $self->defeats($B, $Y); + return ( + $self->{'table'}{"$A+$X"} < $self->{'table'}{"$B+$Y"} or + ( + $self->{'table'}{"$A+$X"} == $self->{'table'}{"$B+$Y"} and + $self->{'table'}{"$X+$A"} > $self->{'table'}{"$Y+$B"} + ) + ); +} + +sub cssd { + my ($self) = @_; + my (@candidates); + + @candidates = sort keys %{$self->{'candidates'}}; + + while (1) { + my (%transitive_defeats); + my (@active, @plist); + + ###################################################################### + # 5. From the list of [undropped] pairwise defeats, we generate a + # set of transitive defeats. + # 1. An option A transitively defeats an option C if A + # defeats C or if there is some other option B where A + # defeats B AND B transitively defeats C. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + $transitive_defeats{"$o1+$o2"} = 1 if $self->defeats($o1, $o2); + } + } + for my $i (@candidates) { + for my $j (@candidates) { + for my $k (@candidates) { + if (exists $transitive_defeats{"$j+$i"} and + exists $transitive_defeats{"$i+$k"}) + { + $transitive_defeats{"$j+$k"} = 1; + } + } + } + } + + ###################################################################### + # 6. We construct the Schwartz set from the set of transitive + # defeats. + # 1. An option A is in the Schwartz set if for all options B, + # either A transitively defeats B, or B does not + # transitively defeat A. + print "\n"; + A: for my $A (@candidates) { + for my $B (@candidates) { + next if $transitive_defeats{"$A+$B"} or not $transitive_defeats{"$B+$A"}; + # countify marks entries +++ instead of *** when they've already + # been ranked. + if ($self->{'table'}{"$A+$A"} eq '***') { + print "option $A is eliminated ($B trans-defeats $A, and $A does not trans-defeat $B)\n"; + } + next A; + } + push @active, $A; + } + print "the Schwartz set is {", join(", ", @active), "}\n"; + @candidates = @active; + + ###################################################################### + # 7. If there are defeats between options in the Schwartz set, we + # drop the weakest such defeats from the list of pairwise + # defeats, and return to step 5. + # 1. A defeat (A,X) is weaker than a defeat (B,Y) if V(A,X) + # is less than V(B,Y). Also, (A,X) is weaker than (B,Y) if + # V(A,X) is equal to V(B,Y) and V(X,A) is greater than V + # (Y,B). + # 2. A weakest defeat is a defeat that has no other defeat + # weaker than it. There may be more than one such defeat. + for my $o1 (@candidates) { + for my $o2 (@candidates) { + push @plist, [ $o1, $o2 ] if $self->defeats($o1, $o2); + } + } + last unless @plist; + @plist = sort { + return -1 if $self->is_weaker_defeat(@$a, @$b); + return +1 if $self->is_weaker_defeat(@$b, @$a); + return 0; + } @plist; + for my $dx (@plist) { + my ($o1, $o2) = @$dx; + print("$o1+$o2 ", + $self->{'table'}{"$o1+$o2"}, " $o2+$o1 ", + $self->{'table'}{"$o2+$o1"}, "\n"); + } + my ($o1, $o2) = @{$plist[0]}; + $self->{'table'}{"$o1+$o2"} = 0; + $self->{'table'}{"$o2+$o1"} = 0; + } + + ###################################################################### + # 8. If there are no defeats within the Schwartz set, then the + # winner is chosen from the options in the Schwartz set. If + # there is only one such option, it is the winner. If there + # are multiple options, the elector with the casting vote + # chooses which of those options wins. + print "\n"; + if (@candidates > 1) { + print "result: tie between options ", join(", ", @candidates), "\n"; + } else { + print "result: option @candidates wins\n"; + } + + return @candidates; +} + +###################################################################### +# Ballot +###################################################################### + +package Ballot; + +sub new { + my ($class, $election) = @_; + my ($self) = { + election => $election, + filename => '', + default_filename => $ENV{'HOME'}."/.ballot-$election", + choices => [], + }; + + # Bless me, I'm a ballot! + bless $self, $class; + return $self; +} + +sub from_s { + my ($self, $s) = @_; + my (@choices); + + for (split "\n", $s) { + s/#.*//; + next unless /\S/; + push @choices, [ split(' ', $_) ]; + } + die("No data in string") unless @choices; + + $self->{'choices'} = \@choices; +} + +sub read { + my ($self, $filename) = @_; + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Load the data file + open(F, "<$filename") or die("couldn't open $filename"); + { local $/ = undef; $self->from_s(<F>); } + close(F); +} + +sub populate { + my ($self) = @_; + $self->read("$Votify::datadir/ballot-$self->{election}"); + @{$self->{'choices'}} = List::Util::shuffle(@{$self->{'choices'}}); +} + +sub choices { + my ($self) = @_; + $self->{'choices'}; +} + +sub write { + my ($self, $filename) = @_; + + if ($Votify::mode ne 'user') { + die("we don't write ballots in official mode"); + } + + $filename ||= $self->{'default_filename'}; + $self->{'filename'} = $filename; + + # Don't ever overwrite a ballot + die("File already exists; please remove $filename\n") if -e $filename; + + # Write the user's ballot + open(F, ">$filename") or die "Failed writing $filename"; + print F <<EOT; +# This is a ballot for the $self->{election} election. +# Please rank your choices in order; first choice at the top and last choice at +# the bottom. You can put choices on the same line to indicate no preference +# between them. Any choices you omit from this file are implicitly added at the +# end. +# +# When you're finished editing this, the next step is to verify your ballot +# with: +# +# $Votify::zero --verify $self->{election} +# +# When that passes and you're satisfied, the final step is to submit your vote: +# +# $Votify::zero --submit $self->{election} +# + +EOT + for (@{$self->{'choices'}}) { print F "@$_\n"; } + close(F); +} + +sub verify { + my ($self) = @_; + my (%h, $master, %mh); + my (@dups, @missing, @extra); + my ($errors_found); + + # Load %h from the user's ballot + for my $line (@{$self->{'choices'}}) { + for my $entry (@$line) { + $h{$entry}++; + } + } + + # Load the master ballot into another hash and compare them. + # The master ballots always do one entry per line, making this a little + # easier. + $master = Ballot->new($self->{'election'}); + $master->populate; + %mh = map(($_->[0] => 1), @{$master->{'choices'}}); + + # Check for extra entries (write-ins should be supported in the future) + for (keys %h) { + push @extra, $_ unless exists $mh{$_}; + } + + # Check for duplicate entries + @dups = grep { $h{$_} > 1 } keys %h; + + # Check for missing entries (not necessarily an error) + for (keys %mh) { + push @missing, $_ unless exists $h{$_}; + } + + # Report errors and warnings + if (@extra) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some extra entries that are not part of this election. Sorry, +but write-ins are not (yet) supported. Please remove these from your ballot: + +EOT + print map "\t$_\n", @extra; + print "\n"; + } + $errors_found++; + } + if (@dups) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot has some duplicate entries. Please resolve these to a single entry +to avoid ambiguities: + +EOT + print map "\t$_\n", @dups; + print "\n"; + } + $errors_found++; + } + if (@{$self->{'choices'}} == 0) { + if ($Votify::mode eq 'user') { + print <<EOT; +Your ballot doesn't contain any entries. You can start over by first removing +the existing ballot, then using --new to generate a new ballot. See --help for +more information. + +EOT + } + $errors_found++; + } + elsif (@missing and $Votify::mode eq 'user') { + print <<EOT; +Your ballot is missing some entries. This is not an error, but note that these +will be implied as a final line, with no preference between them, like this: + +EOT + print "\t", join(" ", @missing), "\n"; + print "\n"; + } + if ($Votify::mode eq 'user' and !$errors_found and + @{$self->{'choices'}} == 1 and + scalar(keys %h) == scalar(keys %mh)) + { + print <<EOT; +Your ballot contains all the candidates on a single line! This means you have +no preference between the candidates. This is not an error, but note that this +is a meaningless ballot that will have no effect on the election. + +EOT + } + + # Stop if there were errors + if ($Votify::mode eq 'user' and $errors_found) { + print("There were errors found in your ballot.\n"); + die("Please correct them and try again.\n\n"); + } + return $errors_found; +} + +sub to_s { + my ($self) = @_; + join '', map "@$_\n", @{$self->{'choices'}}; +} + +1; + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + + +__END__ + +$Log: Votify.pm,v $ +Revision 1.5 2005/05/16 23:58:09 agriffis +change wording + +Revision 1.4 2005/05/16 18:40:07 agriffis +fix shortname calculation + +Revision 1.3 2005/05/16 18:10:46 agriffis +ranking works completely now, even if it needs badly to be refactored + +Revision 1.2 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + +Revision 1.3 2005/05/09 23:12:02 agriffis +Add support for registered voters + +Revision 1.2 2005/05/05 23:03:46 agriffis +Fix indentation (and some output as well) + +Revision 1.1 2005/05/05 22:05:34 agriffis +first pass at Gentoo Foundation voting program + +# vim:sw=4 et diff --git a/trustees2009/ballot-trustees2009 b/trustees2009/ballot-trustees2009 new file mode 100644 index 0000000..eba0fc2 --- /dev/null +++ b/trustees2009/ballot-trustees2009 @@ -0,0 +1,4 @@ +musikc +Patrick +quantumsummers +robbat2 diff --git a/trustees2009/officials-trustees2009 b/trustees2009/officials-trustees2009 new file mode 100644 index 0000000..17a74fc --- /dev/null +++ b/trustees2009/officials-trustees2009 @@ -0,0 +1,4 @@ +jmbsvicetto +rane +antarus +root diff --git a/trustees2009/start-trustees2009 b/trustees2009/start-trustees2009 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/trustees2009/start-trustees2009 diff --git a/trustees2009/stop-trustees2009 b/trustees2009/stop-trustees2009 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/trustees2009/stop-trustees2009 diff --git a/trustees2009/voters-trustees2009 b/trustees2009/voters-trustees2009 new file mode 100644 index 0000000..0e05965 --- /dev/null +++ b/trustees2009/voters-trustees2009 @@ -0,0 +1,141 @@ +a3li +aballier +agaffney +ali_bush +amne +anant +antarus +araujo +armin76 +bangert +bass +beandog +betelgeuse +bicatali +blackace +bluebird +calchan +cam +cardoe +carlo +caster +cedk +chainsaw +chiguire +coldwind +compnerd +corsair +cryos +dang +darkside +dberkholz +deathwing00 +dertobi123 +desultory +dev-zero +dirtyepic +dmwaters +dostrow +dragonheart +dsd +eradicator +fauli +ferdy +flameeyes +flammie +fmccor +ford_prefect +fox2mike +fuzzyray +g2boojum +ge +genstef +gentoofan23 +graaff +grahl +gregkh +grobian +halcy0n +hattya +hollow +hparker +ian +iluxa +jaervosz +je_fro +jer +jkt +jmbsvicetto +joker +jrinkovs +kallamej +kanaka +keri +killerfox +klieber +kumba +lack +leonardop +lu_zero +mabi +maedhros +mark_alec +markusle +moloh +mpagano +mr_bones_ +mrness +musikc +neddyseagoon +nelchael +nerdboy +neysx +nichoj +nightmorph +nixnut +nixphoeni +nyhm +omp +patrick +pauldv +pilla +pjp +pva +pvdabeel +pythonhead +quantumsummers +radek +rajiv +ramereth +rane +ranger +rbu +remi +ribosome +rich0 +rl03 +robbat2 +solar +spock +steev +stefaan +swift +tantive +tester +tgall +the_paya +tove +tsunam +tupone +ulm +vapier +vorlon +wormo +yoswink +yvasilev +zzam +ciaranm +david +jay +philantrop +seemant @@ -0,0 +1,220 @@ +#!/usr/bin/perl -w +# $Id: votify,v 1.5 2005/05/16 04:03:46 agriffis Exp $ +# +# Copyright 2005 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# votify: generate, verify and submit voting ballots for trustee elections +# + +#BEGIN { push @INC, (getpwnam 'fox2mike')[7].'/elections' } +BEGIN { push @INC, '/etc/elections/current' } + +use POSIX; +use Getopt::Long; +use List::Util; +use Votify 'user'; +use strict; + +###################################################################### +# Global vars +###################################################################### + +(my $zero = $0) =~ s,.*/,,; +(my $version = '$Revision: 1.5 $') =~ s/.*?(\d.*\d).*/$zero version $1\n/; +my (%opt); + +# Collect the open elections +my (@open_elections, $usage_elections); +opendir(D, "$Votify::datadir/") or die; +@open_elections = sort grep { + s/^start-// and do { + my ($starttime) = (stat _)[9] if stat("$Votify::datadir/start-$_"); + my ($stoptime) = (stat _)[9] if stat("$Votify::datadir/stop-$_"); + ((not defined $starttime or $starttime < time) and + (not defined $stoptime or $stoptime > time)) + } +} readdir D; +closedir D; +if (@open_elections) { + $usage_elections = join("\n ", @open_elections); +} else { + $usage_elections = "(no elections currently open)"; +} + +my $usage = <<EOT; + +usage: $zero <command> <election> + +where <command> is one of: + + --new Generate a new ~/.ballot-<election> for editing + --verify Verify content of ~/.ballot-<election> + --submit Submit ~/.ballot-<election> to be counted + --help Show this help message + --version Show version information + +and <election> is one of the elections currently in-progress. The following +elections are currently open: + + $usage_elections + +Instructions: + +(1) Create a new ballot with the --new command. This generates a file in your + home directory called ~/.ballot-<election>. The file contains a shuffled + listing of the candidates. Note that the permissions on the file are set so + that only you have read+write permissions. + + \$ $zero --new <election> + +(2) Edit ~/.ballot-<election> and rearrange the candidates linewise in order of + preference. Candidates you consider equal can be listed on the same line. + Any candidates you omit are implied on the last line. For example, if tom, + jerry, sam, linford and karin are running, you could put: + + karin + linford jerry + + In this case, you prefer karin over everybody else. You prefer linford and + jerry over tom and sam. + + The file format is case-insensitive, ignores empty lines and comments that + start with the hash '#' symbol. + +(3) Verify your choices. The $zero program will explain in English if it + appears that you've made any errors in your ballot. + + \$ $zero --verify <election> + +(4) Submit your ballot. This renames your ballot to + ~/.ballot-<election>-submitted so that it will be tallied when the votes + are collected. + + \$ $zero --submit <election> + +EOT + +###################################################################### +# Main +###################################################################### + +package main; + +# Make sure umask is secure before we do anything +umask 077; + +# Parse the options on the cmdline. Put the short versions first in +# each optionstring so that the hash keys are created using the short +# versions. For example, use 'q|qar', not 'qar|q'. +my ($result) = GetOptions( + \%opt, + 'new', # generate new ~/.ballot-<election> + 'verify', # verify content of ~/.ballot-<election> + 'submit', # rename ~/.ballot to ~/.ballot-<election>-submitted + 'help', # help message + 'version', # version information +); +if ($opt{'help'} or not %opt) { print STDERR $usage; exit 0 } +if ($opt{'version'}) { print STDERR $version; exit 0 } +die "$zero: only one command allowed; use --help for help\n" if 1 < keys %opt; +die "$zero: election required; use --help for help\n" unless @ARGV == 1; + +my ($election) = $ARGV[0]; +my ($b) = Ballot->new($election); + +# Check if the election is open. This should really happen in an +# Election class in Votify.pm eventually +my ($starttime, $stoptime); +if (stat("$Votify::datadir/start-$election")) { $starttime = (stat _)[9] } +if (stat("$Votify::datadir/stop-$election")) { $stoptime = (stat _)[9] } +if ($starttime && $starttime > time) { + print "\n", "*" x 75, "\n"; + print "WARNING: Specified election doesn't start until ", + scalar(localtime $starttime), "\n"; + print "*" x 75, "\n"; +} +if ($stoptime && $stoptime < time) { + print "\n", "*" x 75, "\n"; + print "WARNING: Specified election ended at ", + scalar(localtime $stoptime), "\n"; + print "*" x 75, "\n"; +} + +if ($opt{'new'}) { + # Make sure the user is eligible + open(F, "<$Votify::datadir/voters-$election") or die "Failed to open voters file"; + my (@voters) = <F>; + chomp(@voters); + close(F); + + unless (grep { $_ eq getpwuid($>) } @voters) { + print STDERR <<EOT; + +I'm sorry, you are not a registered voter for this election. Please +contact one of the election officials if you feel this is in error, +either on IRC or in email. + +EOT + exit 1; + } + + $b->populate(); + $b->write(); + + # Let the user know what we did + print <<EOF; + +Welcome to the $election election! Your ballot has been written to + + $b->{filename} + +Please edit this file with your preferred editor. Additional instructions can +be read at the top of the ballot. + +EOF + exit 0; +} + +if ($opt{'verify'} or $opt{'submit'}) { + $b->read(); + $b->verify(); + if ($opt{'verify'}) { + print <<EOF; +Your ballot checks out ok, congratulations! Your next step should be + + $zero --submit $election + +EOF + exit 0; + } + + my ($s) = $b->{'filename'}.'-submitted'; + die("File already exists, please remove $s") if -e $s; + rename($b->{'default_filename'}, $s) or die("Couldn't rename $b->{default_filename}"); + print <<EOF; +Your ballot has been renamed to $s +so that it will be counted when the election is over. Please do not remove your +ballot until the election results have been announced. Thank you for +participating in the $election election! + +EOF + exit 0; +} + +__END__ + +$Log: votify,v $ +Revision 1.5 2005/05/16 04:03:46 agriffis +add first pass at countify --rank + +Revision 1.3 2005/05/09 23:12:02 agriffis +Add support for registered voters + +Revision 1.2 2005/05/05 23:03:46 agriffis +Fix indentation (and some output as well) + +Revision 1.1 2005/05/05 22:05:34 agriffis +first pass at Gentoo Foundation voting program + +# vim:sw=4 et |