Tidying up

Refactor STV options implementations into separate file
Fix/update documentation
This commit is contained in:
RunasSudo 2022-08-22 11:35:20 +10:00
parent 55f2e8816a
commit 395de771fa
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
8 changed files with 645 additions and 619 deletions

View File

@ -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 };

View File

@ -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>,

View File

@ -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() {

View File

@ -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,
}

View File

@ -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);
}

View File

@ -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 {

620
src/stv/options.rs Normal file
View File

@ -0,0 +1,620 @@
/* OpenTally: Open-source election vote counting
* Copyright © 20212022 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"),
}
}
}

View File

@ -50,7 +50,7 @@ macro_rules! wasm_error {
// Init
/// Wrapper for [DynNum::set_kind]
// Wrapper for [DynNum::set_kind]
//#[wasm_bindgen]
//pub fn dynnum_set_kind(kind: NumKind) {
// DynNum::set_kind(kind);
@ -194,7 +194,7 @@ macro_rules! impl_type {
return [<CountState$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> {
return self.0.transfer_table.as_ref().map(|tt| tt.render_text(&opts.0));
}