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