2021-05-28 19:58:40 +10:00
|
|
|
/* OpenTally: Open-source election vote counting
|
|
|
|
* Copyright © 2021 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)]
|
|
|
|
|
2021-06-16 13:00:54 +10:00
|
|
|
/// Gregory method of surplus distributions
|
2021-06-14 20:43:36 +10:00
|
|
|
pub mod gregory;
|
2021-06-16 13:00:54 +10:00
|
|
|
/// Meek method of surplus distributions, etc.
|
|
|
|
pub mod meek;
|
2021-06-14 20:43:36 +10:00
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// WebAssembly wrappers
|
2021-05-30 18:28:39 +10:00
|
|
|
//#[cfg(target_arch = "wasm32")]
|
|
|
|
pub mod wasm;
|
|
|
|
|
2021-05-28 19:58:40 +10:00
|
|
|
use crate::numbers::Number;
|
2021-06-27 21:57:24 +10:00
|
|
|
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, Vote};
|
2021-06-13 03:15:15 +10:00
|
|
|
use crate::sharandom::SHARandom;
|
2021-06-12 02:09:26 +10:00
|
|
|
use crate::ties::TieStrategy;
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-11 21:22:28 +10:00
|
|
|
use itertools::Itertools;
|
2021-06-27 21:57:24 +10:00
|
|
|
use ndarray::Dimension;
|
2021-05-31 22:25:53 +10:00
|
|
|
use wasm_bindgen::prelude::wasm_bindgen;
|
|
|
|
|
2021-05-28 19:58:40 +10:00
|
|
|
use std::collections::HashMap;
|
2021-05-30 02:28:52 +10:00
|
|
|
use std::ops;
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Options for conducting an STV count
|
2021-06-13 03:15:15 +10:00
|
|
|
pub struct STVOptions {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Round transfer values to specified decimal places
|
2021-06-01 21:20:38 +10:00
|
|
|
pub round_tvs: Option<usize>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Round ballot weights to specified decimal places
|
2021-06-01 21:20:38 +10:00
|
|
|
pub round_weights: Option<usize>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Round votes to specified decimal places
|
2021-05-29 17:51:45 +10:00
|
|
|
pub round_votes: Option<usize>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Round quota to specified decimal places
|
2021-06-01 21:20:38 +10:00
|
|
|
pub round_quota: Option<usize>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// How to calculate votes to credit to candidates in surplus transfers
|
2021-06-11 21:22:28 +10:00
|
|
|
pub sum_surplus_transfers: SumSurplusTransfersMode,
|
2021-06-18 18:48:12 +10:00
|
|
|
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
|
|
|
pub meek_surplus_tolerance: String,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Convert ballots with value >1 to multiple ballots of value 1
|
2021-06-12 16:03:31 +10:00
|
|
|
pub normalise_ballots: bool,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Quota type
|
2021-06-02 18:07:05 +10:00
|
|
|
pub quota: QuotaType,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota
|
2021-06-02 18:07:05 +10:00
|
|
|
pub quota_criterion: QuotaCriterion,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Whether to apply a form of progressive quota
|
2021-06-07 20:52:18 +10:00
|
|
|
pub quota_mode: QuotaMode,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Tie-breaking method
|
2021-06-13 03:15:15 +10:00
|
|
|
pub ties: Vec<TieStrategy>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Method of surplus distributions
|
2021-05-31 22:25:53 +10:00
|
|
|
pub surplus: SurplusMethod,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Order to distribute surpluses
|
2021-06-01 18:57:56 +10:00
|
|
|
pub surplus_order: SurplusOrder,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Examine only transferable papers during surplus distributions
|
2021-05-31 23:17:21 +10:00
|
|
|
pub transferable_only: bool,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Method of exclusions
|
2021-05-31 22:25:53 +10:00
|
|
|
pub exclusion: ExclusionMethod,
|
2021-06-20 01:28:54 +10:00
|
|
|
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
|
|
|
|
pub meek_nz_exclusion: bool,
|
2021-06-23 00:52:25 +10:00
|
|
|
/// Bulk elect as soon as continuing candidates fill all remaining vacancies
|
|
|
|
pub early_bulk_elect: bool,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Use bulk exclusion
|
2021-06-08 22:22:43 +10:00
|
|
|
pub bulk_exclude: bool,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Defer surplus distributions if possible
|
2021-06-09 12:16:25 +10:00
|
|
|
pub defer_surpluses: bool,
|
2021-06-18 18:48:12 +10:00
|
|
|
/// (Meek STV) Immediately elect candidates even if keep values have not converged
|
|
|
|
pub meek_immediate_elect: bool,
|
2021-06-27 21:57:24 +10:00
|
|
|
/// Mode of handling constraints
|
|
|
|
pub constraint_mode: ConstraintMode,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Print votes to specified decimal places in results report
|
2021-05-31 22:25:53 +10:00
|
|
|
pub pp_decimals: usize,
|
|
|
|
}
|
|
|
|
|
2021-06-13 03:15:15 +10:00
|
|
|
impl STVOptions {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Returns a new [STVOptions] based on arguments given as strings
|
2021-06-01 19:04:03 +10:00
|
|
|
pub fn new(
|
2021-06-01 21:20:38 +10:00
|
|
|
round_tvs: Option<usize>,
|
|
|
|
round_weights: Option<usize>,
|
2021-06-01 19:04:03 +10:00
|
|
|
round_votes: Option<usize>,
|
2021-06-01 21:20:38 +10:00
|
|
|
round_quota: Option<usize>,
|
2021-06-12 16:03:31 +10:00
|
|
|
sum_surplus_transfers: &str,
|
2021-06-18 18:48:12 +10:00
|
|
|
meek_surplus_tolerance: &str,
|
2021-06-12 16:03:31 +10:00
|
|
|
normalise_ballots: bool,
|
2021-06-02 18:07:05 +10:00
|
|
|
quota: &str,
|
|
|
|
quota_criterion: &str,
|
2021-06-07 20:52:18 +10:00
|
|
|
quota_mode: &str,
|
2021-06-13 00:15:14 +10:00
|
|
|
ties: &Vec<String>,
|
2021-06-13 03:15:15 +10:00
|
|
|
random_seed: &Option<String>,
|
2021-06-01 19:04:03 +10:00
|
|
|
surplus: &str,
|
|
|
|
surplus_order: &str,
|
|
|
|
transferable_only: bool,
|
|
|
|
exclusion: &str,
|
2021-06-20 01:28:54 +10:00
|
|
|
meek_nz_exclusion: bool,
|
2021-06-23 00:52:25 +10:00
|
|
|
early_bulk_elect: bool,
|
2021-06-08 22:22:43 +10:00
|
|
|
bulk_exclude: bool,
|
2021-06-09 12:16:25 +10:00
|
|
|
defer_surpluses: bool,
|
2021-06-18 18:48:12 +10:00
|
|
|
meek_immediate_elect: bool,
|
2021-06-27 21:57:24 +10:00
|
|
|
constraint_mode: &str,
|
2021-06-01 19:04:03 +10:00
|
|
|
pp_decimals: usize,
|
|
|
|
) -> Self {
|
|
|
|
return STVOptions {
|
2021-06-01 21:20:38 +10:00
|
|
|
round_tvs,
|
|
|
|
round_weights,
|
|
|
|
round_votes,
|
|
|
|
round_quota,
|
2021-06-12 16:03:31 +10:00
|
|
|
sum_surplus_transfers: match sum_surplus_transfers {
|
2021-06-11 21:22:28 +10:00
|
|
|
"single_step" => SumSurplusTransfersMode::SingleStep,
|
|
|
|
"by_value" => SumSurplusTransfersMode::ByValue,
|
|
|
|
"per_ballot" => SumSurplusTransfersMode::PerBallot,
|
|
|
|
_ => panic!("Invalid --sum-transfers"),
|
|
|
|
},
|
2021-06-18 18:48:12 +10:00
|
|
|
meek_surplus_tolerance: meek_surplus_tolerance.to_string(),
|
2021-06-12 16:03:31 +10:00
|
|
|
normalise_ballots,
|
2021-06-02 18:07:05 +10:00
|
|
|
quota: match quota {
|
|
|
|
"droop" => QuotaType::Droop,
|
|
|
|
"hare" => QuotaType::Hare,
|
|
|
|
"droop_exact" => QuotaType::DroopExact,
|
|
|
|
"hare_exact" => QuotaType::HareExact,
|
|
|
|
_ => panic!("Invalid --quota"),
|
|
|
|
},
|
|
|
|
quota_criterion: match quota_criterion {
|
|
|
|
"geq" => QuotaCriterion::GreaterOrEqual,
|
|
|
|
"gt" => QuotaCriterion::Greater,
|
|
|
|
_ => panic!("Invalid --quota-criterion"),
|
|
|
|
},
|
2021-06-07 20:52:18 +10:00
|
|
|
quota_mode: match quota_mode {
|
|
|
|
"static" => QuotaMode::Static,
|
|
|
|
"ers97" => QuotaMode::ERS97,
|
|
|
|
_ => panic!("Invalid --quota-mode"),
|
|
|
|
},
|
2021-06-13 00:15:14 +10:00
|
|
|
ties: ties.into_iter().map(|t| match t.as_str() {
|
|
|
|
"forwards" => TieStrategy::Forwards,
|
|
|
|
"backwards" => TieStrategy::Backwards,
|
2021-06-13 03:15:15 +10:00
|
|
|
"random" => TieStrategy::Random(random_seed.as_ref().expect("Must provide a --random-seed if using --ties random").clone()),
|
2021-06-13 00:15:14 +10:00
|
|
|
"prompt" => TieStrategy::Prompt,
|
|
|
|
_ => panic!("Invalid --ties"),
|
|
|
|
}).collect(),
|
2021-06-01 19:04:03 +10:00
|
|
|
surplus: match surplus {
|
|
|
|
"wig" => SurplusMethod::WIG,
|
|
|
|
"uig" => SurplusMethod::UIG,
|
|
|
|
"eg" => SurplusMethod::EG,
|
|
|
|
"meek" => SurplusMethod::Meek,
|
|
|
|
_ => panic!("Invalid --surplus"),
|
|
|
|
},
|
|
|
|
surplus_order: match surplus_order {
|
|
|
|
"by_size" => SurplusOrder::BySize,
|
|
|
|
"by_order" => SurplusOrder::ByOrder,
|
|
|
|
_ => panic!("Invalid --surplus-order"),
|
|
|
|
},
|
2021-06-01 21:20:38 +10:00
|
|
|
transferable_only,
|
2021-06-01 19:04:03 +10:00
|
|
|
exclusion: match exclusion {
|
|
|
|
"single_stage" => ExclusionMethod::SingleStage,
|
|
|
|
"by_value" => ExclusionMethod::ByValue,
|
|
|
|
"parcels_by_order" => ExclusionMethod::ParcelsByOrder,
|
2021-06-22 15:23:46 +10:00
|
|
|
"wright" => ExclusionMethod::Wright,
|
2021-06-01 19:04:03 +10:00
|
|
|
_ => panic!("Invalid --exclusion"),
|
|
|
|
},
|
2021-06-20 01:28:54 +10:00
|
|
|
meek_nz_exclusion,
|
2021-06-23 00:52:25 +10:00
|
|
|
early_bulk_elect,
|
2021-06-08 22:22:43 +10:00
|
|
|
bulk_exclude,
|
2021-06-09 12:16:25 +10:00
|
|
|
defer_surpluses,
|
2021-06-18 18:48:12 +10:00
|
|
|
meek_immediate_elect,
|
2021-06-27 21:57:24 +10:00
|
|
|
constraint_mode: match constraint_mode {
|
|
|
|
"guard_doom" => ConstraintMode::GuardDoom,
|
|
|
|
"rollback" => ConstraintMode::Rollback,
|
|
|
|
_ => panic!("Invalid --constraint-mode"),
|
|
|
|
},
|
2021-06-01 21:20:38 +10:00
|
|
|
pp_decimals,
|
2021-06-01 19:04:03 +10:00
|
|
|
};
|
|
|
|
}
|
2021-06-13 00:15:14 +10:00
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Converts the [STVOptions] into CLI argument representation
|
2021-06-03 21:35:25 +10:00
|
|
|
pub fn describe<N: Number>(&self) -> String {
|
|
|
|
let mut flags = Vec::new();
|
2021-06-13 00:15:14 +10:00
|
|
|
let n_str = N::describe_opt(); if !n_str.is_empty() { flags.push(N::describe_opt()) };
|
2021-06-03 21:35:25 +10:00
|
|
|
if let Some(dps) = self.round_tvs { flags.push(format!("--round-tvs {}", dps)); }
|
|
|
|
if let Some(dps) = self.round_weights { flags.push(format!("--round-weights {}", 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)); }
|
2021-06-20 01:28:54 +10:00
|
|
|
if self.surplus != SurplusMethod::Meek && self.sum_surplus_transfers != SumSurplusTransfersMode::SingleStep { flags.push(self.sum_surplus_transfers.describe()); }
|
2021-06-18 18:48:12 +10:00
|
|
|
if self.surplus == SurplusMethod::Meek && self.meek_surplus_tolerance != "0.001%" { flags.push(format!("--meek-surplus-tolerance {}", self.meek_surplus_tolerance)); }
|
2021-06-12 16:03:31 +10:00
|
|
|
if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); }
|
2021-06-20 01:28:54 +10:00
|
|
|
if self.quota != QuotaType::DroopExact { flags.push(self.quota.describe()); }
|
|
|
|
if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); }
|
|
|
|
if self.surplus != SurplusMethod::Meek && self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); }
|
2021-06-13 00:15:14 +10:00
|
|
|
let ties_str = self.ties.iter().map(|t| t.describe()).join(" ");
|
|
|
|
if ties_str != "prompt" { flags.push(format!("--ties {}", ties_str)); }
|
2021-06-13 03:15:15 +10:00
|
|
|
for t in self.ties.iter() { if let TieStrategy::Random(seed) = t { flags.push(format!("--random-seed {}", seed)); } }
|
2021-06-03 21:35:25 +10:00
|
|
|
if self.surplus != SurplusMethod::WIG { flags.push(self.surplus.describe()); }
|
2021-06-20 01:28:54 +10:00
|
|
|
if self.surplus != SurplusMethod::Meek && self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); }
|
|
|
|
if self.surplus != SurplusMethod::Meek && self.transferable_only { flags.push("--transferable-only".to_string()); }
|
|
|
|
if self.surplus != SurplusMethod::Meek && 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()); }
|
2021-06-23 00:52:25 +10:00
|
|
|
if !self.early_bulk_elect { flags.push("--no-early-bulk-elect".to_string()); }
|
2021-06-12 16:03:31 +10:00
|
|
|
if self.bulk_exclude { flags.push("--bulk-exclude".to_string()); }
|
|
|
|
if self.defer_surpluses { flags.push("--defer-surpluses".to_string()); }
|
2021-06-18 18:48:12 +10:00
|
|
|
if self.surplus == SurplusMethod::Meek && self.meek_immediate_elect { flags.push("--meek-immediate-elect".to_string()); }
|
2021-06-03 21:35:25 +10:00
|
|
|
if self.pp_decimals != 2 { flags.push(format!("--pp-decimals {}", self.pp_decimals)); }
|
|
|
|
return flags.join(" ");
|
|
|
|
}
|
2021-06-22 14:34:26 +10:00
|
|
|
|
|
|
|
/// Validate the combination of [STVOptions] and panic if invalid
|
|
|
|
pub fn validate(&self) {
|
|
|
|
if self.surplus == SurplusMethod::Meek {
|
|
|
|
if self.transferable_only { panic!("--surplus meek is incompatible with --transferable-only"); }
|
|
|
|
if self.exclusion != ExclusionMethod::SingleStage { panic!("--surplus meek requires --exclusion single_stage"); }
|
|
|
|
}
|
|
|
|
}
|
2021-06-03 21:35:25 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Enum of options for [STVOptions::sum_surplus_transfers]
|
2021-06-11 21:22:28 +10:00
|
|
|
#[wasm_bindgen]
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
#[derive(PartialEq)]
|
|
|
|
pub enum SumSurplusTransfersMode {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Sum and round all surplus transfers for a candidate in a single step
|
2021-06-11 21:22:28 +10:00
|
|
|
SingleStep,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Sum and round a candidate's surplus transfers separately for ballot papers received at each particular value
|
2021-06-11 21:22:28 +10:00
|
|
|
ByValue,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Sum and round a candidate's surplus transfers individually for each ballot paper
|
2021-06-11 21:22:28 +10:00
|
|
|
PerBallot,
|
|
|
|
}
|
|
|
|
|
2021-06-12 16:03:31 +10:00
|
|
|
impl SumSurplusTransfersMode {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Convert to CLI argument representation
|
2021-06-12 16:03:31 +10:00
|
|
|
fn describe(self) -> String {
|
|
|
|
match self {
|
|
|
|
SumSurplusTransfersMode::SingleStep => "--sum-surplus-transfers single_step",
|
|
|
|
SumSurplusTransfersMode::ByValue => "--sum-surplus-transfers by_value",
|
|
|
|
SumSurplusTransfersMode::PerBallot => "--sum-surplus-transfers per_ballot",
|
|
|
|
}.to_string()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Enum of options for [STVOptions::quota]
|
2021-06-02 18:07:05 +10:00
|
|
|
#[wasm_bindgen]
|
|
|
|
#[derive(Clone, Copy)]
|
2021-06-03 21:35:25 +10:00
|
|
|
#[derive(PartialEq)]
|
2021-06-02 18:07:05 +10:00
|
|
|
pub enum QuotaType {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Droop quota
|
2021-06-02 18:07:05 +10:00
|
|
|
Droop,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Hare quota
|
2021-06-02 18:07:05 +10:00
|
|
|
Hare,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Exact Droop quota (Newland–Britton/Hagenbach-Bischoff quota)
|
2021-06-02 18:07:05 +10:00
|
|
|
DroopExact,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Exact Hare quota
|
2021-06-02 18:07:05 +10:00
|
|
|
HareExact,
|
|
|
|
}
|
|
|
|
|
2021-06-03 21:35:25 +10:00
|
|
|
impl QuotaType {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Convert to CLI argument representation
|
2021-06-03 21:35:25 +10:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Enum of options for [STVOptions::quota_criterion]
|
2021-06-02 18:07:05 +10:00
|
|
|
#[wasm_bindgen]
|
|
|
|
#[derive(Clone, Copy)]
|
2021-06-03 21:35:25 +10:00
|
|
|
#[derive(PartialEq)]
|
2021-06-02 18:07:05 +10:00
|
|
|
pub enum QuotaCriterion {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Elect candidates on equalling or exceeding the quota
|
2021-06-02 18:07:05 +10:00
|
|
|
GreaterOrEqual,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Elect candidates on strictly exceeding the quota
|
2021-06-02 18:07:05 +10:00
|
|
|
Greater,
|
|
|
|
}
|
|
|
|
|
2021-06-03 21:35:25 +10:00
|
|
|
impl QuotaCriterion {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Convert to CLI argument representation
|
2021-06-03 21:35:25 +10:00
|
|
|
fn describe(self) -> String {
|
|
|
|
match self {
|
|
|
|
QuotaCriterion::GreaterOrEqual => "--quota-criterion geq",
|
|
|
|
QuotaCriterion::Greater => "--quota-criterion gt",
|
|
|
|
}.to_string()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Enum of options for [STVOptions::quota_mode]
|
2021-06-07 20:52:18 +10:00
|
|
|
#[wasm_bindgen]
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
#[derive(PartialEq)]
|
|
|
|
pub enum QuotaMode {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Static quota
|
2021-06-07 20:52:18 +10:00
|
|
|
Static,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Static quota with ERS97 rules
|
2021-06-07 20:52:18 +10:00
|
|
|
ERS97,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl QuotaMode {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Convert to CLI argument representation
|
2021-06-07 20:52:18 +10:00
|
|
|
fn describe(self) -> String {
|
|
|
|
match self {
|
|
|
|
QuotaMode::Static => "--quota-mode static",
|
|
|
|
QuotaMode::ERS97 => "--quota-mode ers97",
|
|
|
|
}.to_string()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Enum of options for [STVOptions::surplus]
|
2021-05-31 22:25:53 +10:00
|
|
|
#[wasm_bindgen]
|
|
|
|
#[derive(Clone, Copy)]
|
2021-06-03 21:35:25 +10:00
|
|
|
#[derive(PartialEq)]
|
2021-05-31 22:25:53 +10:00
|
|
|
pub enum SurplusMethod {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Weighted inclusive Gregory method
|
2021-05-31 22:25:53 +10:00
|
|
|
WIG,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Unweighted inclusive Gregory method
|
2021-05-31 22:25:53 +10:00
|
|
|
UIG,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Exclusive Gregory method (last bundle)
|
2021-05-31 22:25:53 +10:00
|
|
|
EG,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Meek method
|
2021-05-31 22:25:53 +10:00
|
|
|
Meek,
|
2021-05-29 17:51:45 +10:00
|
|
|
}
|
|
|
|
|
2021-06-03 21:35:25 +10:00
|
|
|
impl SurplusMethod {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Convert to CLI argument representation
|
2021-06-03 21:35:25 +10:00
|
|
|
fn describe(self) -> String {
|
|
|
|
match self {
|
|
|
|
SurplusMethod::WIG => "--surplus wig",
|
|
|
|
SurplusMethod::UIG => "--surplus uig",
|
|
|
|
SurplusMethod::EG => "--surplus eg",
|
|
|
|
SurplusMethod::Meek => "--surplus meek",
|
|
|
|
}.to_string()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Enum of options for [STVOptions::surplus_order]
|
2021-06-01 18:57:56 +10:00
|
|
|
#[wasm_bindgen]
|
|
|
|
#[derive(Clone, Copy)]
|
2021-06-03 21:35:25 +10:00
|
|
|
#[derive(PartialEq)]
|
2021-06-01 18:57:56 +10:00
|
|
|
pub enum SurplusOrder {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Transfer the largest surplus first, even if it arose at a later stage of the count
|
2021-06-01 18:57:56 +10:00
|
|
|
BySize,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Transfer the surplus of the candidate elected first, even if it is smaller than another
|
2021-06-01 18:57:56 +10:00
|
|
|
ByOrder,
|
|
|
|
}
|
|
|
|
|
2021-06-03 21:35:25 +10:00
|
|
|
impl SurplusOrder {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Convert to CLI argument representation
|
2021-06-03 21:35:25 +10:00
|
|
|
fn describe(self) -> String {
|
|
|
|
match self {
|
|
|
|
SurplusOrder::BySize => "--surplus-order by_size",
|
|
|
|
SurplusOrder::ByOrder => "--surplus-order by_order",
|
|
|
|
}.to_string()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Enum of options for [STVOptions::exclusion]
|
2021-05-31 22:25:53 +10:00
|
|
|
#[wasm_bindgen]
|
|
|
|
#[derive(Clone, Copy)]
|
2021-06-03 21:35:25 +10:00
|
|
|
#[derive(PartialEq)]
|
2021-05-31 22:25:53 +10:00
|
|
|
pub enum ExclusionMethod {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Transfer all ballot papers of an excluded candidate in one stage
|
2021-05-31 22:25:53 +10:00
|
|
|
SingleStage,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Transfer the ballot papers of an excluded candidate in descending order of accumulated transfer value
|
2021-05-31 22:25:53 +10:00
|
|
|
ByValue,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Transfer the ballot papers of an excluded candidate parcel by parcel in the order received
|
2021-05-31 22:25:53 +10:00
|
|
|
ParcelsByOrder,
|
2021-06-22 15:23:46 +10:00
|
|
|
/// Wright method (re-iterate)
|
|
|
|
Wright,
|
2021-05-31 22:25:53 +10:00
|
|
|
}
|
|
|
|
|
2021-06-03 21:35:25 +10:00
|
|
|
impl ExclusionMethod {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Convert to CLI argument representation
|
2021-06-03 21:35:25 +10:00
|
|
|
fn describe(self) -> String {
|
|
|
|
match self {
|
|
|
|
ExclusionMethod::SingleStage => "--exclusion single_stage",
|
|
|
|
ExclusionMethod::ByValue => "--exclusion by_value",
|
2021-06-07 20:52:18 +10:00
|
|
|
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
|
2021-06-22 15:23:46 +10:00
|
|
|
ExclusionMethod::Wright => "--exclusion wright",
|
2021-06-03 21:35:25 +10:00
|
|
|
}.to_string()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
/// Enum of options for [STVOptions::constraint_mode]
|
|
|
|
pub enum ConstraintMode {
|
|
|
|
/// Guard or doom candidates as soon as required to secure a conformant result
|
|
|
|
GuardDoom,
|
|
|
|
/// TODO: NYI
|
|
|
|
Rollback,
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// An error during the STV count
|
2021-06-12 02:09:26 +10:00
|
|
|
#[wasm_bindgen]
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum STVError {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// User input is required
|
2021-06-12 02:09:26 +10:00
|
|
|
RequireInput,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Tie could not be resolved
|
2021-06-13 00:15:14 +10:00
|
|
|
UnresolvedTie,
|
2021-06-12 02:09:26 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Distribute first preferences, and initialise other states such as the random number generator and tie-breaking rules
|
2021-06-16 13:00:54 +10:00
|
|
|
pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a STVOptions)
|
|
|
|
where
|
|
|
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
|
|
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
|
|
|
{
|
2021-06-13 03:15:15 +10:00
|
|
|
// Initialise RNG
|
|
|
|
for t in opts.ties.iter() {
|
|
|
|
if let TieStrategy::Random(seed) = t {
|
|
|
|
state.random = Some(SHARandom::new(seed));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-16 13:00:54 +10:00
|
|
|
distribute_first_preferences(&mut state, opts);
|
2021-05-31 22:25:53 +10:00
|
|
|
calculate_quota(&mut state, opts);
|
2021-06-02 18:07:05 +10:00
|
|
|
elect_meeting_quota(&mut state, opts);
|
2021-06-13 00:15:14 +10:00
|
|
|
init_tiebreaks(&mut state, opts);
|
2021-05-29 17:51:45 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Perform a single stage of the STV count
|
2021-06-12 02:09:26 +10:00
|
|
|
pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
2021-05-29 17:51:45 +10:00
|
|
|
where
|
2021-05-30 02:28:52 +10:00
|
|
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
2021-06-16 13:00:54 +10:00
|
|
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
2021-05-30 02:28:52 +10:00
|
|
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
|
|
|
for<'r> &'r N: ops::Neg<Output=N>,
|
2021-05-29 17:51:45 +10:00
|
|
|
{
|
|
|
|
state.logger.entries.clear();
|
|
|
|
state.step_all();
|
|
|
|
|
|
|
|
// Finish count
|
|
|
|
if finished_before_stage(&state) {
|
2021-06-12 02:09:26 +10:00
|
|
|
return Ok(true);
|
2021-05-29 17:51:45 +10:00
|
|
|
}
|
|
|
|
|
2021-06-23 00:52:25 +10:00
|
|
|
// Attempt early bulk election
|
|
|
|
if opts.early_bulk_elect {
|
|
|
|
if bulk_elect(&mut state, &opts)? {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-29 17:51:45 +10:00
|
|
|
// Continue exclusions
|
|
|
|
if continue_exclusion(&mut state, &opts) {
|
2021-06-07 20:52:18 +10:00
|
|
|
calculate_quota(&mut state, opts);
|
2021-06-02 18:07:05 +10:00
|
|
|
elect_meeting_quota(&mut state, opts);
|
2021-06-13 00:15:14 +10:00
|
|
|
update_tiebreaks(&mut state, opts);
|
2021-06-12 02:09:26 +10:00
|
|
|
return Ok(false);
|
2021-05-29 17:51:45 +10:00
|
|
|
}
|
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
// Exclude doomed candidates
|
|
|
|
if exclude_doomed(&mut state, &opts)? {
|
|
|
|
calculate_quota(&mut state, opts);
|
|
|
|
elect_meeting_quota(&mut state, opts);
|
|
|
|
update_tiebreaks(&mut state, opts);
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
|
2021-05-29 17:51:45 +10:00
|
|
|
// Distribute surpluses
|
2021-06-12 02:09:26 +10:00
|
|
|
if distribute_surpluses(&mut state, &opts)? {
|
2021-06-07 20:52:18 +10:00
|
|
|
calculate_quota(&mut state, opts);
|
2021-06-02 18:07:05 +10:00
|
|
|
elect_meeting_quota(&mut state, opts);
|
2021-06-13 00:15:14 +10:00
|
|
|
update_tiebreaks(&mut state, opts);
|
2021-06-12 02:09:26 +10:00
|
|
|
return Ok(false);
|
2021-05-29 17:51:45 +10:00
|
|
|
}
|
|
|
|
|
2021-06-23 00:52:25 +10:00
|
|
|
// Attempt late bulk election
|
2021-06-13 00:15:14 +10:00
|
|
|
if bulk_elect(&mut state, &opts)? {
|
2021-06-12 02:09:26 +10:00
|
|
|
return Ok(false);
|
2021-05-29 17:51:45 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
// Exclude lowest hopeful
|
2021-06-12 02:09:26 +10:00
|
|
|
if exclude_hopefuls(&mut state, &opts)? {
|
2021-06-07 20:52:18 +10:00
|
|
|
calculate_quota(&mut state, opts);
|
2021-06-02 18:07:05 +10:00
|
|
|
elect_meeting_quota(&mut state, opts);
|
2021-06-13 00:15:14 +10:00
|
|
|
update_tiebreaks(&mut state, opts);
|
2021-06-12 02:09:26 +10:00
|
|
|
return Ok(false);
|
2021-05-29 17:51:45 +10:00
|
|
|
}
|
|
|
|
|
2021-06-12 02:09:26 +10:00
|
|
|
panic!("Count incomplete but unable to proceed");
|
2021-05-29 17:51:45 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// See [next_preferences]
|
2021-05-28 19:58:40 +10:00
|
|
|
struct NextPreferencesResult<'a, N> {
|
|
|
|
candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>,
|
|
|
|
exhausted: NextPreferencesEntry<'a, N>,
|
|
|
|
total_ballots: N,
|
2021-05-30 02:28:52 +10:00
|
|
|
total_votes: N,
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// See [next_preferences]
|
2021-05-28 19:58:40 +10:00
|
|
|
struct NextPreferencesEntry<'a, N> {
|
|
|
|
//count_card: Option<&'a CountCard<'a, N>>,
|
|
|
|
votes: Vec<Vote<'a, N>>,
|
|
|
|
num_ballots: N,
|
|
|
|
num_votes: N,
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Count the given votes, grouping according to next available preference
|
2021-05-28 19:58:40 +10:00
|
|
|
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(),
|
|
|
|
num_votes: N::new(),
|
|
|
|
},
|
|
|
|
total_ballots: N::new(),
|
2021-05-30 02:28:52 +10:00
|
|
|
total_votes: N::new(),
|
2021-05-28 19:58:40 +10:00
|
|
|
};
|
|
|
|
|
|
|
|
for mut vote in votes.into_iter() {
|
|
|
|
result.total_ballots += &vote.ballot.orig_value;
|
2021-05-30 02:28:52 +10:00
|
|
|
result.total_votes += &vote.value;
|
2021-05-28 19:58:40 +10:00
|
|
|
|
|
|
|
let mut next_candidate = None;
|
|
|
|
|
|
|
|
for (i, preference) in vote.ballot.preferences.iter().enumerate().skip(vote.up_to_pref) {
|
|
|
|
let candidate = &state.election.candidates[*preference];
|
|
|
|
let count_card = state.candidates.get(candidate).unwrap();
|
|
|
|
|
2021-06-12 00:50:01 +10:00
|
|
|
if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state {
|
2021-05-28 19:58:40 +10:00
|
|
|
next_candidate = Some(candidate);
|
|
|
|
vote.up_to_pref = i + 1;
|
|
|
|
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.num_votes += &vote.value;
|
|
|
|
entry.votes.push(vote);
|
|
|
|
} else {
|
|
|
|
let entry = NextPreferencesEntry {
|
|
|
|
num_ballots: vote.ballot.orig_value.clone(),
|
|
|
|
num_votes: vote.value.clone(),
|
|
|
|
votes: vec![vote],
|
|
|
|
};
|
|
|
|
result.candidates.insert(candidate, entry);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
result.exhausted.num_ballots += &vote.ballot.orig_value;
|
|
|
|
result.exhausted.num_votes += &vote.value;
|
|
|
|
result.exhausted.votes.push(vote);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Distribute first preference votes
|
2021-06-16 13:00:54 +10:00
|
|
|
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 => {
|
|
|
|
gregory::distribute_first_preferences(state);
|
|
|
|
}
|
|
|
|
SurplusMethod::Meek => {
|
2021-06-18 18:48:12 +10:00
|
|
|
meek::distribute_first_preferences(state, opts);
|
2021-06-16 13:00:54 +10:00
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Calculate the quota, given the total vote, according to [STVOptions::quota]
|
2021-06-07 20:52:18 +10:00
|
|
|
fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N {
|
2021-06-02 18:07:05 +10:00
|
|
|
match opts.quota {
|
|
|
|
QuotaType::Droop | QuotaType::DroopExact => {
|
2021-06-07 20:52:18 +10:00
|
|
|
total /= N::from(seats + 1);
|
2021-06-02 18:07:05 +10:00
|
|
|
}
|
|
|
|
QuotaType::Hare | QuotaType::HareExact => {
|
2021-06-07 20:52:18 +10:00
|
|
|
total /= N::from(seats);
|
2021-06-02 18:07:05 +10:00
|
|
|
}
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-01 21:20:38 +10:00
|
|
|
if let Some(dps) = opts.round_quota {
|
2021-06-02 18:07:05 +10:00
|
|
|
match opts.quota {
|
|
|
|
QuotaType::Droop | QuotaType::Hare => {
|
|
|
|
// Increment to next available increment
|
|
|
|
let mut factor = N::from(10);
|
|
|
|
factor.pow_assign(dps as i32);
|
2021-06-07 20:52:18 +10:00
|
|
|
total *= &factor;
|
|
|
|
total.floor_mut(0);
|
|
|
|
total += N::one();
|
|
|
|
total /= factor;
|
2021-06-02 18:07:05 +10:00
|
|
|
}
|
|
|
|
QuotaType::DroopExact | QuotaType::HareExact => {
|
|
|
|
// Round up to next available increment if necessary
|
2021-06-07 20:52:18 +10:00
|
|
|
total.ceil_mut(dps);
|
2021-06-02 18:07:05 +10:00
|
|
|
}
|
|
|
|
}
|
2021-06-01 21:20:38 +10:00
|
|
|
}
|
|
|
|
|
2021-06-07 20:52:18 +10:00
|
|
|
return total;
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Calculate the quota according to [STVOptions::quota]
|
2021-06-07 20:52:18 +10:00
|
|
|
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
|
|
|
// Calculate quota
|
2021-06-16 13:00:54 +10:00
|
|
|
if state.quota.is_none() || opts.surplus == SurplusMethod::Meek {
|
2021-06-07 20:52:18 +10:00
|
|
|
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);
|
|
|
|
}
|
2021-05-29 01:22:46 +10:00
|
|
|
|
2021-06-07 20:52:18 +10:00
|
|
|
if let QuotaMode::ERS97 = opts.quota_mode {
|
|
|
|
// ERS97 rules
|
|
|
|
|
|
|
|
// -------------------------
|
|
|
|
// Reduce quota if allowable
|
|
|
|
|
|
|
|
if 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 {
|
|
|
|
let mut log = String::new();
|
|
|
|
|
|
|
|
// Calculate total active vote
|
|
|
|
let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
|
|
|
|
match cc.state {
|
2021-06-12 00:50:01 +10:00
|
|
|
CandidateState::Elected => { if &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } }
|
2021-06-07 20:52:18 +10:00
|
|
|
_ => { acc + &cc.votes }
|
|
|
|
}
|
|
|
|
});
|
|
|
|
log.push_str(format!("Total active vote is {:.dps$}, so the vote required for election is ", total_active_vote, dps=opts.pp_decimals).as_str());
|
|
|
|
|
|
|
|
let vote_req = total_to_quota(total_active_vote, state.election.seats - state.num_elected, opts);
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// No ERS97 rules
|
2021-06-16 13:00:54 +10:00
|
|
|
if state.vote_required_election.is_none() || opts.surplus == SurplusMethod::Meek {
|
2021-06-07 20:52:18 +10:00
|
|
|
state.vote_required_election = state.quota.clone();
|
|
|
|
}
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Determine if the given candidate meets the quota, according to [STVOptions::quota_criterion]
|
2021-06-02 18:07:05 +10:00
|
|
|
fn meets_quota<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;
|
|
|
|
}
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Declare elected all candidates meeting the quota
|
2021-06-18 18:48:12 +10:00
|
|
|
fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
|
2021-06-27 21:57:24 +10:00
|
|
|
let vote_req = state.vote_required_election.as_ref().unwrap().clone(); // Have to do this or else the borrow checker gets confused
|
2021-06-07 20:52:18 +10:00
|
|
|
|
|
|
|
let mut cands_meeting_quota: Vec<&Candidate> = state.election.candidates.iter()
|
2021-06-27 21:57:24 +10:00
|
|
|
.filter(|c| {
|
|
|
|
let cc = state.candidates.get(c).unwrap();
|
|
|
|
return (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_quota(&vote_req, cc, opts);
|
|
|
|
})
|
2021-05-28 22:37:18 +10:00
|
|
|
.collect();
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
// Sort by votes
|
|
|
|
cands_meeting_quota.sort_unstable_by(|a, b| state.candidates.get(a).unwrap().votes.cmp(&state.candidates.get(b).unwrap().votes));
|
|
|
|
|
|
|
|
let elected = !cands_meeting_quota.is_empty();
|
|
|
|
|
|
|
|
while !cands_meeting_quota.is_empty() {
|
2021-05-28 19:58:40 +10:00
|
|
|
// Declare elected in descending order of votes
|
2021-06-27 21:57:24 +10:00
|
|
|
let candidate = cands_meeting_quota.pop().unwrap();
|
|
|
|
|
|
|
|
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(
|
|
|
|
"{} meets the quota and is elected.",
|
|
|
|
"{} meet the quota and are elected.",
|
|
|
|
vec![&candidate.name]
|
|
|
|
);
|
|
|
|
|
|
|
|
if update_constraints(state, opts) {
|
|
|
|
// Recheck as some candidates may have been doomed
|
|
|
|
cands_meeting_quota = state.election.candidates.iter()
|
|
|
|
.filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::Hopeful && meets_quota(&vote_req, cc, opts) })
|
|
|
|
.collect();
|
|
|
|
cands_meeting_quota.sort_unstable_by(|a, b| state.candidates.get(a).unwrap().votes.cmp(&state.candidates.get(b).unwrap().votes));
|
|
|
|
}
|
|
|
|
|
|
|
|
if opts.quota_mode == QuotaMode::ERS97 {
|
|
|
|
// Vote required for election may have changed
|
|
|
|
calculate_quota(state, opts);
|
2021-06-07 20:52:18 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
if opts.quota_mode == QuotaMode::ERS97 {
|
|
|
|
// Repeat in case vote required for election has changed
|
|
|
|
elect_meeting_quota(state, opts);
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
2021-06-27 21:57:24 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
return elected;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn candidates_in_constraint_cell<'a, N: Number>(election: &'a Election<N>, candidates: &HashMap<&Candidate, CountCard<N>>, idx: &[usize]) -> Vec<&'a Candidate> {
|
|
|
|
let mut result: Vec<&Candidate> = Vec::new();
|
|
|
|
for (i, candidate) in election.candidates.iter().enumerate() {
|
|
|
|
let cc = candidates.get(candidate).unwrap();
|
|
|
|
if cc.state != CandidateState::Hopeful {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Is this candidate within this constraint cell?
|
|
|
|
let mut matches = true;
|
|
|
|
for (coord, constraint) in idx.iter().zip(election.constraints.as_ref().unwrap().0.iter()) {
|
|
|
|
let group = &constraint.groups[coord - 1]; // The group referred to by this constraint cell
|
|
|
|
if !group.candidates.contains(&i) {
|
|
|
|
matches = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-06-18 18:48:12 +10:00
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
if matches {
|
|
|
|
result.push(candidate);
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
2021-06-27 21:57:24 +10:00
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn update_constraints<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
|
|
|
|
if state.constraint_matrix.is_none() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
let cm = state.constraint_matrix.as_mut().unwrap();
|
|
|
|
|
|
|
|
// Update cands/elected
|
|
|
|
cm.update_from_state(&state.election, &state.candidates);
|
|
|
|
cm.recount_cands();
|
|
|
|
|
|
|
|
// Iterate for stable state
|
|
|
|
//println!("{}", cm);
|
|
|
|
while !cm.step().expect("No conformant result") {
|
|
|
|
//println!("{}", cm);
|
|
|
|
}
|
|
|
|
//println!("{}", cm);
|
|
|
|
|
|
|
|
// TODO: Refactor and move this to constraints module?
|
|
|
|
match opts.constraint_mode {
|
|
|
|
ConstraintMode::GuardDoom => {
|
|
|
|
// Check for guarded or doomed candidates
|
|
|
|
let mut guarded_or_doomed = false;
|
|
|
|
|
|
|
|
for idx in ndarray::indices(cm.0.shape()) {
|
|
|
|
if (0..idx.ndim()).fold(0, |acc, d| if idx[d] == 0 { acc + 1 } else { acc }) != 0 {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let cell = &cm.0[&idx];
|
|
|
|
|
|
|
|
if cell.elected == cell.max {
|
|
|
|
// Doom remaining candidates in this cell
|
|
|
|
let doomed = candidates_in_constraint_cell(state.election, &state.candidates, idx.slice());
|
|
|
|
if !doomed.is_empty() {
|
|
|
|
for candidate in doomed.iter() {
|
|
|
|
state.candidates.get_mut(candidate).unwrap().state = CandidateState::Doomed;
|
|
|
|
}
|
|
|
|
|
|
|
|
state.logger.log_smart(
|
|
|
|
"{} must be doomed to comply with constraints.",
|
|
|
|
"{} must be doomed to comply with constraints.",
|
|
|
|
doomed.iter().map(|c| c.name.as_str()).collect()
|
|
|
|
);
|
|
|
|
|
|
|
|
guarded_or_doomed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if cell.cands == cell.min {
|
|
|
|
// Guard remaining candidates in this cell
|
|
|
|
let guarded = candidates_in_constraint_cell(state.election, &state.candidates, idx.slice());
|
|
|
|
if !guarded.is_empty() {
|
|
|
|
for candidate in guarded.iter() {
|
|
|
|
state.candidates.get_mut(candidate).unwrap().state = CandidateState::Guarded;
|
|
|
|
}
|
|
|
|
|
|
|
|
state.logger.log_smart(
|
|
|
|
"{} must be guarded to comply with constraints.",
|
|
|
|
"{} must be guarded to comply with constraints.",
|
|
|
|
guarded.iter().map(|c| c.name.as_str()).collect()
|
|
|
|
);
|
|
|
|
|
|
|
|
guarded_or_doomed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return guarded_or_doomed;
|
|
|
|
}
|
|
|
|
_ => { todo!() }
|
|
|
|
}
|
|
|
|
|
|
|
|
//return false;
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// 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
|
2021-06-09 12:42:47 +10:00
|
|
|
fn can_defer_surpluses<N: Number>(state: &CountState<N>, opts: &STVOptions, total_surpluses: &N) -> bool
|
2021-06-09 12:16:25 +10:00
|
|
|
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()
|
2021-06-12 00:50:01 +10:00
|
|
|
.filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded)
|
2021-06-09 12:16:25 +10:00
|
|
|
.collect();
|
|
|
|
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.get(c).unwrap().votes);
|
|
|
|
if total_surpluses > &(&hopefuls[num_to_exclude + 1].1.votes - &total_excluded) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Distribute surpluses according to [STVOptions::surplus]
|
2021-06-12 02:09:26 +10:00
|
|
|
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
2021-05-29 00:43:58 +10:00
|
|
|
where
|
2021-05-30 02:28:52 +10:00
|
|
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
2021-06-16 13:00:54 +10:00
|
|
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
2021-06-11 21:22:28 +10:00
|
|
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
2021-06-16 13:00:54 +10:00
|
|
|
for<'r> &'r N: ops::Neg<Output=N>,
|
2021-05-29 00:43:58 +10:00
|
|
|
{
|
2021-05-31 22:25:53 +10:00
|
|
|
match opts.surplus {
|
2021-06-14 20:43:36 +10:00
|
|
|
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
|
|
|
return gregory::distribute_surpluses(state, opts);
|
2021-05-31 22:25:53 +10:00
|
|
|
}
|
|
|
|
SurplusMethod::Meek => {
|
2021-06-16 13:00:54 +10:00
|
|
|
return meek::distribute_surpluses(state, opts);
|
2021-05-31 22:25:53 +10:00
|
|
|
}
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Declare all continuing candidates elected, if the number equals the number of remaining vacancies
|
2021-06-13 00:15:14 +10:00
|
|
|
fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> {
|
2021-05-28 19:58:40 +10:00
|
|
|
if state.election.candidates.len() - state.num_excluded <= state.election.seats {
|
2021-05-29 01:22:46 +10:00
|
|
|
state.kind = None;
|
|
|
|
state.title = "Bulk election".to_string();
|
|
|
|
|
2021-05-28 19:58:40 +10:00
|
|
|
// Bulk elect all remaining candidates
|
2021-06-13 00:15:14 +10:00
|
|
|
let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter()
|
2021-06-27 21:57:24 +10:00
|
|
|
.filter(|c| {
|
|
|
|
let cc = state.candidates.get(c).unwrap();
|
|
|
|
return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded;
|
|
|
|
})
|
2021-05-28 22:37:18 +10:00
|
|
|
.collect();
|
|
|
|
|
2021-06-13 00:15:14 +10:00
|
|
|
while !hopefuls.is_empty() {
|
|
|
|
let max_votes = hopefuls.iter()
|
|
|
|
.max_by(|a, b| state.candidates.get(**a).unwrap().votes.cmp(&state.candidates.get(**b).unwrap().votes))
|
|
|
|
.unwrap();
|
|
|
|
let max_votes = &state.candidates.get(max_votes).unwrap().votes;
|
|
|
|
let max_hopefuls: Vec<&Candidate> = hopefuls.iter()
|
|
|
|
.filter(|c| &state.candidates.get(**c).unwrap().votes == max_votes)
|
|
|
|
.map(|c| *c)
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let candidate;
|
|
|
|
if max_hopefuls.len() > 1 {
|
|
|
|
// Handle ties
|
|
|
|
candidate = choose_highest(state, opts, max_hopefuls)?;
|
|
|
|
} else {
|
|
|
|
candidate = max_hopefuls[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
2021-06-12 00:50:01 +10:00
|
|
|
count_card.state = CandidateState::Elected;
|
2021-05-28 19:58:40 +10:00
|
|
|
state.num_elected += 1;
|
|
|
|
count_card.order_elected = state.num_elected as isize;
|
2021-05-29 01:22:46 +10:00
|
|
|
|
2021-05-29 02:13:47 +10:00
|
|
|
state.logger.log_smart(
|
|
|
|
"{} is elected to fill the remaining vacancy.",
|
|
|
|
"{} are elected to fill the remaining vacancies.",
|
|
|
|
vec![&candidate.name]
|
|
|
|
);
|
2021-06-13 00:15:14 +10:00
|
|
|
|
|
|
|
hopefuls.remove(hopefuls.iter().position(|c| *c == candidate).unwrap());
|
2021-06-27 21:57:24 +10:00
|
|
|
|
|
|
|
update_constraints(state, opts);
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-13 00:15:14 +10:00
|
|
|
return Ok(true);
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
2021-06-13 00:15:14 +10:00
|
|
|
return Ok(false);
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
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 mut doomed: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
|
|
|
|
.map(|c| (c, state.candidates.get(c).unwrap()))
|
|
|
|
.filter(|(_, cc)| cc.state == CandidateState::Doomed)
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
if !doomed.is_empty() {
|
|
|
|
let excluded_candidates;
|
|
|
|
|
|
|
|
if opts.bulk_exclude {
|
|
|
|
excluded_candidates = doomed.into_iter().map(|(c, _)| c).collect();
|
|
|
|
} else {
|
|
|
|
// Exclude only the lowest-ranked doomed candidate
|
|
|
|
// Sort by votes
|
|
|
|
doomed.sort_by(|a, b| a.1.votes.cmp(&b.1.votes));
|
|
|
|
|
|
|
|
// Handle ties
|
|
|
|
if doomed.len() > 1 && doomed[0].1.votes == doomed[1].1.votes {
|
|
|
|
let min_votes = &doomed[0].1.votes;
|
|
|
|
let doomed = doomed.into_iter().filter_map(|(c, cc)| if &cc.votes == min_votes { Some(c) } else { None }).collect();
|
|
|
|
excluded_candidates = vec![choose_lowest(state, opts, doomed)?];
|
|
|
|
} else {
|
|
|
|
excluded_candidates = vec![&doomed[0].0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect();
|
|
|
|
state.kind = Some("Exclusion of");
|
|
|
|
state.title = names.join(", ");
|
|
|
|
state.logger.log_smart(
|
|
|
|
"Doomed candidate, {}, is excluded.",
|
|
|
|
"Doomed candidates, {}, are excluded.",
|
|
|
|
names
|
|
|
|
);
|
|
|
|
|
|
|
|
exclude_candidates(state, opts, excluded_candidates);
|
|
|
|
return Ok(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// 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
|
2021-06-09 12:16:25 +10:00
|
|
|
fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &STVOptions) -> Vec<&'a Candidate> {
|
|
|
|
let mut excluded_candidates = Vec::new();
|
|
|
|
|
2021-05-28 22:37:18 +10:00
|
|
|
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
2021-06-12 00:50:01 +10:00
|
|
|
.filter(|(_, cc)| cc.state == CandidateState::Hopeful)
|
2021-05-28 22:37:18 +10:00
|
|
|
.collect();
|
2021-05-28 19:58:40 +10:00
|
|
|
|
|
|
|
// Sort by votes
|
2021-06-13 00:15:14 +10:00
|
|
|
// 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));
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-09 12:16:25 +10:00
|
|
|
let total_surpluses = state.candidates.iter()
|
|
|
|
.filter(|(_, cc)| &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.into_iter().fold(N::new(), |agg, (_, cc)| agg + &cc.votes);
|
2021-06-13 00:15:14 +10:00
|
|
|
if i != 0 && total_votes + &total_surpluses >= hopefuls[hopefuls.len()-i].1.votes {
|
2021-06-09 12:16:25 +10:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (c, _) in try_exclude.into_iter() {
|
|
|
|
excluded_candidates.push(**c);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return excluded_candidates;
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Exclude the lowest-ranked hopeful candidate(s)
|
2021-06-12 02:09:26 +10:00
|
|
|
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
2021-06-09 12:16:25 +10:00
|
|
|
where
|
2021-06-16 13:00:54 +10:00
|
|
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
|
|
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
2021-06-09 12:16:25 +10:00
|
|
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
|
|
|
{
|
2021-06-08 22:22:43 +10:00
|
|
|
let mut excluded_candidates: Vec<&Candidate> = Vec::new();
|
|
|
|
|
|
|
|
// Attempt a bulk exclusion
|
|
|
|
if opts.bulk_exclude {
|
2021-06-09 12:16:25 +10:00
|
|
|
excluded_candidates = hopefuls_to_bulk_exclude(state, opts);
|
2021-06-08 22:22:43 +10:00
|
|
|
}
|
|
|
|
|
2021-05-28 19:58:40 +10:00
|
|
|
// Exclude lowest ranked candidate
|
2021-06-13 00:15:14 +10:00
|
|
|
if excluded_candidates.is_empty() {
|
2021-06-12 02:09:26 +10:00
|
|
|
let mut hopefuls: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
|
|
|
|
.map(|c| (c, state.candidates.get(c).unwrap()))
|
2021-06-12 00:50:01 +10:00
|
|
|
.filter(|(_, cc)| cc.state == CandidateState::Hopeful)
|
|
|
|
.collect();
|
2021-06-09 12:16:25 +10:00
|
|
|
|
|
|
|
// Sort by votes
|
2021-06-13 00:15:14 +10:00
|
|
|
hopefuls.sort_by(|a, b| a.1.votes.cmp(&b.1.votes));
|
2021-06-09 12:16:25 +10:00
|
|
|
|
2021-06-12 02:09:26 +10:00
|
|
|
// Handle ties
|
|
|
|
if hopefuls.len() > 1 && hopefuls[0].1.votes == hopefuls[1].1.votes {
|
|
|
|
let min_votes = &hopefuls[0].1.votes;
|
|
|
|
let hopefuls = hopefuls.into_iter().filter_map(|(c, cc)| if &cc.votes == min_votes { Some(c) } else { None }).collect();
|
2021-06-13 00:15:14 +10:00
|
|
|
excluded_candidates = vec![choose_lowest(state, opts, hopefuls)?];
|
2021-06-12 02:09:26 +10:00
|
|
|
} else {
|
2021-06-13 00:15:14 +10:00
|
|
|
excluded_candidates = vec![&hopefuls[0].0];
|
2021-06-12 02:09:26 +10:00
|
|
|
}
|
2021-06-08 22:22:43 +10:00
|
|
|
}
|
2021-05-29 01:22:46 +10:00
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect();
|
2021-05-29 01:22:46 +10:00
|
|
|
state.kind = Some("Exclusion of");
|
2021-06-08 22:22:43 +10:00
|
|
|
state.title = names.join(", ");
|
2021-05-29 02:13:47 +10:00
|
|
|
state.logger.log_smart(
|
|
|
|
"No surpluses to distribute, so {} is excluded.",
|
|
|
|
"No surpluses to distribute, so {} are excluded.",
|
2021-06-08 22:22:43 +10:00
|
|
|
names
|
2021-05-29 02:13:47 +10:00
|
|
|
);
|
2021-05-29 01:22:46 +10:00
|
|
|
|
2021-06-08 22:22:43 +10:00
|
|
|
exclude_candidates(state, opts, excluded_candidates);
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-12 02:09:26 +10:00
|
|
|
return Ok(true);
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Continue the exclusion of a candidate who is being excluded
|
2021-06-08 22:22:43 +10:00
|
|
|
fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
|
2021-05-30 02:28:52 +10:00
|
|
|
where
|
2021-06-16 13:00:54 +10:00
|
|
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
|
|
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
2021-05-30 02:28:52 +10:00
|
|
|
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
|
2021-05-28 22:37:18 +10:00
|
|
|
let mut excluded_with_votes: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
2021-06-13 00:15:14 +10:00
|
|
|
.filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| !p.is_empty()))
|
2021-05-28 22:37:18 +10:00
|
|
|
.collect();
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-13 00:15:14 +10:00
|
|
|
if !excluded_with_votes.is_empty() {
|
|
|
|
excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
|
2021-05-29 01:22:46 +10:00
|
|
|
|
2021-06-13 00:15:14 +10:00
|
|
|
let order_excluded = excluded_with_votes[0].1.order_elected;
|
2021-06-08 22:22:43 +10:00
|
|
|
let excluded_candidates: Vec<&Candidate> = excluded_with_votes.into_iter()
|
|
|
|
.filter(|(_, cc)| cc.order_elected == order_excluded)
|
|
|
|
.map(|(c, _)| *c)
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let mut names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect();
|
|
|
|
names.sort();
|
2021-05-29 01:22:46 +10:00
|
|
|
state.kind = Some("Exclusion of");
|
2021-06-08 22:22:43 +10:00
|
|
|
state.title = names.join(", ");
|
2021-05-29 02:13:47 +10:00
|
|
|
state.logger.log_smart(
|
|
|
|
"Continuing exclusion of {}.",
|
|
|
|
"Continuing exclusion of {}.",
|
2021-06-08 22:22:43 +10:00
|
|
|
names
|
2021-05-29 02:13:47 +10:00
|
|
|
);
|
2021-05-29 01:22:46 +10:00
|
|
|
|
2021-06-08 22:22:43 +10:00
|
|
|
exclude_candidates(state, opts, excluded_candidates);
|
2021-05-28 19:58:40 +10:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion]
|
2021-06-08 22:22:43 +10:00
|
|
|
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
2021-05-30 02:28:52 +10:00
|
|
|
where
|
2021-06-16 13:00:54 +10:00
|
|
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
|
|
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
2021-05-30 02:28:52 +10:00
|
|
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
|
|
|
{
|
2021-06-22 14:34:26 +10:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ExclusionMethod::ByValue | ExclusionMethod::ParcelsByOrder => {
|
|
|
|
// Exclusion in parts compatible only with Gregory method
|
2021-06-16 13:00:54 +10:00
|
|
|
gregory::exclude_candidates(state, opts, excluded_candidates);
|
2021-06-08 22:22:43 +10:00
|
|
|
}
|
2021-06-22 15:23:46 +10:00
|
|
|
ExclusionMethod::Wright => {
|
|
|
|
gregory::wright_exclude_candidates(state, opts, excluded_candidates);
|
2021-05-30 02:28:52 +10:00
|
|
|
}
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Determine if the count is complete because the number of elected candidates equals the number of vacancies
|
2021-05-29 17:51:45 +10:00
|
|
|
fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
|
2021-05-28 19:58:40 +10:00
|
|
|
if state.num_elected >= state.election.seats {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2021-06-13 00:15:14 +10:00
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// 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
|
2021-06-13 00:56:18 +10:00
|
|
|
fn choose_highest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
2021-06-13 00:15:14 +10:00
|
|
|
for strategy in opts.ties.iter() {
|
|
|
|
match strategy.choose_highest(state, &candidates) {
|
|
|
|
Ok(c) => {
|
|
|
|
return Ok(c);
|
|
|
|
}
|
|
|
|
Err(e) => {
|
|
|
|
if let STVError::UnresolvedTie = e {
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
return Err(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
panic!("Unable to resolve tie");
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// 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
|
2021-06-13 00:56:18 +10:00
|
|
|
fn choose_lowest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
2021-06-13 00:15:14 +10:00
|
|
|
for strategy in opts.ties.iter() {
|
|
|
|
match strategy.choose_lowest(state, &candidates) {
|
|
|
|
Ok(c) => {
|
|
|
|
return Ok(c);
|
|
|
|
}
|
|
|
|
Err(e) => {
|
|
|
|
if let STVError::UnresolvedTie = e {
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
return Err(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-06-14 20:43:36 +10:00
|
|
|
panic!("Unable to resolve tie");
|
2021-06-13 00:15:14 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// If required, initialise the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties]
|
2021-06-13 00:15:14 +10:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// If required, update the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties]
|
2021-06-13 00:15:14 +10:00
|
|
|
fn update_tiebreaks<N: Number>(state: &mut CountState<N>, _opts: &STVOptions) {
|
|
|
|
if let None = state.forwards_tiebreak {
|
|
|
|
if let None = state.backwards_tiebreak {
|
|
|
|
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.into_iter().map(|c| *c).collect();
|
|
|
|
tied_this_round.sort_unstable_by(|a, b| hm_orig.get(a).unwrap().cmp(hm_orig.get(b).unwrap()));
|
|
|
|
let tied_this_round = tied_this_round.into_iter()
|
|
|
|
.group_by(|c| hm_orig.get(c).unwrap());
|
|
|
|
|
|
|
|
for (_, group2) in tied_this_round.into_iter() {
|
|
|
|
for candidate in group2 {
|
|
|
|
hm.insert(candidate, i);
|
|
|
|
}
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|