/* OpenTally: Open-source election vote counting
* Copyright © 2021–2022 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::{self, Constraints};
use crate::election::{CandidateState, CountState, Election, StageKind};
use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
use crate::parser::{bin, blt};
use crate::stv::{self, STVOptions};
use crate::ties;
use clap::{AppSettings, Parser};
use itertools::Itertools;
use std::cmp::max;
use std::fs::File;
use std::io::{self, BufRead};
use std::ops;
/// Count a single transferable vote (STV) election
#[derive(Parser)]
#[clap(setting=AppSettings::DeriveDisplayOrder)]
pub struct SubcmdOptions {
// ----------------
// -- File input --
/// Path to the BLT file to be counted
#[clap(help_heading=Some("INPUT"))]
filename: String,
/// Input is in serialised binary format from "opentally convert"
#[clap(help_heading=Some("INPUT"), long)]
bin: bool,
// ----------------------
// -- 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,
// -----------------------
// -- 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 round subtransfers during surpluses/exclusions
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["single_step", "by_value", "by_value_and_source", "by_parcel", "per_ballot"], default_value="single_step", value_name="mode")]
round_subtransfers: 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, multiple_values=true, 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 [default: wig] [possible values: wig, uig, eg, meek, ihare, hare]
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek", "ihare", "hare", "eh"], default_value="wig", value_name="method", hide_possible_values=true, hide_default_value=true)]
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) If --transferable-only, calculate value of transferable papers by subtracting value of non-transferable papers
#[clap(help_heading=Some("STV VARIANTS"), long)]
subtract_nontransferable: 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,
/// (Hare) Method of drawing a sample [default: stratify] [possible values: stratify, by_order, cincinnati]
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["stratify", "stratify_lr", "by_order", "cincinnati", "nth_ballot"], default_value="stratify", value_name="method", hide_possible_values=true, hide_default_value=true)]
sample: String,
/// (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", "repeat_count"], default_value="guard_doom")]
constraint_mode: String,
// ---------------------
// -- Output settings --
#[clap(help_heading=Some("OUTPUT"), short, long, possible_values=&["text", "csv"], default_value="text")]
output: String,
/// Hide excluded candidates from results report
#[clap(help_heading=Some("OUTPUT"), long)]
hide_excluded: bool,
/// Sort candidates by votes in results report
#[clap(help_heading=Some("OUTPUT"), long)]
sort_votes: bool,
/// Show details of transfers to candidates during surplus distributions/candidate exclusions
#[clap(help_heading=Some("OUTPUT"), long)]
transfers_detail: bool,
/// Print votes to specified decimal places in results report
#[clap(help_heading=Some("OUTPUT"), 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, cmd_opts.bin)?;
maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
// 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, cmd_opts.bin)?;
maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
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, cmd_opts.bin)?;
maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
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, cmd_opts.bin)?;
maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
count_election::(election, cmd_opts)?;
}
return Ok(());
}
fn election_from_file(path: &str, bin: bool) -> Result, i32> {
if bin {
// BIN format
return Ok(bin::parse_path(path));
} else {
// BLT format
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, constraint_mode: &str) -> Result<(), i32> {
if let Some(c) = constraints {
let file = File::open(c).expect("IO Error");
let lines = io::BufReader::new(file).lines();
let lines: Vec<_> = lines.map(|r| r.expect("IO Error")).collect();
match Constraints::from_con(lines.into_iter()) {
Ok(c) => {
election.constraints = Some(c);
}
Err(err) => {
println!("Constraint Syntax Error: {}", err);
return Err(1);
}
}
// Validate constraints
if let Err(err) = election.constraints.as_ref().unwrap().validate_constraints(election.candidates.len(), constraint_mode.into()) {
println!("Constraint Validation Error: {}", err);
return Err(1);
}
if constraint_mode == "repeat_count" {
constraints::init_repeat_count(election);
}
}
Ok(())
}
fn count_election(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