Tidying up
Refactor STV options implementations into separate file Fix/update documentation
This commit is contained in:
parent
55f2e8816a
commit
395de771fa
|
@ -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<Option<(&'e Candidate, V)>>
|
||||
}
|
||||
|
||||
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 };
|
||||
|
|
|
@ -781,7 +781,7 @@ pub fn init_repeat_count<N: Number>(election: &mut Election<N>) {
|
|||
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<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<(), STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||||
|
|
|
@ -164,7 +164,7 @@ pub struct CountState<'a, N: Number> {
|
|||
|
||||
/// [ConstraintMatrix] for constrained elections
|
||||
pub constraint_matrix: Option<ConstraintMatrix>,
|
||||
/// [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<I: Iterator<Item=&'a CountCard<'a, N>>>(&self, count_cards: I) -> N {
|
||||
return count_cards.fold(N::new(), |mut acc, cc| {
|
||||
if !cc.finalised && &cc.votes > self.quota.as_ref().unwrap() {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
602
src/stv/mod.rs
602
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<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,
|
||||
/// 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<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,
|
||||
"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<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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error during the STV count
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum STVError {
|
||||
|
|
|
@ -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 <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,
|
||||
/// 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<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,
|
||||
"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<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)]
|
||||