From 88ab06d63354c839aba7048519c00cc8935db735 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Fri, 20 Aug 2021 02:16:54 +1000 Subject: [PATCH] Add subcommand for BLT/CSP file conversion --- src/cli/convert.rs | 120 ++++++++++++++ src/cli/mod.rs | 21 +++ src/cli/stv.rs | 369 +++++++++++++++++++++++++++++++++++++++++++ src/election.rs | 26 --- src/lib.rs | 11 +- src/main.rs | 354 +---------------------------------------- src/parser/blt.rs | 25 +++ src/parser/csp.rs | 102 ++++++++++++ src/parser/mod.rs | 3 + src/stv/wasm.rs | 5 +- src/writer/blt.rs | 55 +++++++ src/writer/csp.rs | 53 +++++++ src/writer/mod.rs | 21 +++ test.csp | 66 ++++++++ tests/aec.rs | 3 +- tests/constraints.rs | 9 +- tests/meek.rs | 5 +- tests/scotland.rs | 3 +- tests/utils/mod.rs | 7 +- 19 files changed, 870 insertions(+), 388 deletions(-) create mode 100644 src/cli/convert.rs create mode 100644 src/cli/mod.rs create mode 100644 src/cli/stv.rs create mode 100644 src/parser/csp.rs create mode 100644 src/writer/blt.rs create mode 100644 src/writer/csp.rs create mode 100644 src/writer/mod.rs create mode 100644 test.csp diff --git a/src/cli/convert.rs b/src/cli/convert.rs new file mode 100644 index 0000000..2f71ff6 --- /dev/null +++ b/src/cli/convert.rs @@ -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 . + */ + +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, + + /// Format of output file + #[clap(help_heading=Some("INPUT/OUTPUT"), short, long, possible_values=&["blt", "csp"], value_name="format")] + out: Option, + + /// Number of seats + #[clap(help_heading=Some("ELECTION SPECIFICATION"), long)] + seats: Option, +} + +/// 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; + + 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(()); +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..a244dd2 --- /dev/null +++ b/src/cli/mod.rs @@ -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 . + */ + +/// Convert between different ballot data formats +pub mod convert; +/// Count a single transferable vote (STV) election +pub mod stv; diff --git a/src/cli/stv.rs b/src/cli/stv.rs new file mode 100644 index 0000000..8a50f21 --- /dev/null +++ b/src/cli/stv.rs @@ -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 . + */ + +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, + + /// Round ballot values to specified decimal places + #[clap(help_heading=Some("ROUNDING"), long, alias="round-weights", value_name="dps")] + round_values: Option, + + /// Round votes to specified decimal places + #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] + round_votes: Option, + + /// Round quota to specified decimal places + #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] + round_quota: Option, + + /// (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, + + /// Random seed to use with --ties random + #[clap(help_heading=Some("STV VARIANTS"), long, value_name="seed")] + random_seed: Option, + + /// 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, + + /// 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 :: here and in a few other places because ndarray causes E0275 otherwise + count_election::(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::(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::(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::(election, cmd_opts)?; + } + + return Ok(()); +} + +fn election_from_file(path: &str) -> Result, i32> { + match blt::parse_path(path) { + Ok(e) => return Ok(e), + Err(err) => { + println!("Syntax Error: {}", err); + return Err(1); + } + } +} + +fn maybe_load_constraints(election: &mut Election, constraints: &Option) { + 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(mut election: Election, 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 +{ + // 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::(); + 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(stage_num: usize, state: &CountState, 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!(""); +} diff --git a/src/election.rs b/src/election.rs index 8534438..db93469 100644 --- a/src/election.rs +++ b/src/election.rs @@ -18,22 +18,11 @@ use crate::constraints::{Constraints, ConstraintMatrix}; use crate::logger::Logger; use crate::numbers::Number; -use crate::parser::blt::{BLTParser, ParseError}; use crate::sharandom::SHARandom; use crate::stv::{QuotaMode, STVOptions}; use std::cmp::max; 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 pub struct Election { @@ -52,21 +41,6 @@ pub struct Election { } impl Election { - /// Parse the given BLT file and return an [Election] - pub fn from_blt>(input: Peekable) -> Result { - 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>(path: P) -> Result { - 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 pub fn normalise_ballots(&mut self) { let mut normalised_ballots = Vec::new(); diff --git a/src/lib.rs b/src/lib.rs index ea04e81..5eb0f7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,8 +19,6 @@ //! Open source counting software for various preferential voting election systems -/// File parsers -pub mod parser; /// Data types and logic for constraints on elections pub mod constraints; /// Data types for representing abstract elections @@ -29,12 +27,21 @@ pub mod election; pub mod logger; /// Implementations of different numeric representations pub mod numbers; +/// File parsers +pub mod parser; /// Deterministic random number generation using SHA256 pub mod sharandom; /// STV counting logic pub mod stv; /// Tie-breaking methods 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 wasm_bindgen::prelude::wasm_bindgen; diff --git a/src/main.rs b/src/main.rs index 5ae4e56..9408571 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,18 +15,9 @@ * along with this program. If not, see . */ -use opentally::constraints::Constraints; -use opentally::election::{CandidateState, CountState, Election}; -use opentally::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational}; -use opentally::stv::{self, STVOptions}; -use opentally::ties; +use opentally::cli; -use clap::{AppSettings, Clap}; - -use std::cmp::max; -use std::fs::File; -use std::io::{self, BufRead}; -use std::ops; +use clap::Clap; /// Open-source election vote counting #[derive(Clap)] @@ -38,163 +29,8 @@ struct Opts { #[derive(Clap)] enum Command { - STV(STV), -} - -/// 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, - - /// Round ballot values to specified decimal places - #[clap(help_heading=Some("ROUNDING"), long, alias="round-weights", value_name="dps")] - round_values: Option, - - /// Round votes to specified decimal places - #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] - round_votes: Option, - - /// Round quota to specified decimal places - #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] - round_quota: Option, - - /// (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, - - /// Random seed to use with --ties random - #[clap(help_heading=Some("STV VARIANTS"), long, value_name="seed")] - random_seed: Option, - - /// 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, - - /// 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, + Convert(cli::convert::SubcmdOptions), + STV(cli::stv::SubcmdOptions), } fn main() { @@ -209,185 +45,9 @@ fn main() { fn main_() -> Result<(), i32> { // Read arguments let opts: Opts = Opts::parse(); - let Command::STV(cmd_opts) = opts.command; - // 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 :: here and in a few other places because ndarray causes E0275 otherwise - count_election::(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::(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::(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::(election, cmd_opts)?; - } - - return Ok(()); -} - -fn election_from_file(path: &str) -> Result, i32> { - match Election::from_file(path) { - Ok(e) => return Ok(e), - Err(err) => { - println!("Syntax Error: {}", err); - return Err(1); - } - } -} - -fn maybe_load_constraints(election: &mut Election, constraints: &Option) { - 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(mut election: Election, 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 -{ - // 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::(); - 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(stage_num: usize, state: &CountState, opts: &STVOptions) { - // Print stage details - match state.kind { - None => { println!("{}. {}", stage_num, state.title); } - Some(kind) => { println!("{}. {} {}", stage_num, kind, state.title); } + return match opts.command { + Command::Convert(cmd_opts) => cli::convert::main(cmd_opts), + Command::STV(cmd_opts) => cli::stv::main(cmd_opts), }; - println!("{}", state.logger.render().join(" ")); - - // Print candidates - print!("{}", state.describe_candidates(opts)); - - // Print summary rows - print!("{}", state.describe_summary(opts)); - - println!(""); } diff --git a/src/parser/blt.rs b/src/parser/blt.rs index 21d0719..694e1d9 100644 --- a/src/parser/blt.rs +++ b/src/parser/blt.rs @@ -18,9 +18,19 @@ use crate::election::{Ballot, Candidate, Election}; use crate::numbers::Number; +#[cfg(not(target_arch = "wasm32"))] +use utf8_chars::BufReadCharsExt; + use std::fmt; 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 pub struct BLTParser> { /// The peekable iterator of chars representing the BLT file @@ -366,3 +376,18 @@ impl> BLTParser { return self.election; } } + +/// Parse the given BLT file and return an [Election] +pub fn parse_iterator, N: Number>(input: Peekable) -> Result, 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, N: Number>(path: P) -> Result, 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)?); +} diff --git a/src/parser/csp.rs b/src/parser/csp.rs new file mode 100644 index 0000000..00e9de8 --- /dev/null +++ b/src/parser/csp.rs @@ -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 . + */ + +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(reader: R, seats: usize) -> Election { + // 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, + }; +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3ea5b5b..c48aeaa 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17,3 +17,6 @@ /// BLT file parser pub mod blt; +/// CSP file parser +#[cfg(not(target_arch = "wasm32"))] +pub mod csp; diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index a0ed6e6..d91c88b 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -21,6 +21,7 @@ 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; use crate::ties; @@ -65,14 +66,14 @@ macro_rules! impl_type { ($type:ident) => { paste::item! { // Counting - /// Wrapper for [Election::from_blt] + /// Wrapper for [blt::parse_iterator] #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](text: String) -> [] { // Install panic! hook 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, Err(err) => wasm_error!("Syntax Error", err), }; diff --git a/src/writer/blt.rs b/src/writer/blt.rs new file mode 100644 index 0000000..8baf735 --- /dev/null +++ b/src/writer/blt.rs @@ -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 . + */ + +use crate::election::Election; +use crate::numbers::Number; + +use std::io::{BufWriter, Write}; + +/// Write the [Election] into BLT format +pub fn write(election: Election, 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"); +} diff --git a/src/writer/csp.rs b/src/writer/csp.rs new file mode 100644 index 0000000..d6f208f --- /dev/null +++ b/src/writer/csp.rs @@ -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 . + */ + +use crate::election::Election; +use crate::numbers::Number; + +use csv::Writer; + +use std::io::Write; + +/// Write the [Election] into CSP format +pub fn write(election: Election, 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"); + } +} diff --git a/src/writer/mod.rs b/src/writer/mod.rs new file mode 100644 index 0000000..044a14f --- /dev/null +++ b/src/writer/mod.rs @@ -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 . + */ + +/// BLT file writer +pub mod blt; +/// CSP file writer +pub mod csp; diff --git a/test.csp b/test.csp new file mode 100644 index 0000000..827122f --- /dev/null +++ b/test.csp @@ -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 diff --git a/tests/aec.rs b/tests/aec.rs index d5a95ac..160acde 100644 --- a/tests/aec.rs +++ b/tests/aec.rs @@ -19,6 +19,7 @@ mod utils; use opentally::election::Election; use opentally::numbers::Rational; +use opentally::parser::blt; use opentally::stv; use csv::StringRecord; @@ -52,7 +53,7 @@ fn aec_tas19_rational() { let chars = reader.chars().map(|r| r.expect("IO Error")).peekable(); // Read BLT - let election: Election = Election::from_blt(chars).expect("Syntax Error"); + let election: Election = blt::parse_iterator(chars).expect("Syntax Error"); // Validate candidate names for (i, candidate) in candidates.iter().enumerate() { diff --git a/tests/constraints.rs b/tests/constraints.rs index 3370e46..7445e61 100644 --- a/tests/constraints.rs +++ b/tests/constraints.rs @@ -20,6 +20,7 @@ mod utils; use opentally::constraints::Constraints; use opentally::election::{CandidateState, CountState, Election}; use opentally::numbers::Rational; +use opentally::parser::blt; use opentally::stv; use std::fs::File; @@ -30,7 +31,7 @@ fn prsa1_constr1_rational() { // FIXME: This is unvalidated! // Read BLT - let mut election: Election = Election::from_file("tests/data/prsa1.blt").expect("Syntax Error"); + let mut election: Election = blt::parse_path("tests/data/prsa1.blt").expect("Syntax Error"); // Read CON let file = File::open("tests/data/prsa1_constr1.con").expect("IO Error"); @@ -79,7 +80,7 @@ fn prsa1_constr2_rational() { // FIXME: This is unvalidated! // Read BLT - let mut election: Election = Election::from_file("tests/data/prsa1.blt").expect("Syntax Error"); + let mut election: Election = blt::parse_path("tests/data/prsa1.blt").expect("Syntax Error"); // Read CON let file = File::open("tests/data/prsa1_constr2.con").expect("IO Error"); @@ -128,7 +129,7 @@ fn prsa1_constr3_rational() { // FIXME: This is unvalidated! // Read BLT - let mut election: Election = Election::from_file("tests/data/prsa1.blt").expect("Syntax Error"); + let mut election: Election = blt::parse_path("tests/data/prsa1.blt").expect("Syntax Error"); // Read CON let file = File::open("tests/data/prsa1_constr3.con").expect("IO Error"); @@ -189,7 +190,7 @@ fn ers97old_cantbulkexclude_rational() { let stages: Vec = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect(); // Read BLT - let mut election: Election = Election::from_file("tests/data/ers97old.blt").expect("Syntax Error"); + let mut election: Election = blt::parse_path("tests/data/ers97old.blt").expect("Syntax Error"); // Read CON let file = File::open("tests/data/ers97old_cantbulkexclude.con").expect("IO Error"); diff --git a/tests/meek.rs b/tests/meek.rs index fda58bf..0fa5866 100644 --- a/tests/meek.rs +++ b/tests/meek.rs @@ -19,6 +19,7 @@ mod utils; use opentally::election::{CandidateState, CountState, Election}; use opentally::numbers::{Fixed, NativeFloat64, Number}; +use opentally::parser::blt; use opentally::stv; // 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); // Read BLT - let election: Election = Election::from_file("tests/data/ers97old.blt").expect("Syntax Error"); + let election: Election = blt::parse_path("tests/data/ers97old.blt").expect("Syntax Error"); // Initialise count state let mut state = CountState::new(&election); @@ -114,7 +115,7 @@ fn meeknz_ers97old_fixed12() { Fixed::set_dps(12); // Read BLT - let election: Election = Election::from_file("tests/data/ers97old.blt").expect("Syntax Error"); + let election: Election = blt::parse_path("tests/data/ers97old.blt").expect("Syntax Error"); // Initialise count state let mut state = CountState::new(&election); diff --git a/tests/scotland.rs b/tests/scotland.rs index 915e06c..e7eec19 100644 --- a/tests/scotland.rs +++ b/tests/scotland.rs @@ -17,6 +17,7 @@ use opentally::election::{CandidateState, CountState, Election}; use opentally::numbers::{Fixed, GuardedFixed, Number}; +use opentally::parser::blt; use opentally::stv; use xmltree::Element; @@ -85,7 +86,7 @@ where let num_stages = root.get_child("headerrow").expect("Syntax Error").children.len(); // Read BLT - let mut election: Election = Election::from_file("tests/data/linn07.blt").expect("Syntax Error"); + let mut election: Election = blt::parse_path("tests/data/linn07.blt").expect("Syntax Error"); // !!! FOR SCOTTISH STV !!! election.normalise_ballots(); diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 464c997..0214d96 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -17,9 +17,10 @@ use opentally::election::{CandidateState, CountState, Election}; use opentally::numbers::Number; +use opentally::parser::blt; use opentally::stv; -use csv::StringRecord; +use csv::{ReaderBuilder, StringRecord}; use std::ops; @@ -33,7 +34,7 @@ where for<'r> &'r N: ops::Neg, { // Read CSV file - let reader = csv::ReaderBuilder::new() + let reader = ReaderBuilder::new() .has_headers(false) .from_path(csv_file) .expect("IO Error"); @@ -46,7 +47,7 @@ where let stages: Vec = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect(); // Read BLT - let election: Election = Election::from_file(blt_file).expect("Syntax Error"); + let election: Election = blt::parse_path(blt_file).expect("Syntax Error"); // Validate candidate names for (i, candidate) in candidates.iter().enumerate() {