622 lines
22 KiB
Rust
622 lines
22 KiB
Rust
/* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
use super::STVError;
|
|
|
|
use crate::numbers::Number;
|
|
use crate::ties::TieStrategy;
|
|
|
|
use derive_builder::Builder;
|
|
use derive_more::Constructor;
|
|
use itertools::Itertools;
|
|
#[allow(unused_imports)]
|
|
use wasm_bindgen::prelude::wasm_bindgen;
|
|
|
|
/// Options for conducting an STV count
|
|
#[derive(Builder, Constructor)]
|
|
pub struct STVOptions {
|
|
/// Round surplus fractions to specified decimal places
|
|
#[builder(default="None")]
|
|
pub round_surplus_fractions: Option<usize>,
|
|
|
|
/// Round ballot values to specified decimal places
|
|
#[builder(default="None")]
|
|
pub round_values: Option<usize>,
|
|
|
|
/// Round votes to specified decimal places
|
|
#[builder(default="None")]
|
|
pub round_votes: Option<usize>,
|
|
|
|
/// Round quota to specified decimal places
|
|
#[builder(default="None")]
|
|
pub round_quota: Option<usize>,
|
|
|
|
/// How to round votes in transfer table
|
|
#[builder(default="RoundSubtransfersMode::SingleStep")]
|
|
pub round_subtransfers: RoundSubtransfersMode,
|
|
|
|
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
|
#[builder(default=r#"String::from("0.001%")"#)]
|
|
pub meek_surplus_tolerance: String,
|
|
|
|
/// Quota type
|
|
#[builder(default="QuotaType::Droop")]
|
|
pub quota: QuotaType,
|
|
|
|
/// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota
|
|
#[builder(default="QuotaCriterion::Greater")]
|
|
pub quota_criterion: QuotaCriterion,
|
|
|
|
/// Whether to apply a form of progressive quota
|
|
#[builder(default="QuotaMode::Static")]
|
|
pub quota_mode: QuotaMode,
|
|
|
|
/// Tie-breaking method
|
|
#[builder(default="vec![TieStrategy::Prompt]")]
|
|
pub ties: Vec<TieStrategy>,
|
|
|
|
/// Method of surplus distributions
|
|
#[builder(default="SurplusMethod::WIG")]
|
|
pub surplus: SurplusMethod,
|
|
|
|
/// (Gregory STV) Order to distribute surpluses
|
|
#[builder(default="SurplusOrder::BySize")]
|
|
pub surplus_order: SurplusOrder,
|
|
|
|
/// (Gregory STV) Examine only transferable papers during surplus distributions
|
|
#[builder(default="false")]
|
|
pub transferable_only: bool,
|
|
|
|
/// (Gregory STV) If --transferable-only, calculate value of transferable papers by subtracting value of non-transferable papers
|
|
#[builder(default="false")]
|
|
pub subtract_nontransferable: bool,
|
|
|
|
/// (Gregory STV) Method of exclusions
|
|
#[builder(default="ExclusionMethod::SingleStage")]
|
|
pub exclusion: ExclusionMethod,
|
|
|
|
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
|
|
#[builder(default="false")]
|
|
pub meek_nz_exclusion: bool,
|
|
|
|
/// (Hare) Method of drawing a sample
|
|
#[builder(default="SampleMethod::StratifyLR")]
|
|
pub sample: SampleMethod,
|
|
|
|
/// (Hare) Sample-based methods: Check for candidate election after each individual ballot paper transfer
|
|
#[builder(default="false")]
|
|
pub sample_per_ballot: bool,
|
|
|
|
/// Bulk elect as soon as continuing candidates fill all remaining vacancies
|
|
#[builder(default="true")]
|
|
pub early_bulk_elect: bool,
|
|
|
|
/// Use bulk exclusion
|
|
#[builder(default="false")]
|
|
pub bulk_exclude: bool,
|
|
|
|
/// Defer surplus distributions if possible
|
|
#[builder(default="false")]
|
|
pub defer_surpluses: bool,
|
|
|
|
/// Elect candidates on meeting the quota, rather than on surpluses being distributed; (Meek STV) Immediately elect candidates even if keep values have not converged
|
|
#[builder(default="true")]
|
|
pub immediate_elect: bool,
|
|
|
|
/// On exclusion, exclude any candidate with this many votes or fewer
|
|
#[builder(default="\"0\".to_string()")]
|
|
pub min_threshold: String,
|
|
|
|
/// Path to constraints file (used only for [STVOptions::describe])
|
|
#[builder(default="None")]
|
|
pub constraints_path: Option<String>,
|
|
|
|
/// Mode of handling constraints
|
|
#[builder(default="ConstraintMode::GuardDoom")]
|
|
pub constraint_mode: ConstraintMode,
|
|
|
|
/// (CLI) Hide excluded candidates from results report
|
|
#[builder(default="false")]
|
|
pub hide_excluded: bool,
|
|
|
|
/// (CLI) Sort candidates by votes in results report
|
|
#[builder(default="false")]
|
|
pub sort_votes: bool,
|
|
|
|
/// (CLI) Show details of transfers to candidates during surplus distributions/candidate exclusions
|
|
#[builder(default="false")]
|
|
pub transfers_detail: bool,
|
|
|
|
/// Print votes to specified decimal places in results report
|
|
#[builder(default="2")]
|
|
pub pp_decimals: usize,
|
|
}
|
|
|
|
impl STVOptions {
|
|
/// Converts the [STVOptions] into CLI argument representation
|
|
pub fn describe<N: Number>(&self) -> String {
|
|
let mut flags = Vec::new();
|
|
let n_str = N::describe_opt(); if !n_str.is_empty() { flags.push(N::describe_opt()) };
|
|
if self.surplus != SurplusMethod::IHare && self.surplus != SurplusMethod::Hare {
|
|
if let Some(dps) = self.round_surplus_fractions { flags.push(format!("--round-surplus-fractions {}", dps)); }
|
|
if let Some(dps) = self.round_values { flags.push(format!("--round-values {}", dps)); }
|
|
if let Some(dps) = self.round_votes { flags.push(format!("--round-votes {}", dps)); }
|
|
}
|
|
if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); }
|
|
if self.surplus != SurplusMethod::Meek && self.round_subtransfers != RoundSubtransfersMode::SingleStep { flags.push(self.round_subtransfers.describe()); }
|
|
if self.surplus == SurplusMethod::Meek && self.meek_surplus_tolerance != "0.001%" { flags.push(format!("--meek-surplus-tolerance {}", self.meek_surplus_tolerance)); }
|
|
if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); }
|
|
if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); }
|
|
if self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); }
|
|
let ties_str = self.ties.iter().map(|t| t.describe()).join(" ");
|
|
if ties_str != "prompt" { flags.push(format!("--ties {}", ties_str)); }
|
|
for t in self.ties.iter() { if let TieStrategy::Random(seed) = t { flags.push(format!("--random-seed {}", seed)); } }
|
|
if self.surplus != SurplusMethod::WIG { flags.push(self.surplus.describe()); }
|
|
if self.surplus != SurplusMethod::Meek {
|
|
if self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); }
|
|
if self.transferable_only { flags.push("--transferable-only".to_string()); }
|
|
if self.subtract_nontransferable { flags.push("--subtract-nontransferable".to_string()); }
|
|
if self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); }
|
|
}
|
|
if self.surplus == SurplusMethod::Meek && self.meek_nz_exclusion { flags.push("--meek-nz-exclusion".to_string()); }
|
|
if (self.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare) && self.sample != SampleMethod::StratifyLR { flags.push(self.sample.describe()); }
|
|
if (self.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare) && self.sample_per_ballot { flags.push("--sample-per-ballot".to_string()); }
|
|
if !self.early_bulk_elect { flags.push("--no-early-bulk-elect".to_string()); }
|
|
if self.bulk_exclude { flags.push("--bulk-exclude".to_string()); }
|
|
if self.defer_surpluses { flags.push("--defer-surpluses".to_string()); }
|
|
if !self.immediate_elect { flags.push("--no-immediate-elect".to_string()); }
|
|
if self.min_threshold != "0" { flags.push(format!("--min-threshold {}", self.min_threshold)); }
|
|
if let Some(path) = &self.constraints_path {
|
|
flags.push(format!("--constraints {}", path));
|
|
if self.constraint_mode != ConstraintMode::GuardDoom { flags.push(self.constraint_mode.describe()); }
|
|
}
|
|
if self.hide_excluded { flags.push("--hide-excluded".to_string()); }
|
|
if self.sort_votes { flags.push("--sort-votes".to_string()); }
|
|
if self.transfers_detail { flags.push("--transfers-detail".to_string()); }
|
|
if self.pp_decimals != 2 { flags.push(format!("--pp-decimals {}", self.pp_decimals)); }
|
|
return flags.join(" ");
|
|
}
|
|
|
|
/// Validate the combination of [STVOptions] and error if invalid
|
|
pub fn validate(&self) -> Result<(), STVError> {
|
|
if self.surplus == SurplusMethod::Meek {
|
|
if self.quota_mode == QuotaMode::ERS97 {
|
|
// Invalid because keep values cannot be calculated for a candidate elected with less than a surplus
|
|
return Err(STVError::InvalidOptions("--surplus meek is incompatible with --quota-mode ers97"));
|
|
}
|
|
if self.quota_mode == QuotaMode::ERS76 {
|
|
// Invalid because keep values cannot be calculated for a candidate elected with less than a surplus
|
|
return Err(STVError::InvalidOptions("--surplus meek is incompatible with --quota-mode ers76"));
|
|
}
|
|
if self.quota_mode == QuotaMode::DynamicByActive {
|
|
// Invalid because all votes are "active" in Meek STV
|
|
return Err(STVError::InvalidOptions("--surplus meek is incompatible with --quota-mode dynamic_by_active"));
|
|
}
|
|
if self.transferable_only {
|
|
// Invalid because this would imply a different keep value applies to nontransferable ballots (?)
|
|
// TODO: NYI?
|
|
return Err(STVError::InvalidOptions("--surplus meek is incompatible with --transferable-only"));
|
|
}
|
|
if self.exclusion != ExclusionMethod::SingleStage {
|
|
// Invalid because Meek STV is independent of order of exclusion, so segmented exclusion has no impact
|
|
return Err(STVError::InvalidOptions("--surplus meek requires --exclusion single_stage"));
|
|
}
|
|
if self.constraints_path.is_some() && self.constraint_mode == ConstraintMode::RepeatCount {
|
|
// TODO: NYI?
|
|
return Err(STVError::InvalidOptions("--constraint-mode repeat_count requires a Gregory method for --surplus"));
|
|
}
|
|
}
|
|
if self.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare {
|
|
if self.round_quota != Some(0) {
|
|
// Invalid because votes are counted only in whole numbers
|
|
return Err(STVError::InvalidOptions("--surplus ihare and --surplus hare require --round-quota 0"));
|
|
}
|
|
if self.sample == SampleMethod::StratifyLR && self.sample_per_ballot {
|
|
// Invalid because a stratification cannot be made until all relevant ballots are transferred
|
|
return Err(STVError::InvalidOptions("--sample stratify is incompatible with --sample-per-ballot"));
|
|
}
|
|
if self.sample_per_ballot && !self.immediate_elect {
|
|
// Invalid because otherwise --sample-per-ballot would be ineffectual
|
|
return Err(STVError::InvalidOptions("--sample-per-ballot is incompatible with --no-immediate-elect"));
|
|
}
|
|
if self.constraints_path.is_some() && self.constraint_mode == ConstraintMode::RepeatCount {
|
|
// TODO: NYI?
|
|
return Err(STVError::InvalidOptions("--constraint-mode repeat_count requires a Gregory method for --surplus"));
|
|
}
|
|
}
|
|
if self.subtract_nontransferable {
|
|
if self.surplus != SurplusMethod::WIG {
|
|
// Invalid because other methods do not distinguish between ballots of different value during surplus transfer
|
|
return Err(STVError::InvalidOptions("--subtract-nontransferable requires --surplus wig"));
|
|
}
|
|
if !self.transferable_only {
|
|
// Invalid because nontransferables are only subtracted with --transferable-only
|
|
return Err(STVError::InvalidOptions("--subtract-nontransferable requires --transferable-only"));
|
|
}
|
|
}
|
|
if !self.immediate_elect && self.surplus_order != SurplusOrder::BySize {
|
|
// Invalid because there is no other metric to determine which surplus to distribute
|
|
return Err(STVError::InvalidOptions("--no-immediate-elect requires --surplus-order by_size"));
|
|
}
|
|
if self.min_threshold != "0" && self.defer_surpluses {
|
|
// TODO: NYI
|
|
return Err(STVError::InvalidOptions("--min-threshold is incompatible with --defer-surpluses (not yet implemented)"));
|
|
}
|
|
if self.round_subtransfers == RoundSubtransfersMode::ByValueAndSource && self.bulk_exclude {
|
|
// TODO: NYI
|
|
return Err(STVError::InvalidOptions("--round-subtransfers by_value_and_source is incompatible with --bulk-exclude (not yet implemented)"));
|
|
}
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
/// Enum of options for [STVOptions::round_subtransfers]
|
|
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
|
#[derive(Clone, Copy)]
|
|
#[derive(PartialEq)]
|
|
pub enum RoundSubtransfersMode {
|
|
/// Do not round subtransfers (only round final number of votes credited)
|
|
SingleStep,
|
|
/// Round in subtransfers according to the value when received
|
|
ByValue,
|
|
/// Round in subtransfers according to the candidate from who each vote was received, and the value when received
|
|
ByValueAndSource,
|
|
/// Round in subtransfers according to parcel
|
|
ByParcel,
|
|
/// Sum and round transfers individually for each ballot paper
|
|
PerBallot,
|
|
}
|
|
|
|
impl RoundSubtransfersMode {
|
|
/// Convert to CLI argument representation
|
|
fn describe(self) -> String {
|
|
match self {
|
|
RoundSubtransfersMode::SingleStep => "--round-subtransfers single_step",
|
|
RoundSubtransfersMode::ByValue => "--round-subtransfers by_value",
|
|
RoundSubtransfersMode::ByValueAndSource => "--round-subtransfers by_value_and_source",
|
|
RoundSubtransfersMode::ByParcel => "--round-subtransfers by_parcel",
|
|
RoundSubtransfersMode::PerBallot => "--round-subtransfers per_ballot",
|
|
}.to_string()
|
|
}
|
|
}
|
|
|
|
impl<S: AsRef<str>> From<S> for RoundSubtransfersMode {
|
|
fn from(s: S) -> Self {
|
|
match s.as_ref() {
|
|
"single_step" => RoundSubtransfersMode::SingleStep,
|
|
"by_value" => RoundSubtransfersMode::ByValue,
|
|
"by_value_and_source" => RoundSubtransfersMode::ByValueAndSource,
|
|
"by_parcel" => RoundSubtransfersMode::ByParcel,
|
|
"per_ballot" => RoundSubtransfersMode::PerBallot,
|
|
_ => panic!("Invalid --round-subtransfers"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enum of options for [STVOptions::quota]
|
|
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
|
#[derive(Clone, Copy)]
|
|
#[derive(PartialEq)]
|
|
pub enum QuotaType {
|
|
/// Droop quota
|
|
Droop,
|
|
/// Hare quota
|
|
Hare,
|
|
/// Exact Droop quota (Newland–Britton/Hagenbach-Bischoff quota)
|
|
DroopExact,
|
|
/// Exact Hare quota
|
|
HareExact,
|
|
}
|
|
|
|
impl QuotaType {
|
|
/// Convert to CLI argument representation
|
|
fn describe(self) -> String {
|
|
match self {
|
|
QuotaType::Droop => "--quota droop",
|
|
QuotaType::Hare => "--quota hare",
|
|
QuotaType::DroopExact => "--quota droop_exact",
|
|
QuotaType::HareExact => "--quota hare_exact",
|
|
}.to_string()
|
|
}
|
|
}
|
|
|
|
impl<S: AsRef<str>> From<S> for QuotaType {
|
|
fn from(s: S) -> Self {
|
|
match s.as_ref() {
|
|
"droop" => QuotaType::Droop,
|
|
"hare" => QuotaType::Hare,
|
|
"droop_exact" => QuotaType::DroopExact,
|
|
"hare_exact" => QuotaType::HareExact,
|
|
_ => panic!("Invalid --quota"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enum of options for [STVOptions::quota_criterion]
|
|
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
|
#[derive(Clone, Copy)]
|
|
#[derive(PartialEq)]
|
|
pub enum QuotaCriterion {
|
|
/// Elect candidates on equalling or exceeding the quota
|
|
GreaterOrEqual,
|
|
/// Elect candidates on strictly exceeding the quota
|
|
Greater,
|
|
}
|
|
|
|
impl QuotaCriterion {
|
|
/// Convert to CLI argument representation
|
|
fn describe(self) -> String {
|
|
match self {
|
|
QuotaCriterion::GreaterOrEqual => "--quota-criterion geq",
|
|
QuotaCriterion::Greater => "--quota-criterion gt",
|
|
}.to_string()
|
|
}
|
|
}
|
|
|
|
impl<S: AsRef<str>> From<S> for QuotaCriterion {
|
|
fn from(s: S) -> Self {
|
|
match s.as_ref() {
|
|
"geq" => QuotaCriterion::GreaterOrEqual,
|
|
"gt" => QuotaCriterion::Greater,
|
|
_ => panic!("Invalid --quota-criterion"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enum of options for [STVOptions::quota_mode]
|
|
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
|
#[derive(Clone, Copy)]
|
|
#[derive(PartialEq)]
|
|
pub enum QuotaMode {
|
|
/// Static quota
|
|
Static,
|
|
/// Static quota with ERS97 rules
|
|
ERS97,
|
|
/// Static quota with ERS76 rules
|
|
ERS76,
|
|
/// Dynamic quota by total vote
|
|
DynamicByTotal,
|
|
/// Dynamic quota by active vote
|
|
DynamicByActive,
|
|
}
|
|
|
|
impl QuotaMode {
|
|
/// Convert to CLI argument representation
|
|
fn describe(self) -> String {
|
|
match self {
|
|
QuotaMode::Static => "--quota-mode static",
|
|
QuotaMode::ERS97 => "--quota-mode ers97",
|
|
QuotaMode::ERS76 => "--quota-mode ers76",
|
|
QuotaMode::DynamicByTotal => "--quota-mode dynamic_by_total",
|
|
QuotaMode::DynamicByActive => "--quota-mode dynamic_by_active",
|
|
}.to_string()
|
|
}
|
|
}
|
|
|
|
impl<S: AsRef<str>> From<S> for QuotaMode {
|
|
fn from(s: S) -> Self {
|
|
match s.as_ref() {
|
|
"static" => QuotaMode::Static,
|
|
"ers97" => QuotaMode::ERS97,
|
|
"ers76" => QuotaMode::ERS76,
|
|
"dynamic_by_total" => QuotaMode::DynamicByTotal,
|
|
"dynamic_by_active" => QuotaMode::DynamicByActive,
|
|
_ => panic!("Invalid --quota-mode"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enum of options for [STVOptions::surplus]
|
|
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
|
#[derive(Clone, Copy)]
|
|
#[derive(PartialEq)]
|
|
pub enum SurplusMethod {
|
|
/// Weighted inclusive Gregory method
|
|
WIG,
|
|
/// Unweighted inclusive Gregory method
|
|
UIG,
|
|
/// Exclusive Gregory method (last bundle)
|
|
EG,
|
|
/// Meek method
|
|
Meek,
|
|
/// Inclusive Hare method (random subset)
|
|
IHare,
|
|
/// (Exclusive) Hare method (random subset)
|
|
Hare,
|
|
}
|
|
|
|
impl SurplusMethod {
|
|
/// Convert to CLI argument representation
|
|
fn describe(self) -> String {
|
|
match self {
|
|
SurplusMethod::WIG => "--surplus wig",
|
|
SurplusMethod::UIG => "--surplus uig",
|
|
SurplusMethod::EG => "--surplus eg",
|
|
SurplusMethod::Meek => "--surplus meek",
|
|
SurplusMethod::IHare => "--surplus ihare",
|
|
SurplusMethod::Hare => "--surplus hare",
|
|
}.to_string()
|
|
}
|
|
|
|
/// Returns `true` if this is a weighted method
|
|
pub fn is_weighted(&self) -> bool {
|
|
return match self {
|
|
SurplusMethod::WIG => { true }
|
|
SurplusMethod::UIG | SurplusMethod::EG => { false }
|
|
_ => unreachable!()
|
|
};
|
|
}
|
|
}
|
|
|
|
impl<S: AsRef<str>> From<S> for SurplusMethod {
|
|
fn from(s: S) -> Self {
|
|
match s.as_ref() {
|
|
"wig" => SurplusMethod::WIG,
|
|
"uig" => SurplusMethod::UIG,
|
|
"eg" => SurplusMethod::EG,
|
|
"meek" => SurplusMethod::Meek,
|
|
"ihare" | "ih" | "cincinnati" => SurplusMethod::IHare, // Inclusive Hare method used to be erroneously referred to as "Cincinnati" method - accept for backwards compatibility
|
|
"hare" | "eh" => SurplusMethod::Hare,
|
|
_ => panic!("Invalid --surplus"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enum of options for [STVOptions::surplus_order]
|
|
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
|
#[derive(Clone, Copy)]
|
|
#[derive(PartialEq)]
|
|
pub enum SurplusOrder {
|
|
/// Transfer the largest surplus first, even if it arose at a later stage of the count
|
|
BySize,
|
|
/// Transfer the surplus of the candidate elected first, even if it is smaller than another
|
|
ByOrder,
|
|
}
|
|
|
|
impl SurplusOrder {
|
|
/// Convert to CLI argument representation
|
|
fn describe(self) -> String {
|
|
match self {
|
|
SurplusOrder::BySize => "--surplus-order by_size",
|
|
SurplusOrder::ByOrder => "--surplus-order by_order",
|
|
}.to_string()
|
|
}
|
|
}
|
|
|
|
impl<S: AsRef<str>> From<S> for SurplusOrder {
|
|
fn from(s: S) -> Self {
|
|
match s.as_ref() {
|
|
"by_size" => SurplusOrder::BySize,
|
|
"by_order" => SurplusOrder::ByOrder,
|
|
_ => panic!("Invalid --surplus-order"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enum of options for [STVOptions::exclusion]
|
|
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
|
#[derive(Clone, Copy)]
|
|
#[derive(PartialEq)]
|
|
pub enum ExclusionMethod {
|
|
/// Transfer all ballot papers of an excluded candidate in one stage
|
|
SingleStage,
|
|
/// Transfer the ballot papers of an excluded candidate in descending order of accumulated transfer value
|
|
ByValue,
|
|
/// Transfer the ballot papers of an excluded candidate according to the candidate who transferred the papers to the excluded candidate, in the order the transferring candidates were elected or excluded
|
|
BySource,
|
|
/// Transfer the ballot papers of an excluded candidate parcel by parcel in the order received
|
|
ParcelsByOrder,
|
|
/// Reset count and re-iterate from count of first preferences
|
|
ResetAndReiterate,
|
|
}
|
|
|
|
impl ExclusionMethod {
|
|
/// Convert to CLI argument representation
|
|
fn describe(self) -> String {
|
|
match self {
|
|
ExclusionMethod::SingleStage => "--exclusion single_stage",
|
|
ExclusionMethod::ByValue => "--exclusion by_value",
|
|
ExclusionMethod::BySource => "--exclusion by_source",
|
|
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
|
|
ExclusionMethod::ResetAndReiterate => "--exclusion reset_and_reiterate",
|
|
}.to_string()
|
|
}
|
|
}
|
|
|
|
impl<S: AsRef<str>> From<S> for ExclusionMethod {
|
|
fn from(s: S) -> Self {
|
|
match s.as_ref() {
|
|
"single_stage" => ExclusionMethod::SingleStage,
|
|
"by_value" => ExclusionMethod::ByValue,
|
|
"by_source" => ExclusionMethod::BySource,
|
|
"parcels_by_order" => ExclusionMethod::ParcelsByOrder,
|
|
"reset_and_reiterate" => ExclusionMethod::ResetAndReiterate,
|
|
"wright" => ExclusionMethod::ResetAndReiterate,
|
|
_ => panic!("Invalid --exclusion"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enum of options for [STVOptions::sample]
|
|
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
|
#[derive(Clone, Copy)]
|
|
#[derive(PartialEq)]
|
|
pub enum SampleMethod {
|
|
/// Stratify the ballots into parcels according to next available preference and transfer the last ballots from each parcel; round fractions according to largest remainders
|
|
StratifyLR,
|
|
// Stratify the ballots into parcels according to next available preference and transfer the last ballots from each parcel; disregard fractions
|
|
//StratifyFloor,
|
|
/// Transfer the last ballots
|
|
ByOrder,
|
|
/// Transfer every n-th ballot, Cincinnati style
|
|
Cincinnati,
|
|
}
|
|
|
|
impl SampleMethod {
|
|
/// Convert to CLI argument representation
|
|
fn describe(self) -> String {
|
|
match self {
|
|
SampleMethod::StratifyLR => "--sample stratify",
|
|
//SampleMethod::StratifyFloor => "--sample stratify_floor",
|
|
SampleMethod::ByOrder => "--sample by_order",
|
|
SampleMethod::Cincinnati => "--sample cincinnati",
|
|
}.to_string()
|
|
}
|
|
}
|
|
|
|
impl<S: AsRef<str>> From<S> for SampleMethod {
|
|
fn from(s: S) -> Self {
|
|
match s.as_ref() {
|
|
"stratify" | "stratify_lr" => SampleMethod::StratifyLR,
|
|
//"stratify_floor" => SampleMethod::StratifyFloor,
|
|
"by_order" => SampleMethod::ByOrder,
|
|
"cincinnati" | "nth_ballot" => SampleMethod::Cincinnati,
|
|
_ => panic!("Invalid --sample-method"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Enum of options for [STVOptions::constraint_mode]
|
|
#[derive(Clone, Copy)]
|
|
#[derive(PartialEq)]
|
|
pub enum ConstraintMode {
|
|
/// Guard or doom candidates as soon as required to secure a conformant result
|
|
GuardDoom,
|
|
/// If constraints violated, exclude/reintroduce candidates as required and redistribute ballot papers
|
|
RepeatCount,
|
|
}
|
|
|
|
impl ConstraintMode {
|
|
/// Convert to CLI argument representation
|
|
fn describe(self) -> String {
|
|
match self {
|
|
ConstraintMode::GuardDoom => "--constraint-mode guard_doom",
|
|
ConstraintMode::RepeatCount => "--constraint-mode repeat_count",
|
|
}.to_string()
|
|
}
|
|
}
|
|
|
|
impl<S: AsRef<str>> From<S> for ConstraintMode {
|
|
fn from(s: S) -> Self {
|
|
match s.as_ref() {
|
|
"guard_doom" => ConstraintMode::GuardDoom,
|
|
"repeat_count" => ConstraintMode::RepeatCount,
|
|
_ => panic!("Invalid --constraint-mode"),
|
|
}
|
|
}
|
|
}
|