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;
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct CandidateMap<'e, V> {
|
pub struct CandidateMap<'e, V> {
|
||||||
entries: Vec<Option<(&'e Candidate, V)>>
|
entries: Vec<Option<(&'e Candidate, V)>>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'e, V> CandidateMap<'e, V> {
|
impl<'e, V> CandidateMap<'e, V> {
|
||||||
/// See [HashMap::new]
|
/// See [HashMap::new](std::collections::HashMap::new)
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
entries: Vec::new()
|
entries: Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [HashMap::with_capacity]
|
/// See [HashMap::with_capacity](std::collections::HashMap::with_capacity)
|
||||||
pub fn with_capacity(capacity: usize) -> Self {
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
let mut ret = Self {
|
let mut ret = Self {
|
||||||
entries: Vec::with_capacity(capacity)
|
entries: Vec::with_capacity(capacity)
|
||||||
@ -50,26 +50,26 @@ impl<'e, V> CandidateMap<'e, V> {
|
|||||||
self.entries.resize_with(len, || None);
|
self.entries.resize_with(len, || None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [HashMap::len]
|
/// See [HashMap::len](std::collections::HashMap::len)
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
return self.entries.iter().filter(|e| e.is_some()).count();
|
return self.entries.iter().filter(|e| e.is_some()).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [HashMap::insert]
|
/// See [HashMap::insert](std::collections::HashMap::insert)
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn insert(&mut self, candidate: &'e Candidate, value: V) {
|
pub fn insert(&mut self, candidate: &'e Candidate, value: V) {
|
||||||
self.maybe_resize(candidate.index + 1);
|
self.maybe_resize(candidate.index + 1);
|
||||||
self.entries[candidate.index] = Some((candidate, value));
|
self.entries[candidate.index] = Some((candidate, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [HashMap::get]
|
/// See [HashMap::get](std::collections::HashMap::get)
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get(&self, candidate: &'e Candidate) -> Option<&V> {
|
pub fn get(&self, candidate: &'e Candidate) -> Option<&V> {
|
||||||
return self.entries.get(candidate.index).unwrap_or(&None).as_ref().map(|(_, v)| 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]
|
#[inline]
|
||||||
pub fn get_mut(&mut self, candidate: &'e Candidate) -> Option<&mut V> {
|
pub fn get_mut(&mut self, candidate: &'e Candidate) -> Option<&mut V> {
|
||||||
match self.entries.get_mut(candidate.index) {
|
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]
|
#[inline]
|
||||||
pub fn iter(&self) -> Iter<'_, 'e, V> {
|
pub fn iter(&self) -> Iter<'_, 'e, V> {
|
||||||
return Iter { map: &self, index: 0 };
|
return Iter { map: &self, index: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [HashMap::iter_mut]
|
/// See [HashMap::iter_mut](std::collections::HashMap::iter_mut)
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn iter_mut(&mut self) -> IterMut<'_, 'e, V> {
|
pub fn iter_mut(&mut self) -> IterMut<'_, 'e, V> {
|
||||||
return IterMut { map: self, index: 0 };
|
return IterMut { map: self, index: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [HashMap::values]
|
/// See [HashMap::values](std::collections::HashMap::values)
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn values(&self) -> Values<'_, 'e, V> {
|
pub fn values(&self) -> Values<'_, 'e, V> {
|
||||||
return Values { map: &self, index: 0 };
|
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);
|
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) {
|
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 mut rollback_candidates = CandidateMap::with_capacity(state.candidates.len());
|
||||||
let rollback_exhausted = state.exhausted.clone();
|
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 };
|
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>
|
pub fn rollback_one_stage<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<(), STVError>
|
||||||
where
|
where
|
||||||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||||||
|
@ -164,7 +164,7 @@ pub struct CountState<'a, N: Number> {
|
|||||||
|
|
||||||
/// [ConstraintMatrix] for constrained elections
|
/// [ConstraintMatrix] for constrained elections
|
||||||
pub constraint_matrix: Option<ConstraintMatrix>,
|
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>,
|
pub rollback_state: RollbackState<'a, N>,
|
||||||
|
|
||||||
/// Transfer table for this surplus/exclusion
|
/// 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
|
/// 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 {
|
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| {
|
return count_cards.fold(N::new(), |mut acc, cc| {
|
||||||
if !cc.finalised && &cc.votes > self.quota.as_ref().unwrap() {
|
if !cc.finalised && &cc.votes > self.quota.as_ref().unwrap() {
|
||||||
|
@ -30,13 +30,13 @@ use std::ops::{self, Deref, DerefMut};
|
|||||||
//#[wasm_bindgen]
|
//#[wasm_bindgen]
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub enum NumKind {
|
pub enum NumKind {
|
||||||
/// See [crate::numbers::fixed]
|
/// See [crate::numbers::Fixed]
|
||||||
Fixed,
|
Fixed,
|
||||||
/// See [crate::numbers::gfixed]
|
/// See [crate::numbers::GuardedFixed]
|
||||||
GuardedFixed,
|
GuardedFixed,
|
||||||
/// See [crate::numbers::native]
|
/// See [crate::numbers::NativeFloat64]
|
||||||
NativeFloat64,
|
NativeFloat64,
|
||||||
/// See [crate::numbers::rational_rug] or [crate::numbers::rational_num]
|
/// See [crate::numbers::Rational]
|
||||||
Rational,
|
Rational,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ impl Table {
|
|||||||
self.rows.push(row);
|
self.rows.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alias for [add_row]
|
/// Alias for [Table::add_row]
|
||||||
pub fn set_titles(&mut self, row: Row) {
|
pub fn set_titles(&mut self, row: Row) {
|
||||||
self.add_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")]
|
//#[cfg(target_arch = "wasm32")]
|
||||||
pub mod wasm;
|
pub mod wasm;
|
||||||
|
|
||||||
|
mod options;
|
||||||
|
pub use options::*;
|
||||||
|
|
||||||
use crate::candmap::CandidateMap;
|
use crate::candmap::CandidateMap;
|
||||||
use crate::constraints;
|
use crate::constraints;
|
||||||
use crate::numbers::Number;
|
|
||||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, RollbackState, StageKind, Vote};
|
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, RollbackState, StageKind, Vote};
|
||||||
|
use crate::numbers::Number;
|
||||||
use crate::sharandom::SHARandom;
|
use crate::sharandom::SHARandom;
|
||||||
use crate::ties::{self, TieStrategy};
|
use crate::ties::{self, TieStrategy};
|
||||||
|
|
||||||
use derive_builder::Builder;
|
|
||||||
use derive_more::Constructor;
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
#[allow(unused_imports)]
|
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops;
|
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
|
/// An error during the STV count
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum STVError {
|
pub enum STVError {
|
||||||
|
620
src/stv/options.rs
Normal file
620
src/stv/options.rs
Normal file
@ -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)]
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -50,7 +50,7 @@ macro_rules! wasm_error {
|
|||||||
|
|
||||||
// Init
|
// Init
|
||||||
|
|
||||||
/// Wrapper for [DynNum::set_kind]
|
// Wrapper for [DynNum::set_kind]
|
||||||
//#[wasm_bindgen]
|
//#[wasm_bindgen]
|
||||||
//pub fn dynnum_set_kind(kind: NumKind) {
|
//pub fn dynnum_set_kind(kind: NumKind) {
|
||||||
// DynNum::set_kind(kind);
|
// DynNum::set_kind(kind);
|
||||||
@ -194,7 +194,7 @@ macro_rules! impl_type {
|
|||||||
return [<CountState$type>](CountState::new(election.as_static()));
|
return [<CountState$type>](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<String> {
|
pub fn transfer_table_render_html(&self, opts: &STVOptions) -> Option<String> {
|
||||||
return self.0.transfer_table.as_ref().map(|tt| tt.render_text(&opts.0));
|
return self.0.transfer_table.as_ref().map(|tt| tt.render_text(&opts.0));
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user