1766 lines
61 KiB
Rust
1766 lines
61 KiB
Rust
/* OpenTally: Open-source election vote counting
|
||
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU Affero General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU Affero General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU Affero General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#![allow(mutable_borrow_reservation_conflict)]
|
||
|
||
/// Gregory methods of surplus distributions
|
||
pub mod gregory;
|
||
/// Meek method of surplus distributions, etc.
|
||
pub mod meek;
|
||
/// Random sample methods of surplus distributions
|
||
pub mod sample;
|
||
|
||
/// WebAssembly wrappers
|
||
//#[cfg(target_arch = "wasm32")]
|
||
pub mod wasm;
|
||
|
||
use crate::constraints;
|
||
use crate::election::Election;
|
||
use crate::numbers::Number;
|
||
use crate::election::{Candidate, CandidateState, CountCard, CountState, StageKind, Vote};
|
||
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::collections::HashMap;
|
||
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,
|
||
|
||
/// Convert ballots with value >1 to multiple ballots of value 1 (used only for [STVOptions::describe])
|
||
#[builder(default="false")]
|
||
pub normalise_ballots: bool,
|
||
|
||
/// 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.normalise_ballots { flags.push("--normalise-ballots".to_string()); }
|
||
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 panic if invalid
|
||
pub fn validate(&self) -> Result<(), STVError> {
|
||
if self.surplus == SurplusMethod::Meek {
|
||
if self.quota_mode != QuotaMode::DynamicByTotal { return Err(STVError::InvalidOptions("--surplus meek requires --quota-mode dynamic_by_total")); }
|
||
if self.transferable_only { return Err(STVError::InvalidOptions("--surplus meek is incompatible with --transferable-only")); }
|
||
if self.exclusion != ExclusionMethod::SingleStage { return Err(STVError::InvalidOptions("--surplus meek requires --exclusion single_stage")); }
|
||
}
|
||
if self.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare {
|
||
if self.round_quota != Some(0) { return Err(STVError::InvalidOptions("--surplus ihare and --surplus hare require --round-quota 0")); }
|
||
if !self.normalise_ballots { return Err(STVError::InvalidOptions("--surplus ihare and --surplus hare require --normalise-ballots")); }
|
||
if self.sample == SampleMethod::StratifyLR && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify is incompatible with --sample-per-ballot")); }
|
||
//if self.sample == SampleMethod::StratifyFloor && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify_floor is incompatible with --sample-per-ballot")); }
|
||
if self.sample_per_ballot && !self.immediate_elect { return Err(STVError::InvalidOptions("--sample-per-ballot is incompatible with --no-immediate-elect")); }
|
||
}
|
||
if self.subtract_nontransferable && !self.transferable_only { return Err(STVError::InvalidOptions("--subtract-nontransferable requires --transferable-only")) }
|
||
if self.min_threshold != "0" && self.defer_surpluses { return Err(STVError::InvalidOptions("--min-threshold is incompatible with --defer-surpluses (not yet implemented)")); } // TODO: NYI
|
||
if self.round_subtransfers == RoundSubtransfersMode::ByValueAndSource && self.bulk_exclude { return Err(STVError::InvalidOptions("--round-subtransfers by_value_and_source is incompatible with --bulk-exclude (not yet implemented)")); } // TODO: NYI
|
||
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,
|
||
/// 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::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,
|
||
"per_ballot" => RoundSubtransfersMode::PerBallot,
|
||
_ => panic!("Invalid --sum-transfers"),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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,
|
||
/// TODO: NYI
|
||
Rollback,
|
||
}
|
||
|
||
impl ConstraintMode {
|
||
/// Convert to CLI argument representation
|
||
fn describe(self) -> String {
|
||
match self {
|
||
ConstraintMode::GuardDoom => "--constraint-mode guard_doom",
|
||
ConstraintMode::Rollback => "--constraint-mode rollback",
|
||
}.to_string()
|
||
}
|
||
}
|
||
|
||
impl<S: AsRef<str>> From<S> for ConstraintMode {
|
||
fn from(s: S) -> Self {
|
||
match s.as_ref() {
|
||
"guard_doom" => ConstraintMode::GuardDoom,
|
||
"rollback" => ConstraintMode::Rollback,
|
||
_ => panic!("Invalid --constraint-mode"),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// An error during the STV count
|
||
#[derive(Debug, Eq, PartialEq)]
|
||
pub enum STVError {
|
||
/// Options for the count are invalid
|
||
InvalidOptions(&'static str),
|
||
/// Tie could not be resolved
|
||
UnresolvedTie,
|
||
/// Unrecoverable error during the count
|
||
CannotCompleteCount(&'static str),
|
||
}
|
||
|
||
impl STVError {
|
||
/// Describe the error
|
||
pub fn describe(&self) -> &'static str {
|
||
match self {
|
||
STVError::InvalidOptions(s) => s,
|
||
STVError::UnresolvedTie => "Unable to resolve tie",
|
||
STVError::CannotCompleteCount(s) => s,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl fmt::Display for STVError {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
f.write_str(self.describe())?;
|
||
return Ok(());
|
||
}
|
||
}
|
||
|
||
/// Preprocess the given election
|
||
pub fn preprocess_election<N: Number>(election: &mut Election<N>, opts: &STVOptions) {
|
||
// Normalise ballots if requested
|
||
if opts.normalise_ballots {
|
||
election.normalise_ballots();
|
||
}
|
||
|
||
// Process equal rankings
|
||
election.realise_equal_rankings();
|
||
}
|
||
|
||
/// Distribute first preferences, and initialise other states such as the random number generator and tie-breaking rules
|
||
pub fn count_init<'a, N: Number>(state: &mut CountState<'a, N>, opts: &'a STVOptions) -> Result<(), STVError>
|
||
where
|
||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||
{
|
||
// Initialise RNG
|
||
for t in opts.ties.iter() {
|
||
if let TieStrategy::Random(seed) = t {
|
||
state.random = Some(SHARandom::new(seed));
|
||
}
|
||
}
|
||
|
||
constraints::update_constraints(state, opts);
|
||
|
||
distribute_first_preferences(state, opts);
|
||
calculate_quota(state, opts);
|
||
elect_hopefuls(state, opts, true)?;
|
||
init_tiebreaks(state, opts);
|
||
|
||
return Ok(());
|
||
}
|
||
|
||
/// Perform a single stage of the STV count
|
||
///
|
||
/// Returns `true` if the count is complete, otherwise `false`.
|
||
pub fn count_one_stage<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||
where
|
||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Neg<Output=N>,
|
||
{
|
||
state.transfer_table = None;
|
||
state.logger.entries.clear();
|
||
state.step_all();
|
||
|
||
// Finish count
|
||
if finished_before_stage(state) {
|
||
return Ok(true);
|
||
}
|
||
|
||
// Attempt early bulk election
|
||
if opts.early_bulk_elect {
|
||
if bulk_elect(state, opts)? {
|
||
return Ok(false);
|
||
}
|
||
}
|
||
|
||
// Continue exclusions
|
||
if continue_exclusion(state, opts)? {
|
||
calculate_quota(state, opts);
|
||
elect_hopefuls(state, opts, true)?;
|
||
update_tiebreaks(state, opts);
|
||
return Ok(false);
|
||
}
|
||
|
||
// Exclude doomed candidates
|
||
if exclude_doomed(state, opts)? {
|
||
calculate_quota(state, opts);
|
||
elect_hopefuls(state, opts, true)?;
|
||
update_tiebreaks(state, opts);
|
||
return Ok(false);
|
||
}
|
||
|
||
// Distribute surpluses
|
||
if distribute_surpluses(state, opts)? {
|
||
calculate_quota(state, opts);
|
||
elect_hopefuls(state, opts, true)?;
|
||
update_tiebreaks(state, opts);
|
||
return Ok(false);
|
||
}
|
||
|
||
// Attempt late bulk election
|
||
if bulk_elect(state, opts)? {
|
||
return Ok(false);
|
||
}
|
||
|
||
// Sanity check
|
||
let num_hopefuls = state.candidates.values()
|
||
.filter(|cc| cc.state == CandidateState::Hopeful)
|
||
.count();
|
||
if num_hopefuls == 0 {
|
||
return Err(STVError::CannotCompleteCount("Insufficient continuing candidates to complete count"));
|
||
}
|
||
|
||
// Exclude lowest hopeful
|
||
exclude_hopefuls(state, opts)?; // Cannot fail
|
||
calculate_quota(state, opts);
|
||
elect_hopefuls(state, opts, true)?;
|
||
update_tiebreaks(state, opts);
|
||
return Ok(false);
|
||
}
|
||
|
||
/// See [next_preferences]
|
||
struct NextPreferencesResult<'a, N> {
|
||
candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>,
|
||
exhausted: NextPreferencesEntry<'a, N>,
|
||
total_ballots: N,
|
||
}
|
||
|
||
/// See [next_preferences]
|
||
struct NextPreferencesEntry<'a, N> {
|
||
votes: Vec<Vote<'a, N>>,
|
||
num_ballots: N,
|
||
}
|
||
|
||
/// Count the given votes, grouping according to next available preference
|
||
fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a, N>>) -> NextPreferencesResult<'a, N> {
|
||
let mut result = NextPreferencesResult {
|
||
candidates: HashMap::new(),
|
||
exhausted: NextPreferencesEntry {
|
||
votes: Vec::new(),
|
||
num_ballots: N::new(),
|
||
},
|
||
total_ballots: N::new(),
|
||
};
|
||
|
||
for mut vote in votes.into_iter() {
|
||
result.total_ballots += &vote.ballot.orig_value;
|
||
|
||
let mut next_candidate = None;
|
||
while let Some(preference) = vote.next_preference() {
|
||
let candidate = &state.election.candidates[preference];
|
||
let count_card = &state.candidates[candidate];
|
||
|
||
if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state {
|
||
next_candidate = Some(candidate);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Have to structure like this to satisfy Rust's borrow checker
|
||
if let Some(candidate) = next_candidate {
|
||
if result.candidates.contains_key(candidate) {
|
||
let entry = result.candidates.get_mut(candidate).unwrap();
|
||
entry.num_ballots += &vote.ballot.orig_value;
|
||
entry.votes.push(vote);
|
||
} else {
|
||
let entry = NextPreferencesEntry {
|
||
num_ballots: vote.ballot.orig_value.clone(),
|
||
votes: vec![vote],
|
||
};
|
||
result.candidates.insert(candidate, entry);
|
||
}
|
||
} else {
|
||
result.exhausted.num_ballots += &vote.ballot.orig_value;
|
||
result.exhausted.votes.push(vote);
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// Distribute first preference votes
|
||
fn distribute_first_preferences<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
|
||
where
|
||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||
{
|
||
match opts.surplus {
|
||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG | SurplusMethod::IHare | SurplusMethod::Hare => {
|
||
gregory::distribute_first_preferences(state, opts);
|
||
}
|
||
SurplusMethod::Meek => {
|
||
meek::distribute_first_preferences(state, opts);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Calculate the quota, given the total vote, according to [STVOptions::quota]
|
||
fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N {
|
||
match opts.quota {
|
||
QuotaType::Droop | QuotaType::DroopExact => {
|
||
total /= N::from(seats + 1);
|
||
}
|
||
QuotaType::Hare | QuotaType::HareExact => {
|
||
total /= N::from(seats);
|
||
}
|
||
}
|
||
|
||
if let Some(dps) = opts.round_quota {
|
||
match opts.quota {
|
||
QuotaType::Droop | QuotaType::Hare => {
|
||
// Increment to next available increment
|
||
let mut factor = N::from(10);
|
||
factor.pow_assign(dps as i32);
|
||
total *= &factor;
|
||
total.floor_mut(0);
|
||
total += N::one();
|
||
total /= factor;
|
||
}
|
||
QuotaType::DroopExact | QuotaType::HareExact => {
|
||
// Round up to next available increment if necessary
|
||
total.ceil_mut(dps);
|
||
}
|
||
}
|
||
}
|
||
|
||
return total;
|
||
}
|
||
|
||
/// Update vote required for election according to ERS97/ERS76 rules
|
||
fn update_vre_ers<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||
if opts.quota_mode == QuotaMode::ERS76 && state.num_excluded == 0 && (state.num_elected == 0 || state.candidates.values().all(|cc| !cc.finalised)) {
|
||
// ERS76 rules: Do not update VRE until a surplus is distributed or candidate is excluded
|
||
state.vote_required_election = state.quota.clone();
|
||
return;
|
||
}
|
||
|
||
let mut log = String::new();
|
||
|
||
// Calculate active vote
|
||
let active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
|
||
match cc.state {
|
||
CandidateState::Elected => { if !cc.finalised && &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } }
|
||
_ => { acc + &cc.votes }
|
||
}
|
||
});
|
||
log.push_str(format!("Active vote is {:.dps$}, so the vote required for election is ", active_vote, dps=opts.pp_decimals).as_str());
|
||
|
||
let vote_req = active_vote / N::from(state.election.seats - state.num_elected + 1);
|
||
|
||
if &vote_req < state.quota.as_ref().unwrap() {
|
||
// VRE is less than the quota
|
||
if let Some(v) = &state.vote_required_election {
|
||
if &vote_req != v {
|
||
log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
|
||
state.vote_required_election = Some(vote_req);
|
||
state.logger.log_literal(log);
|
||
}
|
||
} else {
|
||
log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
|
||
state.vote_required_election = Some(vote_req);
|
||
state.logger.log_literal(log);
|
||
}
|
||
} else {
|
||
// VRE is not less than the quota, so use the quota
|
||
state.vote_required_election = state.quota.clone();
|
||
}
|
||
}
|
||
|
||
/// Update vote required for election if only one candidate remains, used in early bulk election
|
||
///
|
||
/// Assumes early bulk election is enabled.
|
||
fn update_vre_bulk<N: Number>(state: &mut CountState<N>, _opts: &STVOptions) {
|
||
// If --early-bulk-elect and one candidate remains, VRE is half of the active vote
|
||
// For display purposes only
|
||
|
||
if state.election.seats - state.num_elected == 1 {
|
||
//let mut log = String::new();
|
||
|
||
// Calculate active vote
|
||
let active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
|
||
match cc.state {
|
||
CandidateState::Elected => { if !cc.finalised && &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } }
|
||
_ => { acc + &cc.votes }
|
||
}
|
||
});
|
||
//log.push_str(format!("Active vote is {:.dps$}, so the vote required for election is ", active_vote, dps=opts.pp_decimals).as_str());
|
||
|
||
let vote_req = active_vote / N::from(state.election.seats - state.num_elected + 1);
|
||
|
||
if &vote_req < state.quota.as_ref().unwrap() {
|
||
// VRE is less than the quota
|
||
//if let Some(v) = &state.vote_required_election {
|
||
// if &vote_req != v {
|
||
//log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
|
||
state.vote_required_election = Some(vote_req);
|
||
//state.logger.log_literal(log);
|
||
// }
|
||
//} else {
|
||
//log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
|
||
// state.vote_required_election = Some(vote_req);
|
||
//state.logger.log_literal(log);
|
||
//}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Calculate the quota according to [STVOptions::quota]
|
||
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||
if state.quota.is_none() || opts.quota_mode == QuotaMode::DynamicByTotal {
|
||
// Calculate quota by total vote
|
||
|
||
let mut log = String::new();
|
||
|
||
// Calculate the total vote
|
||
let total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
|
||
log.push_str(format!("{:.dps$} usable votes, so the quota is ", total_vote, dps=opts.pp_decimals).as_str());
|
||
|
||
let quota = total_to_quota(total_vote, state.election.seats, opts);
|
||
|
||
log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
|
||
state.quota = Some(quota);
|
||
state.logger.log_literal(log);
|
||
|
||
} else if opts.quota_mode == QuotaMode::DynamicByActive {
|
||
// Calculate quota by active vote
|
||
|
||
let mut log = String::new();
|
||
|
||
// Calculate the active vote
|
||
let active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
|
||
match cc.state {
|
||
CandidateState::Elected => { if !cc.finalised && &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } }
|
||
_ => { acc + &cc.votes }
|
||
}
|
||
});
|
||
log.push_str(format!("Active vote is {:.dps$}, so the quota is is ", active_vote, dps=opts.pp_decimals).as_str());
|
||
|
||
// TODO: Calculate according to --quota ?
|
||
let quota = active_vote / N::from(state.election.seats - state.num_elected + 1);
|
||
|
||
log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
|
||
state.quota = Some(quota);
|
||
state.logger.log_literal(log);
|
||
}
|
||
|
||
if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 {
|
||
// ERS97/ERS76 rules
|
||
|
||
// -------------------------
|
||
// (ERS97) Reduce quota if allowable
|
||
|
||
if opts.quota_mode == QuotaMode::ERS97 && state.num_elected == 0 {
|
||
let mut log = String::new();
|
||
|
||
// Calculate the total vote
|
||
let total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
|
||
log.push_str(format!("{:.dps$} usable votes, so the quota is reduced to ", total_vote, dps=opts.pp_decimals).as_str());
|
||
|
||
let quota = total_to_quota(total_vote, state.election.seats, opts);
|
||
|
||
if "a < state.quota.as_ref().unwrap() {
|
||
log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
|
||
state.quota = Some(quota);
|
||
state.logger.log_literal(log);
|
||
}
|
||
}
|
||
|
||
// ------------------------------------
|
||
// Calculate vote required for election
|
||
|
||
if state.num_elected < state.election.seats {
|
||
update_vre_ers(state, opts);
|
||
}
|
||
} else {
|
||
// No ERS97/ERS76 rules
|
||
|
||
if opts.early_bulk_elect {
|
||
update_vre_bulk(state, opts);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Compare the candidate's votes with the specified target according to [STVOptions::quota_criterion]
|
||
fn cmp_quota_criterion<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
|
||
match opts.quota_criterion {
|
||
QuotaCriterion::GreaterOrEqual => {
|
||
return count_card.votes >= *quota;
|
||
}
|
||
QuotaCriterion::Greater => {
|
||
return count_card.votes > *quota;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Determine if the given candidate meets the vote required to be elected, according to [STVOptions::quota_criterion] and [STVOptions::quota_mode]
|
||
fn meets_vre<N: Number>(state: &CountState<N>, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
|
||
if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 {
|
||
// VRE is set because ERS97/ERS76 rules
|
||
return cmp_quota_criterion(state.vote_required_election.as_ref().unwrap(), count_card, opts);
|
||
} else {
|
||
// VRE is set (if at all) for display purposes only so ignore it here
|
||
return cmp_quota_criterion(state.quota.as_ref().unwrap(), count_card, opts);
|
||
}
|
||
}
|
||
|
||
/// Declare elected the continuing candidates leading for remaining vacancies if they cannot be overtaken
|
||
///
|
||
/// Returns `true` if any candidates were elected.
|
||
fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> {
|
||
if state.num_elected >= state.election.seats {
|
||
return Ok(false);
|
||
}
|
||
|
||
let num_vacancies = state.election.seats - state.num_elected;
|
||
|
||
let mut hopefuls: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter()
|
||
.map(|c| (c, &state.candidates[c]))
|
||
.filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded)
|
||
.collect();
|
||
|
||
hopefuls.sort_unstable_by(|a, b| b.1.votes.cmp(&a.1.votes));
|
||
|
||
let mut total_trailing = N::new();
|
||
// For leading candidates, count only untransferred surpluses
|
||
total_trailing += hopefuls.iter().take(num_vacancies).fold(N::new(), |acc, (_, cc)| {
|
||
if &cc.votes > state.quota.as_ref().unwrap() {
|
||
acc + &cc.votes - state.quota.as_ref().unwrap()
|
||
} else {
|
||
acc
|
||
}
|
||
});
|
||
// For trailing candidates, count all votes
|
||
total_trailing += hopefuls.iter().skip(num_vacancies).fold(N::new(), |acc, (_, cc)| acc + &cc.votes);
|
||
// Add finally any votes awaiting transfer
|
||
total_trailing += state.candidates.values().fold(N::zero(), |acc, cc| {
|
||
match cc.state {
|
||
CandidateState::Elected => { if !cc.finalised && &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } }
|
||
CandidateState::Hopeful | CandidateState::Guarded | CandidateState::Withdrawn => { acc }
|
||
CandidateState::Excluded | CandidateState::Doomed => { acc + &cc.votes }
|
||
}
|
||
});
|
||
|
||
if num_vacancies - 1 < hopefuls.len() {
|
||
let last_winner = hopefuls[num_vacancies - 1].1;
|
||
if last_winner.votes <= total_trailing {
|
||
return Ok(false);
|
||
}
|
||
}
|
||
|
||
let mut leading_hopefuls: Vec<&Candidate> = hopefuls.iter().take(num_vacancies).map(|(c, _)| *c).collect();
|
||
|
||
match constraints::try_constraints(state, &leading_hopefuls, CandidateState::Elected) {
|
||
Ok(_) => {}
|
||
Err(_) => { return Ok(false); } // Bulk election conflicts with constraints
|
||
}
|
||
|
||
// Bulk election is possible!
|
||
// Elect all leading candidates
|
||
|
||
if num_vacancies > 1 {
|
||
// Update VRE
|
||
// (If num_vacancies == 1, this has already been done in calculate_quota)
|
||
state.vote_required_election = Some(total_trailing);
|
||
}
|
||
|
||
while !leading_hopefuls.is_empty() && state.num_elected < state.election.seats {
|
||
let max_cands = ties::multiple_max_by(&leading_hopefuls, |c| &state.candidates[c].votes);
|
||
let candidate = if max_cands.len() > 1 {
|
||
choose_highest(state, opts, &max_cands, "Which candidate to elect?")?
|
||
} else {
|
||
max_cands[0]
|
||
};
|
||
|
||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||
count_card.state = CandidateState::Elected;
|
||
state.num_elected += 1;
|
||
count_card.order_elected = state.num_elected as isize;
|
||
|
||
state.logger.log_smart(
|
||
"As they cannot now be overtaken, {} is elected to fill the remaining vacancy.",
|
||
"As they cannot now be overtaken, {} are elected to fill the remaining vacancies.",
|
||
vec![&candidate.name]
|
||
);
|
||
|
||
leading_hopefuls.remove(leading_hopefuls.iter().position(|c| *c == candidate).unwrap());
|
||
}
|
||
|
||
constraints::update_constraints(state, opts);
|
||
|
||
return Ok(true);
|
||
}
|
||
|
||
/// Declare elected all candidates meeting the quota, and (if enabled) any candidates who can be early bulk elected because they have sufficiently many votes
|
||
///
|
||
/// Returns `true` if any candidates were elected.
|
||
fn elect_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, if_immediate: bool) -> Result<bool, STVError> {
|
||
if opts.immediate_elect != if_immediate && opts.surplus != SurplusMethod::Meek {
|
||
// For --no-immediate-elect
|
||
return Ok(false);
|
||
}
|
||
|
||
let mut cands_meeting_quota: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
|
||
.map(|c| (c, &state.candidates[c]))
|
||
.filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_vre(state, cc, opts) })
|
||
.collect();
|
||
|
||
// Sort by votes
|
||
cands_meeting_quota.sort_unstable_by(|a, b| b.1.votes.cmp(&a.1.votes));
|
||
let mut cands_meeting_quota: Vec<&Candidate> = cands_meeting_quota.iter().map(|(c, _)| *c).collect();
|
||
|
||
let elected = !cands_meeting_quota.is_empty();
|
||
|
||
while !cands_meeting_quota.is_empty() && state.num_elected < state.election.seats {
|
||
// Declare elected in descending order of votes
|
||
let max_cands = ties::multiple_max_by(&cands_meeting_quota, |c| &state.candidates[c].votes);
|
||
let candidate = if max_cands.len() > 1 {
|
||
choose_highest(state, opts, &max_cands, "Which candidate to elect?")?
|
||
} else {
|
||
max_cands[0]
|
||
};
|
||
|
||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||
count_card.state = CandidateState::Elected;
|
||
state.num_elected += 1;
|
||
count_card.order_elected = state.num_elected as isize;
|
||
|
||
let elected_on_quota;
|
||
if cmp_quota_criterion(state.quota.as_ref().unwrap(), count_card, opts) {
|
||
// Elected with a quota
|
||
elected_on_quota = true;
|
||
state.logger.log_smart(
|
||
"{} meets the quota and is elected.",
|
||
"{} meet the quota and are elected.",
|
||
vec![&candidate.name]
|
||
);
|
||
} else {
|
||
// Elected with vote required
|
||
elected_on_quota = false;
|
||
state.logger.log_smart(
|
||
"{} meets the vote required and is elected.",
|
||
"{} meet the vote required and are elected.",
|
||
vec![&candidate.name]
|
||
);
|
||
}
|
||
|
||
if constraints::update_constraints(state, opts) {
|
||
// Recheck as some candidates may have been doomed
|
||
let mut cmq: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
|
||
.map(|c| (c, &state.candidates[c]))
|
||
.filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_vre(state, cc, opts) })
|
||
.collect();
|
||
cmq.sort_unstable_by(|a, b| b.1.votes.cmp(&a.1.votes));
|
||
cands_meeting_quota = cmq.iter().map(|(c, _)| *c).collect();
|
||
} else {
|
||
cands_meeting_quota.remove(cands_meeting_quota.iter().position(|c| *c == candidate).unwrap());
|
||
}
|
||
|
||
if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 || opts.quota_mode == QuotaMode::DynamicByActive {
|
||
// Vote required for election may have changed
|
||
// ERS97: Check this after every elected candidate (cf. model election)
|
||
// ERS76: Check this after every candidate elected on a quota, but all at once for candidates elected on VRE (cf. model election)
|
||
if opts.quota_mode == QuotaMode::ERS97 || (opts.quota_mode == QuotaMode::ERS76 && elected_on_quota) || opts.quota_mode == QuotaMode::DynamicByActive {
|
||
calculate_quota(state, opts);
|
||
|
||
// Repeat in case vote required for election has changed
|
||
match elect_hopefuls(state, opts, true) {
|
||
Ok(_) => { break; }
|
||
Err(e) => { return Err(e); }
|
||
}
|
||
}
|
||
} else if opts.early_bulk_elect {
|
||
// Vote required for election may have changed for display purposes
|
||
update_vre_bulk(state, opts);
|
||
}
|
||
}
|
||
|
||
// Determine if early bulk election can be effected
|
||
if opts.early_bulk_elect {
|
||
if elect_sure_winners(state, opts)? {
|
||
return Ok(true);
|
||
}
|
||
}
|
||
|
||
return Ok(elected);
|
||
}
|
||
|
||
/// Determine whether the transfer of all surpluses can be deferred
|
||
///
|
||
/// The value of [STVOptions::defer_surpluses] is not taken into account and must be handled by the caller.
|
||
fn can_defer_surpluses<N: Number>(state: &CountState<N>, opts: &STVOptions, total_surpluses: &N) -> bool
|
||
where
|
||
for<'r> &'r N: ops::Sub<&'r N, Output=N>
|
||
{
|
||
// Do not defer if this could change the last 2 candidates
|
||
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||
.filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded)
|
||
.collect();
|
||
|
||
if hopefuls.len() < 2 {
|
||
return true;
|
||
}
|
||
|
||
hopefuls.sort_unstable_by(|(_, cc1), (_, cc2)| cc1.votes.cmp(&cc2.votes));
|
||
if total_surpluses > &(&hopefuls[1].1.votes - &hopefuls[0].1.votes) {
|
||
return false;
|
||
}
|
||
|
||
// Do not defer if this could affect a bulk exclusion
|
||
if opts.bulk_exclude {
|
||
let to_exclude = hopefuls_to_bulk_exclude(state, opts);
|
||
let num_to_exclude = to_exclude.len();
|
||
if num_to_exclude > 0 {
|
||
let total_excluded = to_exclude.into_iter()
|
||
.fold(N::new(), |acc, c| acc + &state.candidates[c].votes);
|
||
if total_surpluses > &(&hopefuls[num_to_exclude].1.votes - &total_excluded) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// Distribute surpluses according to [STVOptions::surplus]
|
||
///
|
||
/// Returns `true` if any surpluses were distributed.
|
||
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
||
where
|
||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Neg<Output=N>,
|
||
{
|
||
match opts.surplus {
|
||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG | SurplusMethod::IHare | SurplusMethod::Hare => {
|
||
return gregory::distribute_surpluses(state, opts);
|
||
}
|
||
SurplusMethod::Meek => {
|
||
return meek::distribute_surpluses(state, opts);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Determine if, with the proposed exclusion of num_to_exclude candidates (if any), a bulk election can be made
|
||
fn can_bulk_elect<N: Number>(state: &CountState<N>, num_to_exclude: usize) -> bool {
|
||
let num_hopefuls = state.election.candidates.iter()
|
||
.filter(|c| {
|
||
let cc = &state.candidates[c];
|
||
// Include doomed candidates here as these are included in num_to_exclude and so will later be subtracted
|
||
return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded || cc.state == CandidateState::Doomed;
|
||
})
|
||
.count();
|
||
|
||
if num_hopefuls - num_to_exclude > 0 && state.num_elected + num_hopefuls - num_to_exclude <= state.election.seats {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// Declare all continuing candidates to be elected
|
||
fn do_bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions, template1: &'static str, template2: &'static str) -> Result<(), STVError> {
|
||
let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter()
|
||
.filter(|c| {
|
||
let cc = &state.candidates[c];
|
||
return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded;
|
||
})
|
||
.collect();
|
||
|
||
// Bulk elect all remaining candidates
|
||
while !hopefuls.is_empty() {
|
||
let max_cands = ties::multiple_max_by(&hopefuls, |c| &state.candidates[c].votes);
|
||
let candidate = if max_cands.len() > 1 {
|
||
choose_highest(state, opts, &max_cands, "Which candidate to elect?")?
|
||
} else {
|
||
max_cands[0]
|
||
};
|
||
|
||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||
count_card.state = CandidateState::Elected;
|
||
state.num_elected += 1;
|
||
count_card.order_elected = state.num_elected as isize;
|
||
|
||
state.logger.log_smart(
|
||
template1,
|
||
template2,
|
||
vec![&candidate.name]
|
||
);
|
||
|
||
if constraints::update_constraints(state, opts) {
|
||
// Recheck as some candidates may have been doomed
|
||
hopefuls = state.election.candidates.iter()
|
||
.filter(|c| {
|
||
let cc = &state.candidates[c];
|
||
return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded;
|
||
})
|
||
.collect();
|
||
} else {
|
||
hopefuls.remove(hopefuls.iter().position(|c| *c == candidate).unwrap());
|
||
}
|
||
}
|
||
|
||
return Ok(());
|
||
}
|
||
|
||
/// Declare all continuing candidates elected, if the number equals the number of remaining vacancies
|
||
///
|
||
/// Returns `true` if any candidates were elected.
|
||
fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> {
|
||
if can_bulk_elect(state, 0) {
|
||
state.title = StageKind::BulkElection;
|
||
do_bulk_elect(state, opts, "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.")?;
|
||
return Ok(true);
|
||
}
|
||
return Ok(false);
|
||
}
|
||
|
||
/// Declare all doomed candidates excluded
|
||
///
|
||
/// Returns `true` if any candidates were excluded.
|
||
fn exclude_doomed<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||
where
|
||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||
{
|
||
let doomed: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
|
||
.filter(|c| state.candidates[c].state == CandidateState::Doomed)
|
||
.collect();
|
||
|
||
if !doomed.is_empty() {
|
||
let excluded_candidates;
|
||
if opts.bulk_exclude {
|
||
excluded_candidates = doomed;
|
||
} else {
|
||
// Exclude only the lowest-ranked doomed candidate
|
||
let min_cands = ties::multiple_min_by(&doomed, |c| &state.candidates[c].votes);
|
||
excluded_candidates = if min_cands.len() > 1 {
|
||
vec![choose_lowest(state, opts, &min_cands, "Which candidate to exclude?")?]
|
||
} else {
|
||
vec![min_cands[0]]
|
||
};
|
||
}
|
||
|
||
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
|
||
state.title = StageKind::ExclusionOf(excluded_candidates.clone());
|
||
state.logger.log_smart(
|
||
"Doomed candidate, {}, is excluded.",
|
||
"Doomed candidates, {}, are excluded.",
|
||
names
|
||
);
|
||
|
||
if opts.early_bulk_elect {
|
||
// Determine if the proposed exclusion would enable a bulk election
|
||
// See comment in exclude_hopefuls as to constraints
|
||
if can_bulk_elect(state, excluded_candidates.len()) {
|
||
// Exclude candidates without further transfers
|
||
let order_excluded = state.num_excluded + 1;
|
||
for candidate in excluded_candidates {
|
||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||
count_card.state = CandidateState::Excluded;
|
||
state.num_excluded += 1;
|
||
count_card.order_elected = -(order_excluded as isize);
|
||
}
|
||
|
||
do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.")?;
|
||
return Ok(true);
|
||
}
|
||
}
|
||
|
||
exclude_candidates(state, opts, excluded_candidates)?;
|
||
return Ok(true);
|
||
}
|
||
|
||
return Ok(false);
|
||
}
|
||
|
||
/// Determine which continuing candidates have votes equal to or below the minimum threshold
|
||
fn hopefuls_below_threshold<'a, N: Number>(state: &CountState<'a, N>, opts: &STVOptions) -> Vec<&'a Candidate> {
|
||
let min_threshold = N::parse(&opts.min_threshold);
|
||
|
||
let excluded_candidates: Vec<&Candidate> = state.candidates.iter()
|
||
.filter_map(|(c, cc)|
|
||
if cc.state == CandidateState::Hopeful && cc.votes <= min_threshold {
|
||
Some(*c)
|
||
} else {
|
||
None
|
||
})
|
||
.collect();
|
||
|
||
// Do not exclude if this violates constraints
|
||
match constraints::try_constraints(state, &excluded_candidates, CandidateState::Excluded) {
|
||
Ok(_) => { return excluded_candidates; }
|
||
Err(_) => { return Vec::new(); } // Bulk exclusion conflicts with constraints
|
||
}
|
||
}
|
||
|
||
/// Determine which continuing candidates could be excluded in a bulk exclusion
|
||
///
|
||
/// The value of [STVOptions::bulk_exclude] is not taken into account and must be handled by the caller.
|
||
fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &STVOptions) -> Vec<&'a Candidate> {
|
||
let mut excluded_candidates = Vec::new();
|
||
|
||
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||
.filter(|(_, cc)| cc.state == CandidateState::Hopeful)
|
||
.collect();
|
||
|
||
// Sort by votes
|
||
// NB: Unnecessary to handle ties, as ties will be rejected at "Do not exclude if this could change the order of exclusion"
|
||
hopefuls.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
|
||
|
||
let total_surpluses = state.candidates.iter()
|
||
.filter(|(_, cc)| !cc.finalised && &cc.votes > state.quota.as_ref().unwrap())
|
||
.fold(N::new(), |agg, (_, cc)| agg + &cc.votes - state.quota.as_ref().unwrap());
|
||
|
||
// Attempt to exclude as many candidates as possible
|
||
for i in 0..hopefuls.len() {
|
||
let try_exclude = &hopefuls[0..hopefuls.len()-i];
|
||
|
||
// Do not exclude if this leaves insufficient candidates
|
||
if state.num_elected + hopefuls.len() - try_exclude.len() < state.election.seats {
|
||
continue;
|
||
}
|
||
|
||
// Do not exclude if this could change the order of exclusion
|
||
let total_votes = try_exclude.iter().fold(N::new(), |agg, (_, cc)| agg + &cc.votes);
|
||
if i != 0 && total_votes + &total_surpluses >= hopefuls[hopefuls.len()-i].1.votes {
|
||
continue;
|
||
}
|
||
|
||
let try_exclude: Vec<&Candidate> = try_exclude.iter().map(|(c, _)| **c).collect();
|
||
|
||
// Do not exclude if this violates constraints
|
||
match constraints::try_constraints(state, &try_exclude, CandidateState::Excluded) {
|
||
Ok(_) => {}
|
||
Err(_) => { break; } // Bulk exclusion conflicts with constraints
|
||
}
|
||
|
||
excluded_candidates.extend(try_exclude);
|
||
break;
|
||
}
|
||
|
||
return excluded_candidates;
|
||
}
|
||
|
||
/// Exclude the lowest-ranked hopeful candidate(s)
|
||
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<(), STVError>
|
||
where
|
||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||
{
|
||
let mut excluded_candidates: Vec<&Candidate> = Vec::new();
|
||
|
||
if state.num_excluded == 0 {
|
||
if opts.bulk_exclude && opts.min_threshold == "0" {
|
||
// Proceed directly to bulk exclusion, as candidates with 0 votes will necessarily be included
|
||
} else {
|
||
// Exclude candidates below min threshold
|
||
excluded_candidates = hopefuls_below_threshold(state, opts);
|
||
}
|
||
}
|
||
|
||
// Attempt a bulk exclusion
|
||
if excluded_candidates.is_empty() && opts.bulk_exclude {
|
||
excluded_candidates = hopefuls_to_bulk_exclude(state, opts);
|
||
}
|
||
|
||
// Exclude lowest ranked candidate
|
||
if excluded_candidates.is_empty() {
|
||
let hopefuls: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
|
||
.filter(|c| state.candidates[c].state == CandidateState::Hopeful)
|
||
.collect();
|
||
|
||
let min_cands = ties::multiple_min_by(&hopefuls, |c| &state.candidates[c].votes);
|
||
excluded_candidates = if min_cands.len() > 1 {
|
||
vec![choose_lowest(state, opts, &min_cands, "Which candidate to exclude?")?]
|
||
} else {
|
||
vec![min_cands[0]]
|
||
};
|
||
}
|
||
|
||
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
|
||
state.title = StageKind::ExclusionOf(excluded_candidates.clone());
|
||
state.logger.log_smart(
|
||
"No surpluses to distribute, so {} is excluded.",
|
||
"No surpluses to distribute, so {} are excluded.",
|
||
names
|
||
);
|
||
|
||
if opts.early_bulk_elect {
|
||
// Determine if the proposed exclusion would enable a bulk election
|
||
// This should be OK for constraints, as if the election of the remaining candidates would be invalid, the excluded candidate must necessarily have be guarded already
|
||
if can_bulk_elect(state, excluded_candidates.len()) {
|
||
// Exclude candidates without further transfers
|
||
let order_excluded = state.num_excluded + 1;
|
||
for candidate in excluded_candidates {
|
||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||
count_card.state = CandidateState::Excluded;
|
||
state.num_excluded += 1;
|
||
count_card.order_elected = -(order_excluded as isize);
|
||
}
|
||
|
||
do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.")?;
|
||
return Ok(());
|
||
}
|
||
}
|
||
|
||
exclude_candidates(state, opts, excluded_candidates)?;
|
||
return Ok(());
|
||
}
|
||
|
||
/// Continue the exclusion of a candidate who is being excluded
|
||
///
|
||
/// Returns `true` if an exclusion was continued.
|
||
fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||
where
|
||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||
{
|
||
// Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers
|
||
let mut excluded_with_votes: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||
.filter(|(_, cc)| cc.state == CandidateState::Excluded && !cc.finalised)
|
||
.collect();
|
||
|
||
if !excluded_with_votes.is_empty() {
|
||
excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
|
||
|
||
let order_excluded = excluded_with_votes[0].1.order_elected;
|
||
let excluded_candidates: Vec<&Candidate> = excluded_with_votes.into_iter()
|
||
.filter(|(_, cc)| cc.order_elected == order_excluded)
|
||
.map(|(c, _)| *c)
|
||
.collect();
|
||
|
||
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
|
||
state.title = StageKind::ExclusionOf(excluded_candidates.clone());
|
||
state.logger.log_smart(
|
||
"Continuing exclusion of {}.",
|
||
"Continuing exclusion of {}.",
|
||
names
|
||
);
|
||
|
||
exclude_candidates(state, opts, excluded_candidates)?;
|
||
return Ok(true);
|
||
}
|
||
|
||
return Ok(false);
|
||
}
|
||
|
||
/// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion]
|
||
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) -> Result<(), STVError>
|
||
where
|
||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||
{
|
||
match opts.exclusion {
|
||
ExclusionMethod::SingleStage => {
|
||
match opts.surplus {
|
||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
||
gregory::exclude_candidates(state, opts, excluded_candidates);
|
||
}
|
||
SurplusMethod::Meek => {
|
||
meek::exclude_candidates(state, opts, excluded_candidates);
|
||
}
|
||
SurplusMethod::IHare | SurplusMethod::Hare => {
|
||
sample::exclude_candidates(state, opts, excluded_candidates)?;
|
||
}
|
||
}
|
||
}
|
||
ExclusionMethod::ByValue | ExclusionMethod::BySource | ExclusionMethod::ParcelsByOrder => {
|
||
// Exclusion in parts compatible only with Gregory method
|
||
gregory::exclude_candidates(state, opts, excluded_candidates);
|
||
}
|
||
ExclusionMethod::Wright => {
|
||
gregory::wright_exclude_candidates(state, opts, excluded_candidates);
|
||
}
|
||
}
|
||
|
||
return Ok(());
|
||
}
|
||
|
||
/// Determine if the count is complete because the number of elected candidates equals the number of vacancies
|
||
fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
|
||
if state.num_elected >= state.election.seats {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// Break a tie between the given candidates according to [STVOptions::ties], selecting the highest candidate
|
||
///
|
||
/// The given candidates are assumed to be tied in this round.
|
||
pub fn choose_highest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> {
|
||
for strategy in opts.ties.iter() {
|
||
match strategy.choose_highest(state, opts, candidates, prompt_text) {
|
||
Ok(c) => {
|
||
return Ok(c);
|
||
}
|
||
Err(e) => {
|
||
if let STVError::UnresolvedTie = e {
|
||
continue;
|
||
} else {
|
||
return Err(e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return Err(STVError::UnresolvedTie);
|
||
}
|
||
|
||
/// Break a tie between the given candidates according to [STVOptions::ties], selecting the lowest candidate
|
||
///
|
||
/// The given candidates are assumed to be tied in this round.
|
||
pub fn choose_lowest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> {
|
||
for strategy in opts.ties.iter() {
|
||
match strategy.choose_lowest(state, opts, candidates, prompt_text) {
|
||
Ok(c) => {
|
||
return Ok(c);
|
||
}
|
||
Err(e) => {
|
||
if let STVError::UnresolvedTie = e {
|
||
continue;
|
||
} else {
|
||
return Err(e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return Err(STVError::UnresolvedTie);
|
||
}
|
||
|
||
/// If required, initialise the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties]
|
||
fn init_tiebreaks<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||
if !opts.ties.iter().any(|t| t == &TieStrategy::Forwards) && !opts.ties.iter().any(|t| t == &TieStrategy::Backwards) {
|
||
return;
|
||
}
|
||
|
||
// Sort candidates in this stage by votes, grouping by ties
|
||
let mut sorted_candidates: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter().collect();
|
||
sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
|
||
let sorted_candidates: Vec<Vec<(&&Candidate, &CountCard<N>)>> = sorted_candidates.into_iter()
|
||
.group_by(|(_, cc)| &cc.votes)
|
||
.into_iter()
|
||
.map(|(_, candidates)| candidates.collect())
|
||
.collect();
|
||
|
||
// Update forwards tie-breaking order
|
||
if opts.ties.iter().any(|t| t == &TieStrategy::Forwards) {
|
||
let mut hm: HashMap<&Candidate, usize> = HashMap::new();
|
||
for (i, group) in sorted_candidates.iter().enumerate() {
|
||
for (candidate, _) in group.iter() {
|
||
hm.insert(candidate, i);
|
||
}
|
||
}
|
||
state.forwards_tiebreak = Some(hm);
|
||
}
|
||
|
||
// Update backwards tie-breaking order
|
||
if opts.ties.iter().any(|t| t == &TieStrategy::Backwards) {
|
||
let mut hm: HashMap<&Candidate, usize> = HashMap::new();
|
||
for (i, group) in sorted_candidates.iter().enumerate() {
|
||
for (candidate, _) in group.iter() {
|
||
hm.insert(candidate, i);
|
||
}
|
||
}
|
||
state.backwards_tiebreak = Some(hm);
|
||
}
|
||
}
|
||
|
||
/// If required, update the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties]
|
||
fn update_tiebreaks<N: Number>(state: &mut CountState<N>, _opts: &STVOptions) {
|
||
if state.forwards_tiebreak.is_none() && state.backwards_tiebreak.is_none() {
|
||
return;
|
||
}
|
||
|
||
// Sort candidates in this stage by votes, grouping by ties
|
||
let mut sorted_candidates: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter().collect();
|
||
sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
|
||
let sorted_candidates: Vec<Vec<&Candidate>> = sorted_candidates.into_iter()
|
||
.group_by(|(_, cc)| &cc.votes)
|
||
.into_iter()
|
||
.map(|(_, candidates)| candidates.map(|(c, _)| *c).collect())
|
||
.collect();
|
||
|
||
// Update forwards tie-breaking order
|
||
if let Some(hm) = state.forwards_tiebreak.as_mut() {
|
||
// TODO: Check if already completely sorted
|
||
let mut sorted_last_round: Vec<(&&Candidate, &usize)> = hm.iter().collect();
|
||
sorted_last_round.sort_unstable_by(|a, b| a.1.cmp(b.1));
|
||
let sorted_last_round: Vec<Vec<&Candidate>> = sorted_last_round.into_iter()
|
||
.group_by(|(_, v)| **v)
|
||
.into_iter()
|
||
.map(|(_, group)| group.map(|(c, _)| *c).collect())
|
||
.collect();
|
||
|
||
let mut i: usize = 0;
|
||
for mut group in sorted_last_round.into_iter() {
|
||
if group.len() == 1 {
|
||
hm.insert(group[0], i);
|
||
i += 1;
|
||
continue;
|
||
} else {
|
||
// Tied in last round - refer to this round
|
||
group.sort_unstable_by(|a, b|
|
||
sorted_candidates.iter().position(|x| x.contains(a)).unwrap()
|
||
.cmp(&sorted_candidates.iter().position(|x| x.contains(b)).unwrap())
|
||
);
|
||
let tied_last_round = group.into_iter()
|
||
.group_by(|c| sorted_candidates.iter().position(|x| x.contains(c)).unwrap());
|
||
|
||
for (_, group2) in tied_last_round.into_iter() {
|
||
for candidate in group2 {
|
||
hm.insert(candidate, i);
|
||
}
|
||
i += 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update backwards tie-breaking order
|
||
if let Some(hm) = state.backwards_tiebreak.as_mut() {
|
||
let hm_orig = hm.clone();
|
||
let mut i: usize = 0;
|
||
for group in sorted_candidates.iter() {
|
||
if group.len() == 1 {
|
||
hm.insert(group[0], i);
|
||
i += 1;
|
||
continue;
|
||
} else {
|
||
// Tied in this round - refer to last round
|
||
let mut tied_this_round: Vec<&Candidate> = group.iter().copied().collect();
|
||
tied_this_round.sort_unstable_by(|a, b| hm_orig[a].cmp(&hm_orig[b]));
|
||
let tied_this_round = tied_this_round.into_iter()
|
||
.group_by(|c| hm_orig[c]);
|
||
|
||
for (_, group2) in tied_this_round.into_iter() {
|
||
for candidate in group2 {
|
||
hm.insert(candidate, i);
|
||
}
|
||
i += 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Returns `true` if the votes required for election should be displayed, based on the given [STVOptions]
|
||
pub fn should_show_vre(opts: &STVOptions) -> bool {
|
||
if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 {
|
||
return true;
|
||
}
|
||
if opts.surplus == SurplusMethod::Meek {
|
||
return false;
|
||
}
|
||
if opts.early_bulk_elect {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|