#!/usr/bin/perl -w
+#
+# usage:
+# normalise [NORM-OPTIONS...] [--] INPUT-FILES...
+#
+# NORM-OPTIONS are
+# +OPTNAME[=OPTVAL] Election option
+# CAND=[DESCRIPTION]
+# +CAND+OPTNAME[=OPTVAL] Candidate option
+# -- End of options to normalise
+# -... Reserved for future options to normalise
use strict;
-our @options, @candiates, @voters;
+our @options;
+our %candidates; # $candidates{CAND}{Desc}, {Opts}[]
+our %candref; # $candref{CAND} => msg of why needed
+our @ballots;
-our %seen_cand, %need_cand;
+my $candvoter_re = '\w+';
+my $cands_re = '\w+(?,\w+)*';
+my $opt_re = '\w+(?:=\S*)?';
-my $candvoter_re = '[^\000-\037!"#$%()*+,/0-\136`-\177]+';
+sub badinput ($) {
+ die "bad input: $_[0]";
+}
-sub normalise_opts ($) {
- my ($os) = @_;
+sub normalise_opts_list ($$) {
+ # $ctx is one of Election Candidate Ballot
+ my ($os,$ctx) = @_;
+ $os //= '';
my @o;
foreach my $o (split /\s+/, $os) {
if ($o =~ m/^\w+$/) {
push @o, $&;
+ } elsif ($o =~ s/^_[Tt]ie\=//) {
+ $o =~ m/^($cands_re)([<>])($cands_re)$/
+ or badinput "bad value \`$_' for tie option";
+ my ($l,$op,$r) = ($1,$2,$3);
+ ($l,$op,$r) = ($r,'>',$l) if $op eq '<';
+ $candref{$_} = "tie break spec" foreach $o =~ m/\w+/g;
+ $l = join ',', sort split /\,/, $l;
+ $r = join ',', sort split /\,/, $r;
+ $l =~ m/\b$_\b/ and badinput "reflexive tie"
+ foreach split /\,/, $r;
+ push @o, "$l$op$r";
} elsif ($o =~ m/^\w+\=\S+$/) {
- push @o, $&;S
- } elseif ($o !~ m/\S/) {
+ push @o, $&;
+ } elsif ($o !~ m/\S/) {
} else {
badinput "bad option \`$o'";
}
}
- return @o ? " | @o" : "";
+ return @o;
+}
+
+sub normalise_opts ($$) {
+ my ($os,$ctx) = @_;
+ my @o = normalise_opts_list $os, $ctx;
+ return " | @o";
+}
+
+sub setcanddesc ($$) {
+ my ($cand,$desc) = @_;
+
+ if (length $desc) {
+ badinput "multiple descriptions for $cand" if
+ defined $candidates{$cand}{Desc};
+ $candidates{$cand}{Desc} = $desc;
+ }
+}
+
+while (@ARGV) {
+ $_ = shift @ARGV;
+ if (m/^--$/) {
+ last;
+ } elsif (m/^(\w+)=([^|]+)$/) {
+ setcanddesc $1, $2;
+ } elsif (m/^\+($opt_re)$/) {
+ push @options, $1;
+ } elsif (m/^\+(\w+)\+($opt_re)$/) {
+ push @{ $candidates{$1}{Opts} }, $2;
+ } elsif (m/^-/) {
+ die "unknown normalise option \`$_'\n";
+ } else {
+ # oh!
+ unshift @ARGV, $_;
+ last;
+ }
}
while (<>) {
next if m/^\#/;
s/^\s+//;
s/\s+$//;
- if (m/^\|\s*(\w+(?:\=\S+)?)$/) {
- push @options, "| $1";
- } elsif (m/^($candvoter_re?)\s*=\s*([^|]+?)\s*|(.*)?$/) {
+ if (m/^\|/) {
+ push @options, normalise_opts_list $', 'Election';
+ } elsif (m/^($candvoter_re?)\s*=\s*([^|]+?)\s*(?:\|(.*))?$/o) {
+ use Data::Dumper;
+#print STDERR Dumper($1,$2,$3);
my ($cand,$desc,$opts) = ($1,$2,$3);
- $desc=$cand unless length $desc;
- $opts = normalise_opts $opts;
- push @candidates, "$cand = $desc".$opts;
- } elsif (m/^($candvoter_re?)?\s*\:/) {
-
+ push @{ $candidates{$cand}{Opts} }, normalise_opts $opts, 'Candidate';
+ setcanddesc $cand, $desc;
+ } elsif (m/^($candvoter_re?)?\s*\:([^|]*)(?:\|(.*))?$/) {
+ my ($voter,$opts) = ($1,$3);
+ my @p;
+ foreach my $p (split /\s+/, $2) {
+ if ($p =~ m/^\w+(?:\=\w+)*$/) {
+ push @p, $&;
+ $candidates{$_} //= { } foreach $p =~ m/\w+/g;
+ } elsif ($p eq '') {
+ # empty entry can only happen if voter casts no prefs at all
+ } else {
+ badinput "bad vote preference \`$p'";
+ }
+ }
+ push @ballots, "$voter : @p".normalise_opts $opts, 'Ballot';
+ } elsif (m/^\.$/) {
+ } else {
+ badinput "unknown line format \`$_'";
+ }
+}
+
+print "| @options\n" or die $!;
+
+foreach my $cand (sort keys %candref) {
+ badinput "missing candidate $cand (ref. by $candref{$cand}"
+ unless defined $candidates{$cand};
+}
+
+foreach my $cand (sort keys %candidates) {
+ my $c = $candidates{$cand};
+ $c->{Desc} //= $cand;
+ $c->{Opts} //= [ ];
+ my $opts = $c->{Opts};
+ print "$cand = $c->{Desc} | @$opts\n" or die $!;
+}
+
+sub vsortkey { $_[0] =~ m/:/; return "$' : $`"; }
+
+print $_,"\n" or die $! foreach
+ sort { vsortkey($a) cmp vsortkey($b) } @ballots;
+
+print ".\n" or die $!;