/* 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)] //#[cfg(target_arch = "wasm32")] pub mod wasm; use crate::numbers::Number; use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; use crate::ties::TieStrategy; use itertools::Itertools; use wasm_bindgen::prelude::wasm_bindgen; use std::cmp::max; use std::collections::HashMap; use std::ops; pub struct STVOptions<'o> { pub round_tvs: Option, pub round_weights: Option, pub round_votes: Option, pub round_quota: Option, pub sum_surplus_transfers: SumSurplusTransfersMode, pub normalise_ballots: bool, pub quota: QuotaType, pub quota_criterion: QuotaCriterion, pub quota_mode: QuotaMode, pub ties: Vec>, pub surplus: SurplusMethod, pub surplus_order: SurplusOrder, pub transferable_only: bool, pub exclusion: ExclusionMethod, pub bulk_exclude: bool, pub defer_surpluses: bool, pub pp_decimals: usize, } impl<'o> STVOptions<'o> { pub fn new( round_tvs: Option, round_weights: Option, round_votes: Option, round_quota: Option, sum_surplus_transfers: &str, normalise_ballots: bool, quota: &str, quota_criterion: &str, quota_mode: &str, ties: &Vec, surplus: &str, surplus_order: &str, transferable_only: bool, exclusion: &str, bulk_exclude: bool, defer_surpluses: bool, 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"), }, 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, _ => panic!("Invalid --quota-mode"), }, ties: ties.into_iter().map(|t| match t.as_str() { "forwards" => TieStrategy::Forwards, "backwards" => TieStrategy::Backwards, "random" => TieStrategy::Random(&"TODO"), "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, "parcels_by_order" => ExclusionMethod::ParcelsByOrder, _ => panic!("Invalid --exclusion"), }, bulk_exclude, defer_surpluses, pp_decimals, }; } 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.sum_surplus_transfers != SumSurplusTransfersMode::SingleStep { flags.push(self.sum_surplus_transfers.describe()); } if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); } let ties_str = self.ties.iter().map(|t| t.describe()).join(" "); if ties_str != "prompt" { flags.push(format!("--ties {}", ties_str)); } if self.quota != QuotaType::DroopExact { flags.push(self.quota.describe()); } if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); } if self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); } if self.surplus != SurplusMethod::WIG { flags.push(self.surplus.describe()); } if self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); } if self.transferable_only { flags.push("--transferable-only".to_string()); } if self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); } if self.bulk_exclude { flags.push("--bulk-exclude".to_string()); } if self.defer_surpluses { flags.push("--defer-surpluses".to_string()); } if self.pp_decimals != 2 { flags.push(format!("--pp-decimals {}", self.pp_decimals)); } return flags.join(" "); } } #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum SumSurplusTransfersMode { SingleStep, ByValue, PerBallot, } impl SumSurplusTransfersMode { 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() } } #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum QuotaType { Droop, Hare, DroopExact, HareExact, } impl QuotaType { 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() } } #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum QuotaCriterion { GreaterOrEqual, Greater, } impl QuotaCriterion { fn describe(self) -> String { match self { QuotaCriterion::GreaterOrEqual => "--quota-criterion geq", QuotaCriterion::Greater => "--quota-criterion gt", }.to_string() } } #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum QuotaMode { Static, ERS97, } impl QuotaMode { fn describe(self) -> String { match self { QuotaMode::Static => "--quota-mode static", QuotaMode::ERS97 => "--quota-mode ers97", }.to_string() } } #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum SurplusMethod { WIG, UIG, EG, Meek, } impl SurplusMethod { fn describe(self) -> String { match self { SurplusMethod::WIG => "--surplus wig", SurplusMethod::UIG => "--surplus uig", SurplusMethod::EG => "--surplus eg", SurplusMethod::Meek => "--surplus meek", }.to_string() } } #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum SurplusOrder { BySize, ByOrder, } impl SurplusOrder { fn describe(self) -> String { match self { SurplusOrder::BySize => "--surplus-order by_size", SurplusOrder::ByOrder => "--surplus-order by_order", }.to_string() } } #[wasm_bindgen] #[derive(Clone, Copy)] #[derive(PartialEq)] pub enum ExclusionMethod { SingleStage, ByValue, ParcelsByOrder, } impl ExclusionMethod { fn describe(self) -> String { match self { ExclusionMethod::SingleStage => "--exclusion single_stage", ExclusionMethod::ByValue => "--exclusion by_value", ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order", }.to_string() } } #[wasm_bindgen] #[derive(Debug)] pub enum STVError { RequireInput, UnresolvedTie, } pub fn count_init(mut state: &mut CountState<'_, N>, opts: &STVOptions) { distribute_first_preferences(&mut state); calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); init_tiebreaks(&mut state, opts); } pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result where for<'r> &'r N: ops::Sub<&'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); } // Continue exclusions if continue_exclusion(&mut state, &opts) { calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); update_tiebreaks(&mut state, opts); return Ok(false); } // Distribute surpluses if distribute_surpluses(&mut state, &opts)? { calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); update_tiebreaks(&mut state, opts); return Ok(false); } // Attempt bulk election if bulk_elect(&mut state, &opts)? { return Ok(false); } // Exclude lowest hopeful if exclude_hopefuls(&mut state, &opts)? { calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); update_tiebreaks(&mut state, opts); return Ok(false); } panic!("Count incomplete but unable to proceed"); } struct NextPreferencesResult<'a, N> { candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>, exhausted: NextPreferencesEntry<'a, N>, total_ballots: N, total_votes: N, } struct NextPreferencesEntry<'a, N> { //count_card: Option<&'a CountCard<'a, N>>, votes: Vec>, num_ballots: N, num_votes: N, } 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.get(candidate).unwrap(); 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; } fn distribute_first_preferences(state: &mut CountState) { let votes = state.election.ballots.iter().map(|b| Vote { ballot: b, value: b.orig_value.clone(), up_to_pref: 0, }).collect(); let result = next_preferences(state, votes); // Transfer candidate votes for (candidate, entry) in result.candidates.into_iter() { let parcel = entry.votes as Parcel; let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); count_card.transfer(&entry.num_votes); } // Transfer exhausted votes let parcel = result.exhausted.votes as Parcel; state.exhausted.parcels.push(parcel); state.exhausted.transfer(&result.exhausted.num_votes); state.kind = None; state.title = "First preferences".to_string(); state.logger.log_literal("First preferences distributed.".to_string()); } 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; } fn calculate_quota(state: &mut CountState, opts: &STVOptions) { // Calculate quota if let None = state.quota { 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 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 { 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 rules if let None = state.vote_required_election { state.vote_required_election = state.quota.clone(); } } } 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; } } } fn elect_meeting_quota(state: &mut CountState, opts: &STVOptions) { let vote_req = state.vote_required_election.as_ref().unwrap(); // Have to do this or else the borrow checker gets confused let mut cands_meeting_quota: Vec<&Candidate> = state.election.candidates.iter() .filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::Hopeful && meets_quota(vote_req, cc, opts) }) .collect(); if !cands_meeting_quota.is_empty() { // Sort by votes cands_meeting_quota.sort_unstable_by(|a, b| state.candidates.get(a).unwrap().votes.cmp(&state.candidates.get(b).unwrap().votes)); // Declare elected in descending order of votes for candidate in cands_meeting_quota.into_iter().rev() { 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 opts.quota_mode == QuotaMode::ERS97 { // Vote required for election may have changed calculate_quota(state, opts); } } if opts.quota_mode == QuotaMode::ERS97 { // Repeat in case vote required for election has changed //calculate_quota(state, opts); elect_meeting_quota(state, opts); } } } 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.get(c).unwrap().votes); if total_surpluses > &(&hopefuls[num_to_exclude + 1].1.votes - &total_excluded) { return false; } } } return true; } 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::Div<&'r N, Output=N>, for<'r> &'r N: ops::Neg { let quota = state.quota.as_ref().unwrap(); let mut has_surplus: Vec<(&Candidate, &CountCard)> = state.election.candidates.iter() // Present in order in case of tie .map(|c| (c, state.candidates.get(c).unwrap())) .filter(|(_, cc)| &cc.votes > quota) .collect(); let total_surpluses = has_surplus.iter() .fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota); if !has_surplus.is_empty() { // Determine if surplues can be deferred if opts.defer_surpluses { if can_defer_surpluses(state, opts, &total_surpluses) { state.logger.log_literal(format!("Distribution of surpluses totalling {:.dps$} votes will be deferred.", total_surpluses, dps=opts.pp_decimals)); return Ok(false); } } match opts.surplus_order { SurplusOrder::BySize => { // Compare b with a to sort high-low has_surplus.sort_by(|a, b| b.1.votes.cmp(&a.1.votes)); } SurplusOrder::ByOrder => { has_surplus.sort_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected)); } } // Distribute top candidate's surplus let elected_candidate; // Handle ties if has_surplus.len() > 1 && has_surplus[0].1.votes == has_surplus[1].1.votes { let max_votes = &has_surplus[0].1.votes; let has_surplus = has_surplus.into_iter().filter_map(|(c, cc)| if &cc.votes == max_votes { Some(c) } else { None }).collect(); elected_candidate = choose_highest(state, opts, has_surplus)?; } else { elected_candidate = has_surplus[0].0; } distribute_surplus(state, &opts, elected_candidate); return Ok(true); } return Ok(false); } /// Return the denominator of the transfer value fn calculate_surplus_denom(surplus: &N, result: &NextPreferencesResult, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option where for<'r> &'r N: ops::Sub<&'r N, Output=N> { if transferable_only { let total_units = if weighted { &result.total_votes } else { &result.total_ballots }; let exhausted_units = if weighted { &result.exhausted.num_votes } else { &result.exhausted.num_ballots }; let transferable_units = total_units - exhausted_units; if transferable_votes > surplus { return Some(transferable_units); } else { return None; } } else { if weighted { return Some(result.total_votes.clone()); } else { return Some(result.total_ballots.clone()); } } } fn reweight_vote( num_votes: &N, num_ballots: &N, surplus: &N, weighted: bool, surplus_fraction: &Option, surplus_denom: &Option, round_tvs: Option, rounding: Option) -> N { let mut result; match surplus_denom { Some(v) => { if let Some(_) = round_tvs { // Rounding requested: use the rounded transfer value if weighted { result = num_votes.clone() * surplus_fraction.as_ref().unwrap(); } else { result = num_ballots.clone() * surplus_fraction.as_ref().unwrap(); } } else { // Avoid unnecessary rounding error by first multiplying by the surplus if weighted { result = num_votes.clone() * surplus / v; } else { result = num_ballots.clone() * surplus / v; } } } None => { result = num_votes.clone(); } } // Round down if requested if let Some(dps) = rounding { result.floor_mut(dps); } return result; } fn sum_surplus_transfers(entry: &NextPreferencesEntry, surplus: &N, is_weighted: bool, surplus_fraction: &Option, surplus_denom: &Option, _state: &mut CountState, opts: &STVOptions) -> N where for<'r> &'r N: ops::Div<&'r N, Output=N>, { match opts.sum_surplus_transfers { SumSurplusTransfersMode::SingleStep => { // Calculate transfer across all votes //state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals)); return reweight_vote(&entry.num_votes, &entry.num_ballots, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes); } SumSurplusTransfersMode::ByValue => { // Sum transfers by value let mut result = N::new(); // Sort into parcels by value let mut votes: Vec<&Vote> = entry.votes.iter().collect(); votes.sort_unstable_by(|a, b| (&a.value / &a.ballot.orig_value).cmp(&(&b.value / &b.ballot.orig_value))); for (_value, parcel) in &votes.into_iter().group_by(|v| &v.value / &v.ballot.orig_value) { let mut num_votes = N::new(); let mut num_ballots = N::new(); for vote in parcel { num_votes += &vote.value; num_ballots += &vote.ballot.orig_value; } //state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$}.", num_ballots, num_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); result += reweight_vote(&num_votes, &num_ballots, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes); } return result; } SumSurplusTransfersMode::PerBallot => { // Sum transfer per each individual ballot // TODO: This could be moved to distribute_surplus to avoid looping over the votes and calculating transfer values twice let mut result = N::new(); for vote in entry.votes.iter() { result += reweight_vote(&vote.value, &vote.ballot.orig_value, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes); } //state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals)); return result; } } } fn distribute_surplus(state: &mut CountState, opts: &STVOptions, elected_candidate: &Candidate) where for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Neg { state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name)); let count_card = state.candidates.get(elected_candidate).unwrap(); let surplus = &count_card.votes - state.quota.as_ref().unwrap(); let votes; match opts.surplus { SurplusMethod::WIG | SurplusMethod::UIG => { // Inclusive Gregory votes = state.candidates.get(elected_candidate).unwrap().parcels.concat(); } SurplusMethod::EG => { // Exclusive Gregory // Should be safe to unwrap() - or else how did we get a quota! votes = state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap(); } SurplusMethod::Meek => { todo!(); } } // Count next preferences let result = next_preferences(state, votes); state.kind = Some("Surplus of"); state.title = String::from(&elected_candidate.name); // Transfer candidate votes // TODO: Refactor?? let is_weighted = match opts.surplus { SurplusMethod::WIG => { true } SurplusMethod::UIG | SurplusMethod::EG => { false } SurplusMethod::Meek => { todo!() } }; let transferable_votes = &result.total_votes - &result.exhausted.num_votes; let surplus_denom = calculate_surplus_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only); let mut surplus_fraction; match surplus_denom { Some(ref v) => { surplus_fraction = Some(surplus.clone() / v); // Round down if requested if let Some(dps) = opts.round_tvs { surplus_fraction.as_mut().unwrap().floor_mut(dps); } if opts.transferable_only { state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } else { state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", result.total_ballots, result.total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } } None => { surplus_fraction = None; if opts.transferable_only { state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, at values received.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, dps=opts.pp_decimals)); } else { state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, at values received.", result.total_ballots, result.total_votes, dps=opts.pp_decimals)); } } } let mut checksum = N::new(); for (candidate, entry) in result.candidates.into_iter() { // Credit transferred votes let candidate_transfers = sum_surplus_transfers(&entry, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts); let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.transfer(&candidate_transfers); checksum += candidate_transfers; let mut parcel = entry.votes as Parcel; // Reweight votes for vote in parcel.iter_mut() { vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_tvs, opts.round_weights); } count_card.parcels.push(parcel); } // Credit exhausted votes let mut exhausted_transfers; if opts.transferable_only { if transferable_votes > surplus { // No ballots exhaust exhausted_transfers = N::new(); } else { exhausted_transfers = &surplus - &transferable_votes; if let Some(dps) = opts.round_votes { exhausted_transfers.floor_mut(dps); } } } else { exhausted_transfers = sum_surplus_transfers(&result.exhausted, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts); } state.exhausted.transfer(&exhausted_transfers); checksum += exhausted_transfers; // Transfer exhausted votes let parcel = result.exhausted.votes as Parcel; state.exhausted.parcels.push(parcel); // Finalise candidate votes let count_card = state.candidates.get_mut(elected_candidate).unwrap(); count_card.transfers = -&surplus; count_card.votes.assign(state.quota.as_ref().unwrap()); checksum -= surplus; // Update loss by fraction state.loss_fraction.transfer(&-checksum); } fn bulk_elect(state: &mut CountState, opts: &STVOptions) -> Result { if state.election.candidates.len() - state.num_excluded <= state.election.seats { state.kind = None; state.title = "Bulk election".to_string(); // Bulk elect all remaining candidates let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter() .filter(|c| state.candidates.get(c).unwrap().state == CandidateState::Hopeful) .collect(); 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(); count_card.state = CandidateState::Elected; state.num_elected += 1; count_card.order_elected = state.num_elected as isize; state.logger.log_smart( "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.", vec![&candidate.name] ); hopefuls.remove(hopefuls.iter().position(|c| *c == candidate).unwrap()); } return Ok(true); } return Ok(false); } 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; } fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result where 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 mut hopefuls: Vec<(&Candidate, &CountCard)> = state.election.candidates.iter() // Present in order in case of tie .map(|c| (c, state.candidates.get(c).unwrap())) .filter(|(_, cc)| cc.state == CandidateState::Hopeful) .collect(); // Sort by votes hopefuls.sort_by(|a, b| a.1.votes.cmp(&b.1.votes)); // 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(); excluded_candidates = vec![choose_lowest(state, opts, hopefuls)?]; } else { excluded_candidates = vec![&hopefuls[0].0]; } } let mut names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect(); names.sort(); 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 ); exclude_candidates(state, opts, excluded_candidates); return Ok(true); } fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool where 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.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 mut names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect(); names.sort(); state.kind = Some("Exclusion of"); state.title = names.join(", "); state.logger.log_smart( "Continuing exclusion of {}.", "Continuing exclusion of {}.", names ); exclude_candidates(state, opts, excluded_candidates); return true; } return false; } fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) where for<'r> &'r N: ops::Div<&'r N, Output=N>, { // Used to give bulk excluded candidate the same order_elected let order_excluded = state.num_excluded + 1; for excluded_candidate in excluded_candidates.iter() { let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); // Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??! if count_card.state != CandidateState::Excluded { count_card.state = CandidateState::Excluded; state.num_excluded += 1; count_card.order_elected = -(order_excluded as isize); } } // Determine votes to transfer in this stage let mut votes = Vec::new(); let mut votes_remain; let mut checksum = N::new(); match opts.exclusion { ExclusionMethod::SingleStage => { // Exclude in one round for excluded_candidate in excluded_candidates.iter() { let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); votes.append(&mut count_card.parcels.concat()); count_card.parcels.clear(); // Update votes let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value); checksum -= &votes_transferred; count_card.transfer(&-votes_transferred); } votes_remain = false; } ExclusionMethod::ByValue => { // Exclude by value let max_value = excluded_candidates.iter() .map(|c| state.candidates.get(c).unwrap().parcels.iter() .map(|p| p.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap()) .max().unwrap()) .max().unwrap(); votes_remain = false; for excluded_candidate in excluded_candidates.iter() { let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); // Filter out just those votes with max_value let mut remaining_votes = Vec::new(); let cand_votes = count_card.parcels.concat(); let mut votes_transferred = N::new(); for vote in cand_votes.into_iter() { if &vote.value / &vote.ballot.orig_value == max_value { votes_transferred += &vote.value; votes.push(vote); } else { remaining_votes.push(vote); } } if !remaining_votes.is_empty() { votes_remain = true; } // Leave remaining votes with candidate (as one parcel) count_card.parcels = vec![remaining_votes]; // Update votes checksum -= &votes_transferred; count_card.transfer(&-votes_transferred); } } ExclusionMethod::ParcelsByOrder => { // Exclude by parcel by order if excluded_candidates.len() > 1 { panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude"); } let count_card = state.candidates.get_mut(excluded_candidates[0]).unwrap(); votes = count_card.parcels.remove(0); votes_remain = !count_card.parcels.is_empty(); // Update votes let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value); checksum -= &votes_transferred; count_card.transfer(&-votes_transferred); } } if !votes.is_empty() { let value = &votes[0].value / &votes[0].ballot.orig_value; // Count next preferences let result = next_preferences(state, votes); if let ExclusionMethod::SingleStage = opts.exclusion { state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", result.total_ballots, result.total_votes, dps=opts.pp_decimals)); } else { state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$}.", result.total_ballots, result.total_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2))); } // Transfer candidate votes for (candidate, entry) in result.candidates.into_iter() { let parcel = entry.votes as Parcel; let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); // Round transfers let mut candidate_transfers = entry.num_votes; if let Some(dps) = opts.round_votes { candidate_transfers.floor_mut(dps); } count_card.transfer(&candidate_transfers); checksum += candidate_transfers; } // Transfer exhausted votes let parcel = result.exhausted.votes as Parcel; state.exhausted.parcels.push(parcel); let mut exhausted_transfers = result.exhausted.num_votes; if let Some(dps) = opts.round_votes { exhausted_transfers.floor_mut(dps); } state.exhausted.transfer(&exhausted_transfers); checksum += exhausted_transfers; } if !votes_remain { // Finalise candidate votes for excluded_candidate in excluded_candidates.into_iter() { let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); checksum -= &count_card.votes; count_card.transfers -= &count_card.votes; count_card.votes = N::new(); } if let ExclusionMethod::SingleStage = opts.exclusion { } else { state.logger.log_literal("Exclusion complete.".to_string()); } } // Update loss by fraction state.loss_fraction.transfer(&-checksum); } fn finished_before_stage(state: &CountState) -> bool { if state.num_elected >= state.election.seats { return true; } return false; } fn choose_highest<'c, N: Number>(state: &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"); } fn choose_lowest<'c, N: Number>(state: &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); } } } } return Err(STVError::UnresolvedTie); } 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); } } 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.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; } } } } }