From 395de771fa2a9b6e7c71d5ac8c43a177fe35ef5b Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Mon, 22 Aug 2022 11:35:20 +1000 Subject: [PATCH] Tidying up Refactor STV options implementations into separate file Fix/update documentation --- src/candmap.rs | 20 +- src/constraints.rs | 4 +- src/election.rs | 4 +- src/numbers/dynnum.rs | 8 +- src/stv/gregory/prettytable_html.rs | 2 +- src/stv/mod.rs | 602 +-------------------------- src/stv/options.rs | 620 ++++++++++++++++++++++++++++ src/stv/wasm.rs | 4 +- 8 files changed, 645 insertions(+), 619 deletions(-) create mode 100644 src/stv/options.rs diff --git a/src/candmap.rs b/src/candmap.rs index 5f2a877..e460f9b 100644 --- a/src/candmap.rs +++ b/src/candmap.rs @@ -19,21 +19,21 @@ use crate::election::Candidate; use std::ops::Index; -/// Mimics a [HashMap] on [Candidate]s, but internally is a [Vec] based on [Candidate::index] +/// Mimics a [HashMap](std::collections::HashMap) on [Candidate]s, but internally is a [Vec] based on [Candidate::index] #[derive(Clone)] pub struct CandidateMap<'e, V> { entries: Vec> } impl<'e, V> CandidateMap<'e, V> { - /// See [HashMap::new] + /// See [HashMap::new](std::collections::HashMap::new) pub fn new() -> Self { Self { entries: Vec::new() } } - /// See [HashMap::with_capacity] + /// See [HashMap::with_capacity](std::collections::HashMap::with_capacity) pub fn with_capacity(capacity: usize) -> Self { let mut ret = Self { entries: Vec::with_capacity(capacity) @@ -50,26 +50,26 @@ impl<'e, V> CandidateMap<'e, V> { self.entries.resize_with(len, || None); } - /// See [HashMap::len] + /// See [HashMap::len](std::collections::HashMap::len) #[inline] pub fn len(&self) -> usize { return self.entries.iter().filter(|e| e.is_some()).count(); } - /// See [HashMap::insert] + /// See [HashMap::insert](std::collections::HashMap::insert) #[inline] pub fn insert(&mut self, candidate: &'e Candidate, value: V) { self.maybe_resize(candidate.index + 1); self.entries[candidate.index] = Some((candidate, value)); } - /// See [HashMap::get] + /// See [HashMap::get](std::collections::HashMap::get) #[inline] pub fn get(&self, candidate: &'e Candidate) -> Option<&V> { return self.entries.get(candidate.index).unwrap_or(&None).as_ref().map(|(_, v)| v); } - /// See [HashMap::get_mut] + /// See [HashMap::get_mut](std::collections::HashMap::get_mut) #[inline] pub fn get_mut(&mut self, candidate: &'e Candidate) -> Option<&mut V> { match self.entries.get_mut(candidate.index) { @@ -82,19 +82,19 @@ impl<'e, V> CandidateMap<'e, V> { } } - /// See [HashMap::iter] + /// See [HashMap::iter](std::collections::HashMap::iter) #[inline] pub fn iter(&self) -> Iter<'_, 'e, V> { return Iter { map: &self, index: 0 }; } - /// See [HashMap::iter_mut] + /// See [HashMap::iter_mut](std::collections::HashMap::iter_mut) #[inline] pub fn iter_mut(&mut self) -> IterMut<'_, 'e, V> { return IterMut { map: self, index: 0 }; } - /// See [HashMap::values] + /// See [HashMap::values](std::collections::HashMap::values) #[inline] pub fn values(&self) -> Values<'_, 'e, V> { return Values { map: &self, index: 0 }; diff --git a/src/constraints.rs b/src/constraints.rs index 2e458a1..71a063d 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -781,7 +781,7 @@ pub fn init_repeat_count(election: &mut Election) { election.candidates.append(&mut new_candidates); } -/// Initialise the rollback for [ConstraintMode::TwoStage] +/// Initialise the rollback for [ConstraintMode::RepeatCount] pub fn init_repeat_count_rollback<'a, N: Number>(state: &mut CountState<'a, N>, constraint: &'a Constraint, group: &'a ConstrainedGroup) { let mut rollback_candidates = CandidateMap::with_capacity(state.candidates.len()); let rollback_exhausted = state.exhausted.clone(); @@ -794,7 +794,7 @@ pub fn init_repeat_count_rollback<'a, N: Number>(state: &mut CountState<'a, N>, state.rollback_state = RollbackState::NeedsRollback { candidates: Some(rollback_candidates), exhausted: Some(rollback_exhausted), constraint, group }; } -/// Process one stage of rollback for [ConstraintMode::TwoStage] +/// Process one stage of rollback for [ConstraintMode::RepeatCount] pub fn rollback_one_stage(state: &mut CountState, opts: &STVOptions) -> Result<(), STVError> where for<'r> &'r N: ops::Add<&'r N, Output=N>, diff --git a/src/election.rs b/src/election.rs index 0d39ec0..4c0a54f 100644 --- a/src/election.rs +++ b/src/election.rs @@ -164,7 +164,7 @@ pub struct CountState<'a, N: Number> { /// [ConstraintMatrix] for constrained elections pub constraint_matrix: Option, - /// [RollbackState] when using [ConstraintMode::Rollback] + /// [RollbackState] when using [ConstraintMode::RepeatCount](crate::stv::ConstraintMode::RepeatCount) pub rollback_state: RollbackState<'a, N>, /// Transfer table for this surplus/exclusion @@ -376,7 +376,7 @@ impl<'a, N: Number> CountState<'a, N> { /// Get the total surpluses of the given candidates /// - /// See [total_surplus]. + /// See [CountState::total_surplus]. pub fn total_surplus_of>>(&self, count_cards: I) -> N { return count_cards.fold(N::new(), |mut acc, cc| { if !cc.finalised && &cc.votes > self.quota.as_ref().unwrap() { diff --git a/src/numbers/dynnum.rs b/src/numbers/dynnum.rs index 7e4c78b..deaa90e 100644 --- a/src/numbers/dynnum.rs +++ b/src/numbers/dynnum.rs @@ -30,13 +30,13 @@ use std::ops::{self, Deref, DerefMut}; //#[wasm_bindgen] #[derive(Copy, Clone)] pub enum NumKind { - /// See [crate::numbers::fixed] + /// See [crate::numbers::Fixed] Fixed, - /// See [crate::numbers::gfixed] + /// See [crate::numbers::GuardedFixed] GuardedFixed, - /// See [crate::numbers::native] + /// See [crate::numbers::NativeFloat64] NativeFloat64, - /// See [crate::numbers::rational_rug] or [crate::numbers::rational_num] + /// See [crate::numbers::Rational] Rational, } diff --git a/src/stv/gregory/prettytable_html.rs b/src/stv/gregory/prettytable_html.rs index 9cfcf46..b24a899 100644 --- a/src/stv/gregory/prettytable_html.rs +++ b/src/stv/gregory/prettytable_html.rs @@ -36,7 +36,7 @@ impl Table { self.rows.push(row); } - /// Alias for [add_row] + /// Alias for [Table::add_row] pub fn set_titles(&mut self, row: Row) { self.add_row(row); } diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 0229382..e3722fd 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -26,615 +26,21 @@ pub mod sample; //#[cfg(target_arch = "wasm32")] pub mod wasm; +mod options; +pub use options::*; + use crate::candmap::CandidateMap; use crate::constraints; -use crate::numbers::Number; use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, RollbackState, StageKind, Vote}; +use crate::numbers::Number; use crate::sharandom::SHARandom; use crate::ties::{self, TieStrategy}; -use derive_builder::Builder; -use derive_more::Constructor; use itertools::Itertools; -#[allow(unused_imports)] -use wasm_bindgen::prelude::wasm_bindgen; use std::fmt; use std::ops; -/// 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, - - /// Round ballot values to specified decimal places - #[builder(default="None")] - pub round_values: Option, - - /// Round votes to specified decimal places - #[builder(default="None")] - pub round_votes: Option, - - /// Round quota to specified decimal places - #[builder(default="None")] - pub round_quota: Option, - - /// 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, - - /// 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, - - /// 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(&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> From 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> From 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> From 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> From 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> From 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> From 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, - /// Wright method (re-iterate) - Wright, -} - -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::Wright => "--exclusion wright", - }.to_string() - } -} - -impl> From 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, - "wright" => ExclusionMethod::Wright, - _ => 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> From 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> From for ConstraintMode { - fn from(s: S) -> Self { - match s.as_ref() { - "guard_doom" => ConstraintMode::GuardDoom, - "repeat_count" => ConstraintMode::RepeatCount, - _ => panic!("Invalid --constraint-mode"), - } - } -} - /// An error during the STV count #[derive(Debug, Eq, PartialEq)] pub enum STVError { diff --git a/src/stv/options.rs b/src/stv/options.rs new file mode 100644 index 0000000..869e566 --- /dev/null +++ b/src/stv/options.rs @@ -0,0 +1,620 @@ +/* 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 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, + + /// Round ballot values to specified decimal places + #[builder(default="None")] + pub round_values: Option, + + /// Round votes to specified decimal places + #[builder(default="None")] + pub round_votes: Option, + + /// Round quota to specified decimal places + #[builder(default="None")] + pub round_quota: Option, + + /// 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, + + /// 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, + + /// 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(&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> From 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> From 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> From 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> From 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> From 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> From 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, + /// Wright method (re-iterate) + Wright, +} + +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::Wright => "--exclusion wright", + }.to_string() + } +} + +impl> From 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, + "wright" => ExclusionMethod::Wright, + _ => 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> From 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> From for ConstraintMode { + fn from(s: S) -> Self { + match s.as_ref() { + "guard_doom" => ConstraintMode::GuardDoom, + "repeat_count" => ConstraintMode::RepeatCount, + _ => panic!("Invalid --constraint-mode"), + } + } +} diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index bfc993d..68f35d0 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -50,7 +50,7 @@ macro_rules! wasm_error { // Init -/// Wrapper for [DynNum::set_kind] +// Wrapper for [DynNum::set_kind] //#[wasm_bindgen] //pub fn dynnum_set_kind(kind: NumKind) { // DynNum::set_kind(kind); @@ -194,7 +194,7 @@ macro_rules! impl_type { return [](CountState::new(election.as_static())); } - /// Call [render_html](crate::stv::transfers::TransferTable::render_html) on [CountState::transfer_table] + /// Call [render_text](crate::stv::gregory::TransferTable::render_text) (as HTML) on [CountState::transfer_table] pub fn transfer_table_render_html(&self, opts: &STVOptions) -> Option { return self.0.transfer_table.as_ref().map(|tt| tt.render_text(&opts.0)); }