Add subcommand for BLT/CSP file conversion

This commit is contained in:
RunasSudo 2021-08-20 02:16:54 +10:00
parent e7bae376e9
commit 88ab06d633
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
19 changed files with 870 additions and 388 deletions

120
src/cli/convert.rs Normal file
View 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
View 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
View 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!("");
}

View File

@ -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();

View File

@ -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;

View File

@ -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!("");
} }

View File

@ -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
View 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,
};
}

View File

@ -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;

View File

@ -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
View 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
View 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
View 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
View 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

View File

@ -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() {

View File

@ -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");

View File

@ -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);

View File

@ -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();

View File

@ -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() {