Add subcommand for BLT/CSP file conversion
This commit is contained in:
parent
e7bae376e9
commit
88ab06d633
120
src/cli/convert.rs
Normal file
120
src/cli/convert.rs
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/* OpenTally: Open-source election vote counting
|
||||||
|
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::election::Election;
|
||||||
|
use crate::numbers::Rational;
|
||||||
|
use crate::parser;
|
||||||
|
use crate::writer;
|
||||||
|
|
||||||
|
use clap::{AppSettings, Clap};
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
/// Convert between different ballot data formats
|
||||||
|
#[derive(Clap)]
|
||||||
|
#[clap(setting=AppSettings::DeriveDisplayOrder)]
|
||||||
|
pub struct SubcmdOptions {
|
||||||
|
/// Path to the input data file
|
||||||
|
#[clap(help_heading=Some("INPUT/OUTPUT"))]
|
||||||
|
infile: String,
|
||||||
|
|
||||||
|
/// Path to the output data file
|
||||||
|
#[clap(help_heading=Some("INPUT/OUTPUT"))]
|
||||||
|
outfile: String,
|
||||||
|
|
||||||
|
/// Format of input file
|
||||||
|
#[clap(help_heading=Some("INPUT/OUTPUT"), short, long, possible_values=&["blt", "csp"], value_name="format")]
|
||||||
|
r#in: Option<String>,
|
||||||
|
|
||||||
|
/// Format of output file
|
||||||
|
#[clap(help_heading=Some("INPUT/OUTPUT"), short, long, possible_values=&["blt", "csp"], value_name="format")]
|
||||||
|
out: Option<String>,
|
||||||
|
|
||||||
|
/// Number of seats
|
||||||
|
#[clap(help_heading=Some("ELECTION SPECIFICATION"), long)]
|
||||||
|
seats: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entrypoint for subcommand
|
||||||
|
pub fn main(mut cmd_opts: SubcmdOptions) -> Result<(), i32> {
|
||||||
|
// Auto-detect input/output formats
|
||||||
|
if cmd_opts.r#in == None {
|
||||||
|
if cmd_opts.infile.ends_with(".blt") {
|
||||||
|
cmd_opts.r#in = Some("blt".to_string());
|
||||||
|
} else if cmd_opts.infile.ends_with(".csp") {
|
||||||
|
cmd_opts.r#in = Some("csp".to_string());
|
||||||
|
} else {
|
||||||
|
println!("Error: --in not specified and format cannot be determined from input filename");
|
||||||
|
return Err(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cmd_opts.out == None {
|
||||||
|
if cmd_opts.outfile.ends_with(".blt") {
|
||||||
|
cmd_opts.out = Some("blt".to_string());
|
||||||
|
} else if cmd_opts.outfile.ends_with(".csp") {
|
||||||
|
cmd_opts.out = Some("csp".to_string());
|
||||||
|
} else {
|
||||||
|
println!("Error: --out not specified and format cannot be determined from output filename");
|
||||||
|
return Err(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read input file
|
||||||
|
let election: Election<Rational>;
|
||||||
|
|
||||||
|
match cmd_opts.r#in.as_deref().unwrap() {
|
||||||
|
"blt" => {
|
||||||
|
match parser::blt::parse_path(cmd_opts.infile) {
|
||||||
|
Ok(e) => {
|
||||||
|
election = e;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Syntax Error: {}", err);
|
||||||
|
return Err(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"csp" => {
|
||||||
|
match cmd_opts.seats {
|
||||||
|
Some(seats) => {
|
||||||
|
let file = File::open(cmd_opts.infile).expect("IO Error");
|
||||||
|
election = parser::csp::parse_reader(file, seats);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("Error: --seats must be specified with CSP input");
|
||||||
|
return Err(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write output file
|
||||||
|
let output = File::create(cmd_opts.outfile).expect("IO Error");
|
||||||
|
|
||||||
|
match cmd_opts.out.as_deref().unwrap() {
|
||||||
|
"blt" => {
|
||||||
|
writer::blt::write(election, output);
|
||||||
|
}
|
||||||
|
"csp" => {
|
||||||
|
writer::csp::write(election, output);
|
||||||
|
}
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
21
src/cli/mod.rs
Normal file
21
src/cli/mod.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/* OpenTally: Open-source election vote counting
|
||||||
|
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Convert between different ballot data formats
|
||||||
|
pub mod convert;
|
||||||
|
/// Count a single transferable vote (STV) election
|
||||||
|
pub mod stv;
|
369
src/cli/stv.rs
Normal file
369
src/cli/stv.rs
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
/* OpenTally: Open-source election vote counting
|
||||||
|
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::constraints::Constraints;
|
||||||
|
use crate::election::{CandidateState, CountState, Election};
|
||||||
|
use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
|
||||||
|
use crate::parser::blt;
|
||||||
|
use crate::stv::{self, STVOptions};
|
||||||
|
use crate::ties;
|
||||||
|
|
||||||
|
use clap::{AppSettings, Clap};
|
||||||
|
|
||||||
|
use std::cmp::max;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{self, BufRead};
|
||||||
|
use std::ops;
|
||||||
|
|
||||||
|
/// Count a single transferable vote (STV) election
|
||||||
|
#[derive(Clap)]
|
||||||
|
#[clap(setting=AppSettings::DeriveDisplayOrder)]
|
||||||
|
pub struct SubcmdOptions {
|
||||||
|
// ----------------
|
||||||
|
// -- File input --
|
||||||
|
|
||||||
|
/// Path to the BLT file to be counted
|
||||||
|
filename: String,
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
// -- Numbers settings --
|
||||||
|
|
||||||
|
/// Numbers mode
|
||||||
|
#[clap(help_heading=Some("NUMBERS"), short, long, possible_values=&["rational", "fixed", "gfixed", "float64"], default_value="rational", value_name="mode")]
|
||||||
|
numbers: String,
|
||||||
|
|
||||||
|
/// Decimal places if --numbers fixed
|
||||||
|
#[clap(help_heading=Some("NUMBERS"), long, default_value="5", value_name="dps")]
|
||||||
|
decimals: usize,
|
||||||
|
|
||||||
|
/// Convert ballots with value >1 to multiple ballots of value 1
|
||||||
|
#[clap(help_heading=Some("NUMBERS"), long)]
|
||||||
|
normalise_ballots: bool,
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// -- Rounding settings --
|
||||||
|
|
||||||
|
/// Round surplus fractions to specified decimal places
|
||||||
|
#[clap(help_heading=Some("ROUNDING"), long, alias="round-tvs", value_name="dps")]
|
||||||
|
round_surplus_fractions: Option<usize>,
|
||||||
|
|
||||||
|
/// Round ballot values to specified decimal places
|
||||||
|
#[clap(help_heading=Some("ROUNDING"), long, alias="round-weights", value_name="dps")]
|
||||||
|
round_values: Option<usize>,
|
||||||
|
|
||||||
|
/// Round votes to specified decimal places
|
||||||
|
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||||
|
round_votes: Option<usize>,
|
||||||
|
|
||||||
|
/// Round quota to specified decimal places
|
||||||
|
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||||
|
round_quota: Option<usize>,
|
||||||
|
|
||||||
|
/// (Gregory STV) How to calculate votes to credit to candidates in surplus transfers
|
||||||
|
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["by_value", "per_ballot"], default_value="by_value", value_name="mode")]
|
||||||
|
sum_surplus_transfers: String,
|
||||||
|
|
||||||
|
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
||||||
|
#[clap(help_heading=Some("ROUNDING"), long, default_value="0.001%", value_name="tolerance")]
|
||||||
|
meek_surplus_tolerance: String,
|
||||||
|
|
||||||
|
// -----------
|
||||||
|
// -- Quota --
|
||||||
|
|
||||||
|
/// Quota type
|
||||||
|
#[clap(help_heading=Some("QUOTA"), short, long, possible_values=&["droop", "hare", "droop_exact", "hare_exact"], default_value="droop")]
|
||||||
|
quota: String,
|
||||||
|
|
||||||
|
/// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota
|
||||||
|
#[clap(help_heading=Some("QUOTA"), short='c', long, possible_values=&["geq", "gt"], default_value="gt", value_name="criterion")]
|
||||||
|
quota_criterion: String,
|
||||||
|
|
||||||
|
/// Whether to apply a form of progressive quota
|
||||||
|
#[clap(help_heading=Some("QUOTA"), long, possible_values=&["static", "ers97", "ers76", "dynamic_by_total", "dynamic_by_active"], default_value="static", value_name="mode")]
|
||||||
|
quota_mode: String,
|
||||||
|
|
||||||
|
// ------------------
|
||||||
|
// -- STV variants --
|
||||||
|
|
||||||
|
/// Tie-breaking method
|
||||||
|
#[clap(help_heading=Some("STV VARIANTS"), short='t', long, possible_values=&["forwards", "backwards", "random", "prompt"], default_value="prompt", value_name="methods")]
|
||||||
|
ties: Vec<String>,
|
||||||
|
|
||||||
|
/// Random seed to use with --ties random
|
||||||
|
#[clap(help_heading=Some("STV VARIANTS"), long, value_name="seed")]
|
||||||
|
random_seed: Option<String>,
|
||||||
|
|
||||||
|
/// Method of surplus distributions
|
||||||
|
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek", "cincinnati", "hare"], default_value="wig", value_name="method")]
|
||||||
|
surplus: String,
|
||||||
|
|
||||||
|
/// (Gregory STV) Order to distribute surpluses
|
||||||
|
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["by_size", "by_order"], default_value="by_size", value_name="order")]
|
||||||
|
surplus_order: String,
|
||||||
|
|
||||||
|
/// (Gregory STV) Examine only transferable papers during surplus distributions
|
||||||
|
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
||||||
|
transferable_only: bool,
|
||||||
|
|
||||||
|
/// (Gregory STV) Method of exclusions
|
||||||
|
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "by_source", "parcels_by_order", "wright"], default_value="single_stage", value_name="method")]
|
||||||
|
exclusion: String,
|
||||||
|
|
||||||
|
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
|
||||||
|
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
||||||
|
meek_nz_exclusion: bool,
|
||||||
|
|
||||||
|
/// (Cincinnati/Hare) Method of drawing a sample
|
||||||
|
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["stratified", "by_order", "nth_ballot"], default_value="stratified", value_name="method")]
|
||||||
|
sample: String,
|
||||||
|
|
||||||
|
/// (Cincinnati/Hare) Sample-based methods: Check for candidate election after each individual ballot paper transfer
|
||||||
|
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
||||||
|
sample_per_ballot: bool,
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// -- Count optimisations --
|
||||||
|
|
||||||
|
/// Continue count even if continuing candidates fill all remaining vacancies
|
||||||
|
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||||
|
no_early_bulk_elect: bool,
|
||||||
|
|
||||||
|
/// Use bulk exclusion
|
||||||
|
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||||
|
bulk_exclude: bool,
|
||||||
|
|
||||||
|
/// Defer surplus distributions if possible
|
||||||
|
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||||
|
defer_surpluses: bool,
|
||||||
|
|
||||||
|
/// Elect candidates only when their surpluses are distributed; (Meek STV) Wait for keep values to converge before electing candidates
|
||||||
|
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||||
|
no_immediate_elect: bool,
|
||||||
|
|
||||||
|
/// On exclusion, exclude any candidate with fewer than this many votes
|
||||||
|
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long, default_value="0", value_name="votes")]
|
||||||
|
min_threshold: String,
|
||||||
|
|
||||||
|
// -----------------
|
||||||
|
// -- Constraints --
|
||||||
|
|
||||||
|
/// Path to a CON file specifying constraints
|
||||||
|
#[clap(help_heading=Some("CONSTRAINTS"), long)]
|
||||||
|
constraints: Option<String>,
|
||||||
|
|
||||||
|
/// Mode of handling constraints
|
||||||
|
#[clap(help_heading=Some("CONSTRAINTS"), long, possible_values=&["guard_doom"], default_value="guard_doom")]
|
||||||
|
constraint_mode: String,
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
// -- Display settings --
|
||||||
|
|
||||||
|
/// Hide excluded candidates from results report
|
||||||
|
#[clap(help_heading=Some("DISPLAY"), long)]
|
||||||
|
hide_excluded: bool,
|
||||||
|
|
||||||
|
/// Sort candidates by votes in results report
|
||||||
|
#[clap(help_heading=Some("DISPLAY"), long)]
|
||||||
|
sort_votes: bool,
|
||||||
|
|
||||||
|
/// Print votes to specified decimal places in results report
|
||||||
|
#[clap(help_heading=Some("DISPLAY"), long, default_value="2", value_name="dps")]
|
||||||
|
pp_decimals: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entrypoint for subcommand
|
||||||
|
pub fn main(cmd_opts: SubcmdOptions) -> Result<(), i32> {
|
||||||
|
// Read and count election according to --numbers
|
||||||
|
if cmd_opts.numbers == "rational" {
|
||||||
|
let mut election = election_from_file(&cmd_opts.filename)?;
|
||||||
|
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||||
|
|
||||||
|
// Must specify ::<N> here and in a few other places because ndarray causes E0275 otherwise
|
||||||
|
count_election::<Rational>(election, cmd_opts)?;
|
||||||
|
} else if cmd_opts.numbers == "float64" {
|
||||||
|
let mut election = election_from_file(&cmd_opts.filename)?;
|
||||||
|
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||||
|
count_election::<NativeFloat64>(election, cmd_opts)?;
|
||||||
|
} else if cmd_opts.numbers == "fixed" {
|
||||||
|
Fixed::set_dps(cmd_opts.decimals);
|
||||||
|
|
||||||
|
let mut election = election_from_file(&cmd_opts.filename)?;
|
||||||
|
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||||
|
count_election::<Fixed>(election, cmd_opts)?;
|
||||||
|
} else if cmd_opts.numbers == "gfixed" {
|
||||||
|
GuardedFixed::set_dps(cmd_opts.decimals);
|
||||||
|
|
||||||
|
let mut election = election_from_file(&cmd_opts.filename)?;
|
||||||
|
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||||
|
count_election::<GuardedFixed>(election, cmd_opts)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn election_from_file<N: Number>(path: &str) -> Result<Election<N>, i32> {
|
||||||
|
match blt::parse_path(path) {
|
||||||
|
Ok(e) => return Ok(e),
|
||||||
|
Err(err) => {
|
||||||
|
println!("Syntax Error: {}", err);
|
||||||
|
return Err(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_load_constraints<N: Number>(election: &mut Election<N>, constraints: &Option<String>) {
|
||||||
|
if let Some(c) = constraints {
|
||||||
|
let file = File::open(c).expect("IO Error");
|
||||||
|
let lines = io::BufReader::new(file).lines();
|
||||||
|
election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_election<N: Number>(mut election: Election<N>, cmd_opts: SubcmdOptions) -> Result<(), i32>
|
||||||
|
where
|
||||||
|
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Neg<Output=N>
|
||||||
|
{
|
||||||
|
// Copy applicable options
|
||||||
|
let opts = STVOptions::new(
|
||||||
|
cmd_opts.round_surplus_fractions,
|
||||||
|
cmd_opts.round_values,
|
||||||
|
cmd_opts.round_votes,
|
||||||
|
cmd_opts.round_quota,
|
||||||
|
cmd_opts.sum_surplus_transfers.into(),
|
||||||
|
cmd_opts.meek_surplus_tolerance.into(),
|
||||||
|
cmd_opts.normalise_ballots,
|
||||||
|
cmd_opts.quota.into(),
|
||||||
|
cmd_opts.quota_criterion.into(),
|
||||||
|
cmd_opts.quota_mode.into(),
|
||||||
|
ties::from_strs(cmd_opts.ties, cmd_opts.random_seed),
|
||||||
|
cmd_opts.surplus.into(),
|
||||||
|
cmd_opts.surplus_order.into(),
|
||||||
|
cmd_opts.transferable_only,
|
||||||
|
cmd_opts.exclusion.into(),
|
||||||
|
cmd_opts.meek_nz_exclusion,
|
||||||
|
cmd_opts.sample.into(),
|
||||||
|
cmd_opts.sample_per_ballot,
|
||||||
|
!cmd_opts.no_early_bulk_elect,
|
||||||
|
cmd_opts.bulk_exclude,
|
||||||
|
cmd_opts.defer_surpluses,
|
||||||
|
!cmd_opts.no_immediate_elect,
|
||||||
|
cmd_opts.min_threshold,
|
||||||
|
cmd_opts.constraints,
|
||||||
|
cmd_opts.constraint_mode.into(),
|
||||||
|
cmd_opts.hide_excluded,
|
||||||
|
cmd_opts.sort_votes,
|
||||||
|
cmd_opts.pp_decimals,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validate options
|
||||||
|
match opts.validate() {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Error: {}", err.describe());
|
||||||
|
return Err(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Describe count
|
||||||
|
let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value });
|
||||||
|
print!("Count computed by OpenTally (revision {}). Read {:.0} ballots from \"{}\" for election \"{}\". There are {} candidates for {} vacancies. ", crate::VERSION, total_ballots, cmd_opts.filename, election.name, election.candidates.len(), election.seats);
|
||||||
|
let opts_str = opts.describe::<N>();
|
||||||
|
if opts_str.len() > 0 {
|
||||||
|
println!("Counting using options \"{}\".", opts_str);
|
||||||
|
} else {
|
||||||
|
println!("Counting using default options.");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Normalise ballots if requested
|
||||||
|
if cmd_opts.normalise_ballots {
|
||||||
|
election.normalise_ballots();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise count state
|
||||||
|
let mut state = CountState::new(&election);
|
||||||
|
|
||||||
|
// Distribute first preferences
|
||||||
|
match stv::count_init(&mut state, &opts) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Error: {}", err.describe());
|
||||||
|
return Err(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stage_num = 1;
|
||||||
|
print_stage(stage_num, &state, &opts);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match stv::count_one_stage(&mut state, &opts) {
|
||||||
|
Ok(is_done) => {
|
||||||
|
if is_done {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Error: {}", err.describe());
|
||||||
|
return Err(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage_num += 1;
|
||||||
|
print_stage(stage_num, &state, &opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Count complete. The winning candidates are, in order of election:");
|
||||||
|
|
||||||
|
let mut winners = Vec::new();
|
||||||
|
for (candidate, count_card) in state.candidates.iter() {
|
||||||
|
if count_card.state == CandidateState::Elected {
|
||||||
|
winners.push((candidate, count_card));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
winners.sort_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
|
||||||
|
|
||||||
|
for (i, (winner, count_card)) in winners.into_iter().enumerate() {
|
||||||
|
if let Some(kv) = &count_card.keep_value {
|
||||||
|
println!("{}. {} (kv = {:.dps2$})", i + 1, winner.name, kv, dps2=max(opts.pp_decimals, 2));
|
||||||
|
} else {
|
||||||
|
println!("{}. {}", i + 1, winner.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_stage<N: Number>(stage_num: usize, state: &CountState<N>, opts: &STVOptions) {
|
||||||
|
// Print stage details
|
||||||
|
match state.kind {
|
||||||
|
None => { println!("{}. {}", stage_num, state.title); }
|
||||||
|
Some(kind) => { println!("{}. {} {}", stage_num, kind, state.title); }
|
||||||
|
};
|
||||||
|
println!("{}", state.logger.render().join(" "));
|
||||||
|
|
||||||
|
// Print candidates
|
||||||
|
print!("{}", state.describe_candidates(opts));
|
||||||
|
|
||||||
|
// Print summary rows
|
||||||
|
print!("{}", state.describe_summary(opts));
|
||||||
|
|
||||||
|
println!("");
|
||||||
|
}
|
@ -18,22 +18,11 @@
|
|||||||
use crate::constraints::{Constraints, ConstraintMatrix};
|
use crate::constraints::{Constraints, ConstraintMatrix};
|
||||||
use crate::logger::Logger;
|
use crate::logger::Logger;
|
||||||
use crate::numbers::Number;
|
use crate::numbers::Number;
|
||||||
use crate::parser::blt::{BLTParser, ParseError};
|
|
||||||
use crate::sharandom::SHARandom;
|
use crate::sharandom::SHARandom;
|
||||||
use crate::stv::{QuotaMode, STVOptions};
|
use crate::stv::{QuotaMode, STVOptions};
|
||||||
|
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::iter::Peekable;
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use utf8_chars::BufReadCharsExt;
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use std::fs::File;
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use std::io::BufReader;
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
/// An election to be counted
|
/// An election to be counted
|
||||||
pub struct Election<N> {
|
pub struct Election<N> {
|
||||||
@ -52,21 +41,6 @@ pub struct Election<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<N: Number> Election<N> {
|
impl<N: Number> Election<N> {
|
||||||
/// Parse the given BLT file and return an [Election]
|
|
||||||
pub fn from_blt<I: Iterator<Item=char>>(input: Peekable<I>) -> Result<Self, ParseError> {
|
|
||||||
let mut parser = BLTParser::new(input);
|
|
||||||
parser.parse_blt()?;
|
|
||||||
return Ok(parser.as_election());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse the BLT file at the given path and return an [Election]
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ParseError> {
|
|
||||||
let mut reader = BufReader::new(File::open(path).expect("IO Error"));
|
|
||||||
let chars = reader.chars().map(|r| r.expect("IO Error")).peekable();
|
|
||||||
return Ok(Election::from_blt(chars)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert ballots with weight >1 to multiple ballots of weight 1
|
/// Convert ballots with weight >1 to multiple ballots of weight 1
|
||||||
pub fn normalise_ballots(&mut self) {
|
pub fn normalise_ballots(&mut self) {
|
||||||
let mut normalised_ballots = Vec::new();
|
let mut normalised_ballots = Vec::new();
|
||||||
|
11
src/lib.rs
11
src/lib.rs
@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
//! Open source counting software for various preferential voting election systems
|
//! Open source counting software for various preferential voting election systems
|
||||||
|
|
||||||
/// File parsers
|
|
||||||
pub mod parser;
|
|
||||||
/// Data types and logic for constraints on elections
|
/// Data types and logic for constraints on elections
|
||||||
pub mod constraints;
|
pub mod constraints;
|
||||||
/// Data types for representing abstract elections
|
/// Data types for representing abstract elections
|
||||||
@ -29,12 +27,21 @@ pub mod election;
|
|||||||
pub mod logger;
|
pub mod logger;
|
||||||
/// Implementations of different numeric representations
|
/// Implementations of different numeric representations
|
||||||
pub mod numbers;
|
pub mod numbers;
|
||||||
|
/// File parsers
|
||||||
|
pub mod parser;
|
||||||
/// Deterministic random number generation using SHA256
|
/// Deterministic random number generation using SHA256
|
||||||
pub mod sharandom;
|
pub mod sharandom;
|
||||||
/// STV counting logic
|
/// STV counting logic
|
||||||
pub mod stv;
|
pub mod stv;
|
||||||
/// Tie-breaking methods
|
/// Tie-breaking methods
|
||||||
pub mod ties;
|
pub mod ties;
|
||||||
|
/// File writers
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub mod writer;
|
||||||
|
|
||||||
|
/// CLI implementations
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub mod cli;
|
||||||
|
|
||||||
use git_version::git_version;
|
use git_version::git_version;
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
354
src/main.rs
354
src/main.rs
@ -15,18 +15,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use opentally::constraints::Constraints;
|
use opentally::cli;
|
||||||
use opentally::election::{CandidateState, CountState, Election};
|
|
||||||
use opentally::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
|
|
||||||
use opentally::stv::{self, STVOptions};
|
|
||||||
use opentally::ties;
|
|
||||||
|
|
||||||
use clap::{AppSettings, Clap};
|
use clap::Clap;
|
||||||
|
|
||||||
use std::cmp::max;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{self, BufRead};
|
|
||||||
use std::ops;
|
|
||||||
|
|
||||||
/// Open-source election vote counting
|
/// Open-source election vote counting
|
||||||
#[derive(Clap)]
|
#[derive(Clap)]
|
||||||
@ -38,163 +29,8 @@ struct Opts {
|
|||||||
|
|
||||||
#[derive(Clap)]
|
#[derive(Clap)]
|
||||||
enum Command {
|
enum Command {
|
||||||
STV(STV),
|
Convert(cli::convert::SubcmdOptions),
|
||||||
}
|
STV(cli::stv::SubcmdOptions),
|
||||||
|
|
||||||
/// Count a single transferable vote (STV) election
|
|
||||||
#[derive(Clap)]
|
|
||||||
#[clap(setting=AppSettings::DeriveDisplayOrder)]
|
|
||||||
struct STV {
|
|
||||||
// ----------------
|
|
||||||
// -- File input --
|
|
||||||
|
|
||||||
/// Path to the BLT file to be counted
|
|
||||||
filename: String,
|
|
||||||
|
|
||||||
// ----------------------
|
|
||||||
// -- Numbers settings --
|
|
||||||
|
|
||||||
/// Numbers mode
|
|
||||||
#[clap(help_heading=Some("NUMBERS"), short, long, possible_values=&["rational", "fixed", "gfixed", "float64"], default_value="rational", value_name="mode")]
|
|
||||||
numbers: String,
|
|
||||||
|
|
||||||
/// Decimal places if --numbers fixed
|
|
||||||
#[clap(help_heading=Some("NUMBERS"), long, default_value="5", value_name="dps")]
|
|
||||||
decimals: usize,
|
|
||||||
|
|
||||||
/// Convert ballots with value >1 to multiple ballots of value 1
|
|
||||||
#[clap(help_heading=Some("NUMBERS"), long)]
|
|
||||||
normalise_ballots: bool,
|
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// -- Rounding settings --
|
|
||||||
|
|
||||||
/// Round surplus fractions to specified decimal places
|
|
||||||
#[clap(help_heading=Some("ROUNDING"), long, alias="round-tvs", value_name="dps")]
|
|
||||||
round_surplus_fractions: Option<usize>,
|
|
||||||
|
|
||||||
/// Round ballot values to specified decimal places
|
|
||||||
#[clap(help_heading=Some("ROUNDING"), long, alias="round-weights", value_name="dps")]
|
|
||||||
round_values: Option<usize>,
|
|
||||||
|
|
||||||
/// Round votes to specified decimal places
|
|
||||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
|
||||||
round_votes: Option<usize>,
|
|
||||||
|
|
||||||
/// Round quota to specified decimal places
|
|
||||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
|
||||||
round_quota: Option<usize>,
|
|
||||||
|
|
||||||
/// (Gregory STV) How to calculate votes to credit to candidates in surplus transfers
|
|
||||||
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["by_value", "per_ballot"], default_value="by_value", value_name="mode")]
|
|
||||||
sum_surplus_transfers: String,
|
|
||||||
|
|
||||||
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
|
||||||
#[clap(help_heading=Some("ROUNDING"), long, default_value="0.001%", value_name="tolerance")]
|
|
||||||
meek_surplus_tolerance: String,
|
|
||||||
|
|
||||||
// -----------
|
|
||||||
// -- Quota --
|
|
||||||
|
|
||||||
/// Quota type
|
|
||||||
#[clap(help_heading=Some("QUOTA"), short, long, possible_values=&["droop", "hare", "droop_exact", "hare_exact"], default_value="droop")]
|
|
||||||
quota: String,
|
|
||||||
|
|
||||||
/// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota
|
|
||||||
#[clap(help_heading=Some("QUOTA"), short='c', long, possible_values=&["geq", "gt"], default_value="gt", value_name="criterion")]
|
|
||||||
quota_criterion: String,
|
|
||||||
|
|
||||||
/// Whether to apply a form of progressive quota
|
|
||||||
#[clap(help_heading=Some("QUOTA"), long, possible_values=&["static", "ers97", "ers76", "dynamic_by_total", "dynamic_by_active"], default_value="static", value_name="mode")]
|
|
||||||
quota_mode: String,
|
|
||||||
|
|
||||||
// ------------------
|
|
||||||
// -- STV variants --
|
|
||||||
|
|
||||||
/// Tie-breaking method
|
|
||||||
#[clap(help_heading=Some("STV VARIANTS"), short='t', long, possible_values=&["forwards", "backwards", "random", "prompt"], default_value="prompt", value_name="methods")]
|
|
||||||
ties: Vec<String>,
|
|
||||||
|
|
||||||
/// Random seed to use with --ties random
|
|
||||||
#[clap(help_heading=Some("STV VARIANTS"), long, value_name="seed")]
|
|
||||||
random_seed: Option<String>,
|
|
||||||
|
|
||||||
/// Method of surplus distributions
|
|
||||||
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek", "cincinnati", "hare"], default_value="wig", value_name="method")]
|
|
||||||
surplus: String,
|
|
||||||
|
|
||||||
/// (Gregory STV) Order to distribute surpluses
|
|
||||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["by_size", "by_order"], default_value="by_size", value_name="order")]
|
|
||||||
surplus_order: String,
|
|
||||||
|
|
||||||
/// (Gregory STV) Examine only transferable papers during surplus distributions
|
|
||||||
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
|
||||||
transferable_only: bool,
|
|
||||||
|
|
||||||
/// (Gregory STV) Method of exclusions
|
|
||||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "by_source", "parcels_by_order", "wright"], default_value="single_stage", value_name="method")]
|
|
||||||
exclusion: String,
|
|
||||||
|
|
||||||
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
|
|
||||||
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
|
||||||
meek_nz_exclusion: bool,
|
|
||||||
|
|
||||||
/// (Cincinnati/Hare) Method of drawing a sample
|
|
||||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["stratified", "by_order", "nth_ballot"], default_value="stratified", value_name="method")]
|
|
||||||
sample: String,
|
|
||||||
|
|
||||||
/// (Cincinnati/Hare) Sample-based methods: Check for candidate election after each individual ballot paper transfer
|
|
||||||
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
|
||||||
sample_per_ballot: bool,
|
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// -- Count optimisations --
|
|
||||||
|
|
||||||
/// Continue count even if continuing candidates fill all remaining vacancies
|
|
||||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
|
||||||
no_early_bulk_elect: bool,
|
|
||||||
|
|
||||||
/// Use bulk exclusion
|
|
||||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
|
||||||
bulk_exclude: bool,
|
|
||||||
|
|
||||||
/// Defer surplus distributions if possible
|
|
||||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
|
||||||
defer_surpluses: bool,
|
|
||||||
|
|
||||||
/// Elect candidates only when their surpluses are distributed; (Meek STV) Wait for keep values to converge before electing candidates
|
|
||||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
|
||||||
no_immediate_elect: bool,
|
|
||||||
|
|
||||||
/// On exclusion, exclude any candidate with fewer than this many votes
|
|
||||||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long, default_value="0", value_name="votes")]
|
|
||||||
min_threshold: String,
|
|
||||||
|
|
||||||
// -----------------
|
|
||||||
// -- Constraints --
|
|
||||||
|
|
||||||
/// Path to a CON file specifying constraints
|
|
||||||
#[clap(help_heading=Some("CONSTRAINTS"), long)]
|
|
||||||
constraints: Option<String>,
|
|
||||||
|
|
||||||
/// Mode of handling constraints
|
|
||||||
#[clap(help_heading=Some("CONSTRAINTS"), long, possible_values=&["guard_doom"], default_value="guard_doom")]
|
|
||||||
constraint_mode: String,
|
|
||||||
|
|
||||||
// ----------------------
|
|
||||||
// -- Display settings --
|
|
||||||
|
|
||||||
/// Hide excluded candidates from results report
|
|
||||||
#[clap(help_heading=Some("DISPLAY"), long)]
|
|
||||||
hide_excluded: bool,
|
|
||||||
|
|
||||||
/// Sort candidates by votes in results report
|
|
||||||
#[clap(help_heading=Some("DISPLAY"), long)]
|
|
||||||
sort_votes: bool,
|
|
||||||
|
|
||||||
/// Print votes to specified decimal places in results report
|
|
||||||
#[clap(help_heading=Some("DISPLAY"), long, default_value="2", value_name="dps")]
|
|
||||||
pp_decimals: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -209,185 +45,9 @@ fn main() {
|
|||||||
fn main_() -> Result<(), i32> {
|
fn main_() -> Result<(), i32> {
|
||||||
// Read arguments
|
// Read arguments
|
||||||
let opts: Opts = Opts::parse();
|
let opts: Opts = Opts::parse();
|
||||||
let Command::STV(cmd_opts) = opts.command;
|
|
||||||
|
|
||||||
// Read and count election according to --numbers
|
return match opts.command {
|
||||||
if cmd_opts.numbers == "rational" {
|
Command::Convert(cmd_opts) => cli::convert::main(cmd_opts),
|
||||||
let mut election = election_from_file(&cmd_opts.filename)?;
|
Command::STV(cmd_opts) => cli::stv::main(cmd_opts),
|
||||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
|
||||||
|
|
||||||
// Must specify ::<N> here and in a few other places because ndarray causes E0275 otherwise
|
|
||||||
count_election::<Rational>(election, cmd_opts)?;
|
|
||||||
} else if cmd_opts.numbers == "float64" {
|
|
||||||
let mut election = election_from_file(&cmd_opts.filename)?;
|
|
||||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
|
||||||
count_election::<NativeFloat64>(election, cmd_opts)?;
|
|
||||||
} else if cmd_opts.numbers == "fixed" {
|
|
||||||
Fixed::set_dps(cmd_opts.decimals);
|
|
||||||
|
|
||||||
let mut election = election_from_file(&cmd_opts.filename)?;
|
|
||||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
|
||||||
count_election::<Fixed>(election, cmd_opts)?;
|
|
||||||
} else if cmd_opts.numbers == "gfixed" {
|
|
||||||
GuardedFixed::set_dps(cmd_opts.decimals);
|
|
||||||
|
|
||||||
let mut election = election_from_file(&cmd_opts.filename)?;
|
|
||||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
|
||||||
count_election::<GuardedFixed>(election, cmd_opts)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn election_from_file<N: Number>(path: &str) -> Result<Election<N>, i32> {
|
|
||||||
match Election::from_file(path) {
|
|
||||||
Ok(e) => return Ok(e),
|
|
||||||
Err(err) => {
|
|
||||||
println!("Syntax Error: {}", err);
|
|
||||||
return Err(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn maybe_load_constraints<N: Number>(election: &mut Election<N>, constraints: &Option<String>) {
|
|
||||||
if let Some(c) = constraints {
|
|
||||||
let file = File::open(c).expect("IO Error");
|
|
||||||
let lines = io::BufReader::new(file).lines();
|
|
||||||
election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn count_election<N: Number>(mut election: Election<N>, cmd_opts: STV) -> Result<(), i32>
|
|
||||||
where
|
|
||||||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
|
||||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
|
||||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
|
||||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
|
||||||
for<'r> &'r N: ops::Neg<Output=N>
|
|
||||||
{
|
|
||||||
// Copy applicable options
|
|
||||||
let opts = STVOptions::new(
|
|
||||||
cmd_opts.round_surplus_fractions,
|
|
||||||
cmd_opts.round_values,
|
|
||||||
cmd_opts.round_votes,
|
|
||||||
cmd_opts.round_quota,
|
|
||||||
cmd_opts.sum_surplus_transfers.into(),
|
|
||||||
cmd_opts.meek_surplus_tolerance.into(),
|
|
||||||
cmd_opts.normalise_ballots,
|
|
||||||
cmd_opts.quota.into(),
|
|
||||||
cmd_opts.quota_criterion.into(),
|
|
||||||
cmd_opts.quota_mode.into(),
|
|
||||||
ties::from_strs(cmd_opts.ties, cmd_opts.random_seed),
|
|
||||||
cmd_opts.surplus.into(),
|
|
||||||
cmd_opts.surplus_order.into(),
|
|
||||||
cmd_opts.transferable_only,
|
|
||||||
cmd_opts.exclusion.into(),
|
|
||||||
cmd_opts.meek_nz_exclusion,
|
|
||||||
cmd_opts.sample.into(),
|
|
||||||
cmd_opts.sample_per_ballot,
|
|
||||||
!cmd_opts.no_early_bulk_elect,
|
|
||||||
cmd_opts.bulk_exclude,
|
|
||||||
cmd_opts.defer_surpluses,
|
|
||||||
!cmd_opts.no_immediate_elect,
|
|
||||||
cmd_opts.min_threshold,
|
|
||||||
cmd_opts.constraints,
|
|
||||||
cmd_opts.constraint_mode.into(),
|
|
||||||
cmd_opts.hide_excluded,
|
|
||||||
cmd_opts.sort_votes,
|
|
||||||
cmd_opts.pp_decimals,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Validate options
|
|
||||||
match opts.validate() {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => {
|
|
||||||
println!("Error: {}", err.describe());
|
|
||||||
return Err(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Describe count
|
|
||||||
let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value });
|
|
||||||
print!("Count computed by OpenTally (revision {}). Read {:.0} ballots from \"{}\" for election \"{}\". There are {} candidates for {} vacancies. ", opentally::VERSION, total_ballots, cmd_opts.filename, election.name, election.candidates.len(), election.seats);
|
|
||||||
let opts_str = opts.describe::<N>();
|
|
||||||
if opts_str.len() > 0 {
|
|
||||||
println!("Counting using options \"{}\".", opts_str);
|
|
||||||
} else {
|
|
||||||
println!("Counting using default options.");
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
|
|
||||||
// Normalise ballots if requested
|
|
||||||
if cmd_opts.normalise_ballots {
|
|
||||||
election.normalise_ballots();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialise count state
|
|
||||||
let mut state = CountState::new(&election);
|
|
||||||
|
|
||||||
// Distribute first preferences
|
|
||||||
match stv::count_init(&mut state, &opts) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => {
|
|
||||||
println!("Error: {}", err.describe());
|
|
||||||
return Err(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut stage_num = 1;
|
|
||||||
print_stage(stage_num, &state, &opts);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match stv::count_one_stage(&mut state, &opts) {
|
|
||||||
Ok(is_done) => {
|
|
||||||
if is_done {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("Error: {}", err.describe());
|
|
||||||
return Err(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage_num += 1;
|
|
||||||
print_stage(stage_num, &state, &opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Count complete. The winning candidates are, in order of election:");
|
|
||||||
|
|
||||||
let mut winners = Vec::new();
|
|
||||||
for (candidate, count_card) in state.candidates.iter() {
|
|
||||||
if count_card.state == CandidateState::Elected {
|
|
||||||
winners.push((candidate, count_card));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
winners.sort_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
|
|
||||||
|
|
||||||
for (i, (winner, count_card)) in winners.into_iter().enumerate() {
|
|
||||||
if let Some(kv) = &count_card.keep_value {
|
|
||||||
println!("{}. {} (kv = {:.dps2$})", i + 1, winner.name, kv, dps2=max(opts.pp_decimals, 2));
|
|
||||||
} else {
|
|
||||||
println!("{}. {}", i + 1, winner.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_stage<N: Number>(stage_num: usize, state: &CountState<N>, opts: &STVOptions) {
|
|
||||||
// Print stage details
|
|
||||||
match state.kind {
|
|
||||||
None => { println!("{}. {}", stage_num, state.title); }
|
|
||||||
Some(kind) => { println!("{}. {} {}", stage_num, kind, state.title); }
|
|
||||||
};
|
};
|
||||||
println!("{}", state.logger.render().join(" "));
|
|
||||||
|
|
||||||
// Print candidates
|
|
||||||
print!("{}", state.describe_candidates(opts));
|
|
||||||
|
|
||||||
// Print summary rows
|
|
||||||
print!("{}", state.describe_summary(opts));
|
|
||||||
|
|
||||||
println!("");
|
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,19 @@
|
|||||||
use crate::election::{Ballot, Candidate, Election};
|
use crate::election::{Ballot, Candidate, Election};
|
||||||
use crate::numbers::Number;
|
use crate::numbers::Number;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use utf8_chars::BufReadCharsExt;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::iter::Peekable;
|
use std::iter::Peekable;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use std::fs::File;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use std::io::BufReader;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
/// Utility for parsing a BLT file
|
/// Utility for parsing a BLT file
|
||||||
pub struct BLTParser<N: Number, I: Iterator<Item=char>> {
|
pub struct BLTParser<N: Number, I: Iterator<Item=char>> {
|
||||||
/// The peekable iterator of chars representing the BLT file
|
/// The peekable iterator of chars representing the BLT file
|
||||||
@ -366,3 +376,18 @@ impl<N: Number, I: Iterator<Item=char>> BLTParser<N, I> {
|
|||||||
return self.election;
|
return self.election;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse the given BLT file and return an [Election]
|
||||||
|
pub fn parse_iterator<I: Iterator<Item=char>, N: Number>(input: Peekable<I>) -> Result<Election<N>, ParseError> {
|
||||||
|
let mut parser = BLTParser::new(input);
|
||||||
|
parser.parse_blt()?;
|
||||||
|
return Ok(parser.as_election());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the BLT file at the given path and return an [Election]
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn parse_path<P: AsRef<Path>, N: Number>(path: P) -> Result<Election<N>, ParseError> {
|
||||||
|
let mut reader = BufReader::new(File::open(path).expect("IO Error"));
|
||||||
|
let chars = reader.chars().map(|r| r.expect("IO Error")).peekable();
|
||||||
|
return Ok(parse_iterator(chars)?);
|
||||||
|
}
|
||||||
|
102
src/parser/csp.rs
Normal file
102
src/parser/csp.rs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/* OpenTally: Open-source election vote counting
|
||||||
|
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::election::{Ballot, Candidate, Election};
|
||||||
|
use crate::numbers::Number;
|
||||||
|
|
||||||
|
use csv::ReaderBuilder;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
/// Parse the given CSP file
|
||||||
|
pub fn parse_reader<R: Read, N: Number>(reader: R, seats: usize) -> Election<N> {
|
||||||
|
// Read CSV file
|
||||||
|
let mut reader = ReaderBuilder::new()
|
||||||
|
.has_headers(true)
|
||||||
|
.from_reader(reader);
|
||||||
|
|
||||||
|
// Read candidates
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
let mut idx_map = HashMap::new(); // Map csp index -> candidates index
|
||||||
|
|
||||||
|
for (i, cand_name) in reader.headers().expect("Syntax Error").into_iter().enumerate() {
|
||||||
|
if cand_name == "$mult" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
idx_map.insert(i, candidates.len());
|
||||||
|
candidates.push(Candidate {
|
||||||
|
name: cand_name.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read ballots
|
||||||
|
let mut ballots = Vec::new();
|
||||||
|
|
||||||
|
for record in reader.into_records() {
|
||||||
|
let record = record.expect("Syntax Error");
|
||||||
|
|
||||||
|
let mut value = N::one();
|
||||||
|
|
||||||
|
// Record preferences
|
||||||
|
let mut preferences = Vec::new(); // Vec of (ranking, candidate index)
|
||||||
|
for (csv_index, preference) in record.into_iter().enumerate() {
|
||||||
|
match idx_map.get(&csv_index) {
|
||||||
|
Some(cand_index) => {
|
||||||
|
// Preference
|
||||||
|
if preference.len() == 0 || preference == "-" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let preference: usize = preference.parse().expect("Syntax Error");
|
||||||
|
if preference == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences.push((preference, cand_index));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// $mult column
|
||||||
|
let mult: usize = preference.parse().expect("Syntax Error");
|
||||||
|
if mult == 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
value = N::from(mult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by ranking
|
||||||
|
// FIXME: Handle equal rankings and skipped rankings
|
||||||
|
preferences.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
ballots.push(Ballot {
|
||||||
|
orig_value: value,
|
||||||
|
preferences: preferences.into_iter().map(|(_, i)| *i).collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Election {
|
||||||
|
name: String::new(),
|
||||||
|
seats: seats,
|
||||||
|
candidates: candidates,
|
||||||
|
withdrawn_candidates: Vec::new(),
|
||||||
|
ballots: ballots,
|
||||||
|
constraints: None,
|
||||||
|
};
|
||||||
|
}
|
@ -17,3 +17,6 @@
|
|||||||
|
|
||||||
/// BLT file parser
|
/// BLT file parser
|
||||||
pub mod blt;
|
pub mod blt;
|
||||||
|
/// CSP file parser
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub mod csp;
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
use crate::constraints::Constraints;
|
use crate::constraints::Constraints;
|
||||||
use crate::election::{CandidateState, CountState, Election};
|
use crate::election::{CandidateState, CountState, Election};
|
||||||
use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
|
use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
|
||||||
|
use crate::parser::blt;
|
||||||
use crate::stv;
|
use crate::stv;
|
||||||
use crate::ties;
|
use crate::ties;
|
||||||
|
|
||||||
@ -65,14 +66,14 @@ macro_rules! impl_type {
|
|||||||
($type:ident) => { paste::item! {
|
($type:ident) => { paste::item! {
|
||||||
// Counting
|
// Counting
|
||||||
|
|
||||||
/// Wrapper for [Election::from_blt]
|
/// Wrapper for [blt::parse_iterator]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<election_from_blt_$type>](text: String) -> [<Election$type>] {
|
pub fn [<election_from_blt_$type>](text: String) -> [<Election$type>] {
|
||||||
// Install panic! hook
|
// Install panic! hook
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
let election: Election<$type> = match Election::from_blt(text.chars().peekable()) {
|
let election: Election<$type> = match blt::parse_iterator(text.chars().peekable()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => wasm_error!("Syntax Error", err),
|
Err(err) => wasm_error!("Syntax Error", err),
|
||||||
};
|
};
|
||||||
|
55
src/writer/blt.rs
Normal file
55
src/writer/blt.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/* OpenTally: Open-source election vote counting
|
||||||
|
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::election::Election;
|
||||||
|
use crate::numbers::Number;
|
||||||
|
|
||||||
|
use std::io::{BufWriter, Write};
|
||||||
|
|
||||||
|
/// Write the [Election] into BLT format
|
||||||
|
pub fn write<W: Write, N: Number>(election: Election<N>, output: W) {
|
||||||
|
let mut output = BufWriter::new(output);
|
||||||
|
|
||||||
|
// Writer header row
|
||||||
|
output.write_fmt(format_args!("{} {}\n", election.candidates.len(), election.seats)).expect("IO Error");
|
||||||
|
|
||||||
|
// Write withdrawn candidates
|
||||||
|
if !election.withdrawn_candidates.is_empty() {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write ballots
|
||||||
|
for ballot in election.ballots {
|
||||||
|
output.write_fmt(format_args!("{}", ballot.orig_value)).expect("IO Error");
|
||||||
|
|
||||||
|
for preference in ballot.preferences {
|
||||||
|
output.write_fmt(format_args!(" {}", preference + 1)).expect("IO Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
output.write(b" 0\n").expect("IO Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
output.write(b"0\n").expect("IO Error");
|
||||||
|
|
||||||
|
// Write candidate names
|
||||||
|
for candidate in election.candidates {
|
||||||
|
output.write_fmt(format_args!("\"{}\"\n", candidate.name)).expect("IO Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write election name
|
||||||
|
output.write_fmt(format_args!("\"{}\"\n", election.name)).expect("IO Error");
|
||||||
|
}
|
53
src/writer/csp.rs
Normal file
53
src/writer/csp.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/* OpenTally: Open-source election vote counting
|
||||||
|
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::election::Election;
|
||||||
|
use crate::numbers::Number;
|
||||||
|
|
||||||
|
use csv::Writer;
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
/// Write the [Election] into CSP format
|
||||||
|
pub fn write<W: Write, N: Number>(election: Election<N>, output: W) {
|
||||||
|
// Open writer
|
||||||
|
// csv::Writer performs its own buffering
|
||||||
|
let mut output = Writer::from_writer(output);
|
||||||
|
|
||||||
|
// Write header row
|
||||||
|
for candidate in election.candidates.iter() {
|
||||||
|
output.write_field(&candidate.name).expect("IO Error");
|
||||||
|
}
|
||||||
|
output.write_field("$mult").expect("IO Error");
|
||||||
|
output.write_record(None::<&[u8]>).expect("IO Error");
|
||||||
|
|
||||||
|
// Write ballots
|
||||||
|
for ballot in election.ballots {
|
||||||
|
// Code preferences to rankings
|
||||||
|
let mut rankings = vec![0_usize; election.candidates.len()];
|
||||||
|
for (i, preference) in ballot.preferences.into_iter().enumerate() {
|
||||||
|
rankings[preference] = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write rankings
|
||||||
|
for ranking in rankings {
|
||||||
|
output.write_field(format!("{}", ranking)).expect("IO Error");
|
||||||
|
}
|
||||||
|
output.write_field(format!("{}", ballot.orig_value)).expect("IO Error");
|
||||||
|
output.write_record(None::<&[u8]>).expect("IO Error");
|
||||||
|
}
|
||||||
|
}
|
21
src/writer/mod.rs
Normal file
21
src/writer/mod.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/* OpenTally: Open-source election vote counting
|
||||||
|
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// BLT file writer
|
||||||
|
pub mod blt;
|
||||||
|
/// CSP file writer
|
||||||
|
pub mod csp;
|
66
test.csp
Normal file
66
test.csp
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
Evans,Grey,Thomson,Ames,Reid,Spears,White,$mult
|
||||||
|
1,0,2,0,0,0,0,1
|
||||||
|
0,1,0,2,0,0,3,1
|
||||||
|
3,1,0,0,0,2,4,1
|
||||||
|
0,1,4,0,3,2,0,1
|
||||||
|
0,0,1,0,0,0,0,1
|
||||||
|
0,0,0,1,0,0,0,1
|
||||||
|
0,0,1,0,0,0,0,1
|
||||||
|
0,0,0,1,0,0,0,1
|
||||||
|
0,0,0,1,0,0,0,1
|
||||||
|
0,0,1,0,0,0,0,1
|
||||||
|
0,0,0,0,1,0,2,1
|
||||||
|
2,0,0,1,0,0,0,1
|
||||||
|
0,0,0,0,0,1,0,1
|
||||||
|
0,0,0,1,0,0,0,1
|
||||||
|
0,1,2,0,0,0,0,1
|
||||||
|
0,0,1,0,0,0,0,1
|
||||||
|
0,0,0,0,0,0,1,1
|
||||||
|
0,0,0,1,0,0,0,1
|
||||||
|
0,1,0,2,0,0,3,1
|
||||||
|
0,1,0,2,0,0,3,1
|
||||||
|
0,0,0,1,0,0,0,1
|
||||||
|
0,1,2,0,0,0,0,1
|
||||||
|
0,0,0,1,0,0,0,1
|
||||||
|
0,0,0,0,0,0,1,1
|
||||||
|
0,1,0,0,0,2,0,1
|
||||||
|
2,1,0,0,0,0,0,1
|
||||||
|
0,0,0,0,0,0,1,1
|
||||||
|
3,1,4,2,0,0,0,1
|
||||||
|
0,0,0,1,0,0,0,1
|
||||||
|
0,0,0,0,0,1,0,1
|
||||||
|
0,0,0,0,0,1,0,1
|
||||||
|
0,1,3,2,0,0,4,1
|
||||||
|
0,0,0,0,0,1,0,1
|
||||||
|
0,1,0,0,0,0,2,1
|
||||||
|
0,1,0,0,0,0,2,1
|
||||||
|
3,1,0,0,0,2,4,1
|
||||||
|
0,0,1,0,0,0,0,1
|
||||||
|
0,1,0,2,0,0,3,1
|
||||||
|
0,1,0,0,0,2,0,1
|
||||||
|
0,1,3,2,0,0,4,1
|
||||||
|
0,1,0,0,0,2,0,1
|
||||||
|
0,1,0,2,0,0,3,1
|
||||||
|
0,0,0,1,0,0,0,1
|
||||||
|
0,1,0,0,0,2,3,1
|
||||||
|
3,1,0,0,0,2,0,1
|
||||||
|
0,1,0,2,0,0,3,1
|
||||||
|
0,0,0,0,0,1,0,1
|
||||||
|
0,1,0,2,0,0,3,1
|
||||||
|
0,1,2,0,0,0,0,1
|
||||||
|
0,0,0,0,0,1,0,1
|
||||||
|
0,1,4,0,3,2,0,1
|
||||||
|
3,1,0,2,0,0,4,1
|
||||||
|
0,0,0,0,0,1,0,1
|
||||||
|
0,0,0,0,0,1,0,1
|
||||||
|
2,1,0,0,0,0,0,1
|
||||||
|
0,1,0,2,0,0,3,1
|
||||||
|
4,1,0,0,3,2,5,1
|
||||||
|
0,1,0,0,0,0,2,1
|
||||||
|
0,1,3,2,0,0,4,1
|
||||||
|
0,0,0,0,0,1,0,1
|
||||||
|
0,1,2,0,0,0,0,1
|
||||||
|
0,1,0,0,2,0,3,1
|
||||||
|
4,1,5,2,3,0,0,1
|
||||||
|
0,0,0,0,0,1,0,1
|
||||||
|
0,0,0,0,0,1,0,1
|
@ -19,6 +19,7 @@ mod utils;
|
|||||||
|
|
||||||
use opentally::election::Election;
|
use opentally::election::Election;
|
||||||
use opentally::numbers::Rational;
|
use opentally::numbers::Rational;
|
||||||
|
use opentally::parser::blt;
|
||||||
use opentally::stv;
|
use opentally::stv;
|
||||||
|
|
||||||
use csv::StringRecord;
|
use csv::StringRecord;
|
||||||
@ -52,7 +53,7 @@ fn aec_tas19_rational() {
|
|||||||
let chars = reader.chars().map(|r| r.expect("IO Error")).peekable();
|
let chars = reader.chars().map(|r| r.expect("IO Error")).peekable();
|
||||||
|
|
||||||
// Read BLT
|
// Read BLT
|
||||||
let election: Election<Rational> = Election::from_blt(chars).expect("Syntax Error");
|
let election: Election<Rational> = blt::parse_iterator(chars).expect("Syntax Error");
|
||||||
|
|
||||||
// Validate candidate names
|
// Validate candidate names
|
||||||
for (i, candidate) in candidates.iter().enumerate() {
|
for (i, candidate) in candidates.iter().enumerate() {
|
||||||
|
@ -20,6 +20,7 @@ mod utils;
|
|||||||
use opentally::constraints::Constraints;
|
use opentally::constraints::Constraints;
|
||||||
use opentally::election::{CandidateState, CountState, Election};
|
use opentally::election::{CandidateState, CountState, Election};
|
||||||
use opentally::numbers::Rational;
|
use opentally::numbers::Rational;
|
||||||
|
use opentally::parser::blt;
|
||||||
use opentally::stv;
|
use opentally::stv;
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@ -30,7 +31,7 @@ fn prsa1_constr1_rational() {
|
|||||||
// FIXME: This is unvalidated!
|
// FIXME: This is unvalidated!
|
||||||
|
|
||||||
// Read BLT
|
// Read BLT
|
||||||
let mut election: Election<Rational> = Election::from_file("tests/data/prsa1.blt").expect("Syntax Error");
|
let mut election: Election<Rational> = blt::parse_path("tests/data/prsa1.blt").expect("Syntax Error");
|
||||||
|
|
||||||
// Read CON
|
// Read CON
|
||||||
let file = File::open("tests/data/prsa1_constr1.con").expect("IO Error");
|
let file = File::open("tests/data/prsa1_constr1.con").expect("IO Error");
|
||||||
@ -79,7 +80,7 @@ fn prsa1_constr2_rational() {
|
|||||||
// FIXME: This is unvalidated!
|
// FIXME: This is unvalidated!
|
||||||
|
|
||||||
// Read BLT
|
// Read BLT
|
||||||
let mut election: Election<Rational> = Election::from_file("tests/data/prsa1.blt").expect("Syntax Error");
|
let mut election: Election<Rational> = blt::parse_path("tests/data/prsa1.blt").expect("Syntax Error");
|
||||||
|
|
||||||
// Read CON
|
// Read CON
|
||||||
let file = File::open("tests/data/prsa1_constr2.con").expect("IO Error");
|
let file = File::open("tests/data/prsa1_constr2.con").expect("IO Error");
|
||||||
@ -128,7 +129,7 @@ fn prsa1_constr3_rational() {
|
|||||||
// FIXME: This is unvalidated!
|
// FIXME: This is unvalidated!
|
||||||
|
|
||||||
// Read BLT
|
// Read BLT
|
||||||
let mut election: Election<Rational> = Election::from_file("tests/data/prsa1.blt").expect("Syntax Error");
|
let mut election: Election<Rational> = blt::parse_path("tests/data/prsa1.blt").expect("Syntax Error");
|
||||||
|
|
||||||
// Read CON
|
// Read CON
|
||||||
let file = File::open("tests/data/prsa1_constr3.con").expect("IO Error");
|
let file = File::open("tests/data/prsa1_constr3.con").expect("IO Error");
|
||||||
@ -189,7 +190,7 @@ fn ers97old_cantbulkexclude_rational() {
|
|||||||
let stages: Vec<usize> = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect();
|
let stages: Vec<usize> = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect();
|
||||||
|
|
||||||
// Read BLT
|
// Read BLT
|
||||||
let mut election: Election<Rational> = Election::from_file("tests/data/ers97old.blt").expect("Syntax Error");
|
let mut election: Election<Rational> = blt::parse_path("tests/data/ers97old.blt").expect("Syntax Error");
|
||||||
|
|
||||||
// Read CON
|
// Read CON
|
||||||
let file = File::open("tests/data/ers97old_cantbulkexclude.con").expect("IO Error");
|
let file = File::open("tests/data/ers97old_cantbulkexclude.con").expect("IO Error");
|
||||||
|
@ -19,6 +19,7 @@ mod utils;
|
|||||||
|
|
||||||
use opentally::election::{CandidateState, CountState, Election};
|
use opentally::election::{CandidateState, CountState, Election};
|
||||||
use opentally::numbers::{Fixed, NativeFloat64, Number};
|
use opentally::numbers::{Fixed, NativeFloat64, Number};
|
||||||
|
use opentally::parser::blt;
|
||||||
use opentally::stv;
|
use opentally::stv;
|
||||||
|
|
||||||
// Compare ers97old.blt count with result produced by 1987 Hill–Wichmann–Woodall reference implementation
|
// Compare ers97old.blt count with result produced by 1987 Hill–Wichmann–Woodall reference implementation
|
||||||
@ -54,7 +55,7 @@ fn meek06_ers97old_fixed12() {
|
|||||||
Fixed::set_dps(12);
|
Fixed::set_dps(12);
|
||||||
|
|
||||||
// Read BLT
|
// Read BLT
|
||||||
let election: Election<Fixed> = Election::from_file("tests/data/ers97old.blt").expect("Syntax Error");
|
let election: Election<Fixed> = blt::parse_path("tests/data/ers97old.blt").expect("Syntax Error");
|
||||||
|
|
||||||
// Initialise count state
|
// Initialise count state
|
||||||
let mut state = CountState::new(&election);
|
let mut state = CountState::new(&election);
|
||||||
@ -114,7 +115,7 @@ fn meeknz_ers97old_fixed12() {
|
|||||||
Fixed::set_dps(12);
|
Fixed::set_dps(12);
|
||||||
|
|
||||||
// Read BLT
|
// Read BLT
|
||||||
let election: Election<Fixed> = Election::from_file("tests/data/ers97old.blt").expect("Syntax Error");
|
let election: Election<Fixed> = blt::parse_path("tests/data/ers97old.blt").expect("Syntax Error");
|
||||||
|
|
||||||
// Initialise count state
|
// Initialise count state
|
||||||
let mut state = CountState::new(&election);
|
let mut state = CountState::new(&election);
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
use opentally::election::{CandidateState, CountState, Election};
|
use opentally::election::{CandidateState, CountState, Election};
|
||||||
use opentally::numbers::{Fixed, GuardedFixed, Number};
|
use opentally::numbers::{Fixed, GuardedFixed, Number};
|
||||||
|
use opentally::parser::blt;
|
||||||
use opentally::stv;
|
use opentally::stv;
|
||||||
|
|
||||||
use xmltree::Element;
|
use xmltree::Element;
|
||||||
@ -85,7 +86,7 @@ where
|
|||||||
let num_stages = root.get_child("headerrow").expect("Syntax Error").children.len();
|
let num_stages = root.get_child("headerrow").expect("Syntax Error").children.len();
|
||||||
|
|
||||||
// Read BLT
|
// Read BLT
|
||||||
let mut election: Election<N> = Election::from_file("tests/data/linn07.blt").expect("Syntax Error");
|
let mut election: Election<N> = blt::parse_path("tests/data/linn07.blt").expect("Syntax Error");
|
||||||
|
|
||||||
// !!! FOR SCOTTISH STV !!!
|
// !!! FOR SCOTTISH STV !!!
|
||||||
election.normalise_ballots();
|
election.normalise_ballots();
|
||||||
|
@ -17,9 +17,10 @@
|
|||||||
|
|
||||||
use opentally::election::{CandidateState, CountState, Election};
|
use opentally::election::{CandidateState, CountState, Election};
|
||||||
use opentally::numbers::Number;
|
use opentally::numbers::Number;
|
||||||
|
use opentally::parser::blt;
|
||||||
use opentally::stv;
|
use opentally::stv;
|
||||||
|
|
||||||
use csv::StringRecord;
|
use csv::{ReaderBuilder, StringRecord};
|
||||||
|
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ where
|
|||||||
for<'r> &'r N: ops::Neg<Output=N>,
|
for<'r> &'r N: ops::Neg<Output=N>,
|
||||||
{
|
{
|
||||||
// Read CSV file
|
// Read CSV file
|
||||||
let reader = csv::ReaderBuilder::new()
|
let reader = ReaderBuilder::new()
|
||||||
.has_headers(false)
|
.has_headers(false)
|
||||||
.from_path(csv_file)
|
.from_path(csv_file)
|
||||||
.expect("IO Error");
|
.expect("IO Error");
|
||||||
@ -46,7 +47,7 @@ where
|
|||||||
let stages: Vec<usize> = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect();
|
let stages: Vec<usize> = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect();
|
||||||
|
|
||||||
// Read BLT
|
// Read BLT
|
||||||
let election: Election<N> = Election::from_file(blt_file).expect("Syntax Error");
|
let election: Election<N> = blt::parse_path(blt_file).expect("Syntax Error");
|
||||||
|
|
||||||
// Validate candidate names
|
// Validate candidate names
|
||||||
for (i, candidate) in candidates.iter().enumerate() {
|
for (i, candidate) in candidates.iter().enumerate() {
|
||||||
|
Loading…
Reference in New Issue
Block a user