/* 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 . */ #![allow(mutable_borrow_reservation_conflict)] /// Gregory method of surplus distributions pub mod gregory; /// Meek method of surplus distributions, etc. pub mod meek; /// WebAssembly wrappers //#[cfg(target_arch = "wasm32")] pub mod wasm; use crate::constraints; use crate::numbers::Number; use crate::election::{Candidate, CandidateState, CountCard, CountState, Vote}; use crate::sharandom::SHARandom; use crate::ties::{self, TieStrategy}; use itertools::Itertools; use wasm_bindgen::prelude::wasm_bindgen; use std::collections::HashMap; use std::ops; /// Options for conducting an STV count pub struct STVOptions { /// Round transfer values to specified decimal places pub round_tvs: Option, /// Round ballot weights to specified decimal places pub round_weights: Option, /// Round votes to specified decimal places pub round_votes: Option, /// Round quota to specified decimal places pub round_quota: Option, /// How to calculate votes to credit to candidates in surplus transfers pub sum_surplus_transfers: SumSurplusTransfersMode, /// (Meek STV) Limit for stopping iteration of surplus distribution pub meek_surplus_tolerance: String, /// Convert ballots with value >1 to multiple ballots of value 1 (used only for [STVOptions::describe]) pub normalise_ballots: bool, /// Quota type pub quota: QuotaType, /// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota pub quota_criterion: QuotaCriterion, /// Whether to apply a form of progressive quota pub quota_mode: QuotaMode, /// Tie-breaking method pub ties: Vec, /// Method of surplus distributions pub surplus: SurplusMethod, /// Order to distribute surpluses pub surplus_order: SurplusOrder, /// Examine only transferable papers during surplus distributions pub transferable_only: bool, /// Method of exclusions pub exclusion: ExclusionMethod, /// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion pub meek_nz_exclusion: bool, /// Bulk elect as soon as continuing candidates fill all remaining vacancies pub early_bulk_elect: bool, /// Use bulk exclusion pub bulk_exclude: bool, /// Defer surplus distributions if possible pub defer_surpluses: bool, /// (Meek STV) Immediately elect candidates even if keep values have not converged pub meek_immediate_elect: bool, /// Path to constraints file (used only for [STVOptions::describe]) pub constraints_path: Option, /// Mode of handling constraints pub constraint_mode: ConstraintMode, /// Print votes to specified decimal places in results report pub pp_decimals: usize, } impl STVOptions { /// Returns a new [STVOptions] based on arguments given as strings pub fn new( round_tvs: Option, round_weights: Option, round_votes: Option, round_quota: Option, sum_surplus_transfers: &str, meek_surplus_tolerance: &str, normalise_ballots: bool, quota: &str, quota_criterion: &str, quota_mode: &str, ties: &Vec, random_seed: &Option, surplus: &str, surplus_order: &str, transferable_only: bool, exclusion: &str, meek_nz_exclusion: bool, early_bulk_elect: bool, bulk_exclude: bool, defer_surpluses: bool, meek_immediate_elect: bool, constraints_path: Option<&str>, constraint_mode: &str, pp_decimals: usize, ) -> Self { return STVOptions { round_tvs, round_weights, round_votes, round_quota, sum_surplus_transfers: match sum_surplus_transfers { "single_step" => SumSurplusTransfersMode::SingleStep, "by_value" => SumSurplusTransfersMode::ByValue, "per_ballot" => SumSurplusTransfersMode::PerBallot, _ => panic!("Invalid --sum-transfers"), }, meek_surplus_tolerance: meek_surplus_tolerance.to_string(), normalise_ballots, 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"), }, quota_mode: match quota_mode { "static" => QuotaMode::Static, "ers97" => QuotaMode::ERS97, "ers76" => QuotaMode::ERS76, _ => panic!("Invalid --quota-mode"), }, ties: ties.into_iter().map(|t| match t.as_str() { "forwards" => TieStrategy::Forwards, "backwards" => TieStrategy::Backwards, "random" => TieStrategy::Random(random_seed.as_ref().expect("Must provide a --random-seed if using --ties random").clone()), "prompt" => TieStrategy::Prompt, _ => panic!("Invalid --ties"), }).collect(), 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"), }, transferable_only, exclusion: match exclusion { "single_stage" => ExclusionMethod::SingleStage, "by_value" => ExclusionMethod::ByValue, "by_source" => ExclusionMethod::BySource, "parcels_by_order" => ExclusionMethod::ParcelsByOrder, "wright" => ExclusionMethod::Wright, _ => panic!("Invalid --exclusion"), }, meek_nz_exclusion, early_bulk_elect, bulk_exclude, defer_surpluses, meek_immediate_elect, constraints_path: match constraints_path { Some(p) => Some(p.to_string()), None => None, }, constraint_mode: match constraint_mode { "guard_doom" => ConstraintMode::GuardDoom, "rollback" => ConstraintMode::Rollback, _ => panic!("Invalid --constraint-mode"), }, pp_decimals, }; } /// Converts the [STVOptions] into CLI argument representation pub fn describe(&self) -> String { let mut flags = Vec::new(); let n_str = N::describe_opt(); if !n_str.is_empty() { flags.push(N::describe_opt()) }; 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)); } if self.surplus != SurplusMethod::Meek && self.sum_surplus_transfers != SumSurplusTransfersMode::SingleStep { flags.push(self.sum_surplus_transfers.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::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()); } 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 && 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()); } 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.surplus == SurplusMethod::Meek && self.meek_immediate_elect { flags.push("--meek-immediate-elect".to_string()); } 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.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) { 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"); } } } } /// Enum of options for [STVOptions::sum_surplus_transfers] #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum SumSurplusTransfersMode { /// Sum and round all surplus transfers for a candidate in a single step SingleStep, /// Sum and round a candidate's surplus transfers separately for ballot papers received at each particular value ByValue, /// Sum and round a candidate's surplus transfers individually for each ballot paper PerBallot, } impl SumSurplusTransfersMode { /// Convert to CLI argument representation 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() } } /// Enum of options for [STVOptions::quota] #[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() } } /// Enum of options for [STVOptions::quota_criterion] #[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() } } /// Enum of options for [STVOptions::quota_mode] #[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, } 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", }.to_string() } } /// Enum of options for [STVOptions::surplus] #[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, } 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", }.to_string() } } /// Enum of options for [STVOptions::surplus_order] #[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() } } /// Enum of options for [STVOptions::exclusion] #[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() } } /// 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() } } /// An error during the STV count #[wasm_bindgen] #[derive(Debug)] pub enum STVError { /// User input is required RequireInput, /// Tie could not be resolved UnresolvedTie, } impl STVError { /// Return the name of the error as a string pub fn name(&self) -> &'static str { match self { STVError::RequireInput => "RequireInput", STVError::UnresolvedTie => "UnresolvedTie", } } } /// 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 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_meeting_quota(state, opts)?; init_tiebreaks(state, opts); return Ok(true); } /// Perform a single stage of the STV count pub fn count_one_stage<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result 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>, for<'r> &'r N: ops::Neg, { 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_meeting_quota(state, opts)?; update_tiebreaks(state, opts); return Ok(false); } // Exclude doomed candidates if exclude_doomed(state, &opts)? { calculate_quota(state, opts); elect_meeting_quota(state, opts)?; update_tiebreaks(state, opts); return Ok(false); } // Distribute surpluses if distribute_surpluses(state, &opts)? { calculate_quota(state, opts); elect_meeting_quota(state, opts)?; update_tiebreaks(state, opts); return Ok(false); } // Attempt late bulk election if bulk_elect(state, &opts)? { return Ok(false); } // Exclude lowest hopeful if exclude_hopefuls(state, &opts)? { calculate_quota(state, opts); elect_meeting_quota(state, opts)?; update_tiebreaks(state, opts); return Ok(false); } panic!("Count incomplete but unable to proceed"); } /// See [next_preferences] struct NextPreferencesResult<'a, N> { candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>, exhausted: NextPreferencesEntry<'a, N>, total_ballots: N, total_votes: N, } /// See [next_preferences] struct NextPreferencesEntry<'a, N> { //count_card: Option<&'a CountCard<'a, N>>, votes: Vec>, num_ballots: N, num_votes: N, } /// Count the given votes, grouping according to next available preference fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec>) -> 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(), total_votes: N::new(), }; for mut vote in votes.into_iter() { result.total_ballots += &vote.ballot.orig_value; result.total_votes += &vote.value; 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[candidate]; if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state { 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; } /// Distribute first preference votes fn distribute_first_preferences(state: &mut CountState, 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 => { meek::distribute_first_preferences(state, opts); } } } /// Calculate the quota, given the total vote, according to [STVOptions::quota] fn total_to_quota(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; } /// Calculate the quota according to [STVOptions::quota] fn calculate_quota(state: &mut CountState, opts: &STVOptions) { // Calculate quota if state.quota.is_none() || opts.surplus == SurplusMethod::Meek { 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); } if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 { // ERS97/ERS76 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 { CandidateState::Elected => { if &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } } _ => { 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/ERS76 rules if state.vote_required_election.is_none() || opts.surplus == SurplusMethod::Meek { state.vote_required_election = state.quota.clone(); } } } /// Determine if the given candidate meets the quota, according to [STVOptions::quota_criterion] fn meets_quota(quota: &N, count_card: &CountCard, opts: &STVOptions) -> bool { match opts.quota_criterion { QuotaCriterion::GreaterOrEqual => { return count_card.votes >= *quota; } QuotaCriterion::Greater => { return count_card.votes > *quota; } } } /// Declare elected all candidates meeting the quota fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result { let vote_req = state.vote_required_election.as_ref().unwrap().clone(); // Have to do this or else the borrow checker gets confused let mut cands_meeting_quota: Vec<(&Candidate, &CountCard)> = 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_quota(&vote_req, cc, opts) }) .collect(); // Sort by votes cands_meeting_quota.sort_unstable_by(|a, b| a.1.votes.cmp(&b.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() { // 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)? } 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( "{} meets the quota and is elected.", "{} meet the quota 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)> = 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_quota(&vote_req, cc, opts) }) .collect(); cmq.sort_unstable_by(|a, b| a.1.votes.cmp(&b.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 { // Vote required for election may have changed // This is not performed in ERS76 - TODO: Check this calculate_quota(state, opts); // Repeat in case vote required for election has changed match elect_meeting_quota(state, opts) { Ok(_) => {} Err(e) => { return Err(e); } } } } 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(state: &CountState, 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)> = state.candidates.iter() .filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) .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[c].votes); if total_surpluses > &(&hopefuls[num_to_exclude + 1].1.votes - &total_excluded) { return false; } } } return true; } /// Distribute surpluses according to [STVOptions::surplus] fn distribute_surpluses(state: &mut CountState, opts: &STVOptions) -> Result 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>, for<'r> &'r N: ops::Neg, { match opts.surplus { SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { 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(state: &CountState, num_to_exclude: usize) -> bool { let num_hopefuls = state.election.candidates.iter() .filter(|c| { let cc = &state.candidates[c]; return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded; }) .count(); if 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(state: &mut CountState, opts: &STVOptions, template1: &'static str, template2: &'static str) -> Result { 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)? } 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(true); } /// Declare all continuing candidates elected, if the number equals the number of remaining vacancies fn bulk_elect(state: &mut CountState, opts: &STVOptions) -> Result { if can_bulk_elect(state, 0) { state.kind = None; state.title = "Bulk election".to_string(); return do_bulk_elect(state, opts, "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies."); } return Ok(false); } fn exclude_doomed<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result 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)?] } else { vec![min_cands[0]] }; } let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect(); state.kind = Some("Exclusion of"); state.title = names.join(", "); 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 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); } return 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 exclude_candidates(state, opts, excluded_candidates); } return Ok(false); } /// 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)> = 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.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); if i != 0 && total_votes + &total_surpluses >= hopefuls[hopefuls.len()-i].1.votes { continue; } for (c, _) in try_exclude.into_iter() { excluded_candidates.push(**c); } 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 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(); // Attempt a bulk exclusion if 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)?] } else { vec![min_cands[0]] }; } let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect(); state.kind = Some("Exclusion of"); state.title = names.join(", "); 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 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); } return 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 exclude_candidates(state, opts, excluded_candidates); } /// Continue the exclusion of a candidate who is being excluded fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result 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)> = state.candidates.iter() .filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| !p.votes.is_empty())) .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.kind = Some("Exclusion of"); state.title = names.join(", "); state.logger.log_smart( "Continuing exclusion of {}.", "Continuing exclusion of {}.", names ); return exclude_candidates(state, opts, excluded_candidates); } 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 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); } } } 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(true); } /// Determine if the count is complete because the number of elected candidates equals the number of vacancies fn finished_before_stage(state: &CountState) -> 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 fn choose_highest<'c, N: Number>(state: &mut CountState, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { 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"); } /// 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 fn choose_lowest<'c, N: Number>(state: &mut CountState, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { 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); } } } } panic!("Unable to resolve tie"); } /// If required, initialise the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties] fn init_tiebreaks(state: &mut CountState, 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)> = state.candidates.iter().collect(); sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes)); let sorted_candidates: Vec)>> = 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(state: &mut CountState, _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)> = state.candidates.iter().collect(); sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes)); let sorted_candidates: Vec> = 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> = 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[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; } } } } }