OpenTally/src/stv/mod.rs

1091 lines
35 KiB
Rust

/* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#![allow(mutable_borrow_reservation_conflict)]
/// 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::numbers::Number;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Vote};
use crate::sharandom::SHARandom;
use crate::ties::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<usize>,
/// Round ballot weights to specified decimal places
pub round_weights: Option<usize>,
/// Round votes to specified decimal places
pub round_votes: Option<usize>,
/// Round quota to specified decimal places
pub round_quota: Option<usize>,
/// How to calculate votes to credit to candidates in surplus transfers
pub sum_surplus_transfers: SumSurplusTransfersMode,
/// Convert ballots with value >1 to multiple ballots of value 1
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<TieStrategy>,
/// 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,
/// Use bulk exclusion
pub bulk_exclude: bool,
/// Defer surplus distributions if possible
pub defer_surpluses: bool,
/// 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<usize>,
round_weights: Option<usize>,
round_votes: Option<usize>,
round_quota: Option<usize>,
sum_surplus_transfers: &str,
normalise_ballots: bool,
quota: &str,
quota_criterion: &str,
quota_mode: &str,
ties: &Vec<String>,
random_seed: &Option<String>,
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(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,
"parcels_by_order" => ExclusionMethod::ParcelsByOrder,
_ => panic!("Invalid --exclusion"),
},
bulk_exclude,
defer_surpluses,
pp_decimals,
};
}
/// Converts the [STVOptions] into CLI argument representation
pub fn describe<N: Number>(&self) -> String {
let mut flags = Vec::new();
let n_str = N::describe_opt(); if !n_str.is_empty() { flags.push(N::describe_opt()) };
if 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)); }
for t in self.ties.iter() { if let TieStrategy::Random(seed) = t { flags.push(format!("--random-seed {}", seed)); } }
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(" ");
}
}
/// 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,
}
impl QuotaMode {
/// Convert to CLI argument representation
fn describe(self) -> String {
match self {
QuotaMode::Static => "--quota-mode static",
QuotaMode::ERS97 => "--quota-mode ers97",
}.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 parcel by parcel in the order received
ParcelsByOrder,
}
impl ExclusionMethod {
/// Convert to CLI argument representation
fn describe(self) -> String {
match self {
ExclusionMethod::SingleStage => "--exclusion single_stage",
ExclusionMethod::ByValue => "--exclusion by_value",
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
}.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,
}
/// Distribute first preferences, and initialise other states such as the random number generator and tie-breaking rules
pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a STVOptions)
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
{
// Initialise RNG
for t in opts.ties.iter() {
if let TieStrategy::Random(seed) = t {
state.random = Some(SHARandom::new(seed));
}
}
distribute_first_preferences(&mut state, opts);
calculate_quota(&mut state, opts);
elect_meeting_quota(&mut state, opts);
init_tiebreaks(&mut state, opts);
}
/// Perform a single stage of the STV count
pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
for<'r> &'r N: ops::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N>,
{
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");
}
/// 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<Vote<'a, N>>,
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<Vote<'a, N>>) -> NextPreferencesResult<'a, N> {
let mut result = NextPreferencesResult {
candidates: HashMap::new(),
exhausted: NextPreferencesEntry {
votes: Vec::new(),
num_ballots: N::new(),
num_votes: N::new(),
},
total_ballots: N::new(),
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;
}
/// Distribute first preference votes
fn distribute_first_preferences<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
{
match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
gregory::distribute_first_preferences(state);
}
SurplusMethod::Meek => {
meek::distribute_first_preferences(state);
}
}
}
/// Calculate the quota, given the total vote, according to [STVOptions::quota]
fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N {
match opts.quota {
QuotaType::Droop | QuotaType::DroopExact => {
total /= N::from(seats + 1);
}
QuotaType::Hare | QuotaType::HareExact => {
total /= N::from(seats);
}
}
if let Some(dps) = opts.round_quota {
match opts.quota {
QuotaType::Droop | QuotaType::Hare => {
// Increment to next available increment
let mut factor = N::from(10);
factor.pow_assign(dps as i32);
total *= &factor;
total.floor_mut(0);
total += N::one();
total /= factor;
}
QuotaType::DroopExact | QuotaType::HareExact => {
// Round up to next available increment if necessary
total.ceil_mut(dps);
}
}
}
return total;
}
/// Calculate the quota according to [STVOptions::quota]
fn calculate_quota<N: Number>(state: &mut CountState<N>, 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 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 &quota < 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 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<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
match opts.quota_criterion {
QuotaCriterion::GreaterOrEqual => {
return count_card.votes >= *quota;
}
QuotaCriterion::Greater => {
return count_card.votes > *quota;
}
}
}
/// Declare elected all candidates meeting the quota
fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, 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);
}
}
}
/// Determine whether the transfer of all surpluses can be deferred
///
/// The value of [STVOptions::defer_surpluses] is not taken into account and must be handled by the caller
fn can_defer_surpluses<N: Number>(state: &CountState<N>, opts: &STVOptions, total_surpluses: &N) -> bool
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>
{
// Do not defer if this could change the last 2 candidates
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded)
.collect();
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;
}
/// Distribute surpluses according to [STVOptions::surplus]
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
for<'r> &'r N: ops::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N>,
{
match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
return gregory::distribute_surpluses(state, opts);
}
SurplusMethod::Meek => {
return meek::distribute_surpluses(state, opts);
}
}
}
/// Declare all continuing candidates elected, if the number equals the number of remaining vacancies
fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> {
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);
}
/// Determine which continuing candidates could be excluded in a bulk exclusion
///
/// The value of [STVOptions::bulk_exclude] is not taken into account and must be handled by the caller
fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &STVOptions) -> Vec<&'a Candidate> {
let mut excluded_candidates = Vec::new();
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::Hopeful)
.collect();
// Sort by votes
// NB: Unnecessary to handle ties, as ties will be rejected at "Do not exclude if this could change the order of exclusion"
hopefuls.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
let total_surpluses = state.candidates.iter()
.filter(|(_, cc)| &cc.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<bool, STVError>
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
let mut 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<N>)> = state.election.candidates.iter() // Present in order in case of tie
.map(|c| (c, state.candidates.get(c).unwrap()))
.filter(|(_, cc)| cc.state == CandidateState::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);
}
/// Continue the exclusion of a candidate who is being excluded
fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
// Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers
let mut excluded_with_votes: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.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;
}
/// 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>)
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.surplus {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
gregory::exclude_candidates(state, opts, excluded_candidates);
}
SurplusMethod::Meek => {
meek::exclude_candidates(state, opts, excluded_candidates);
}
}
}
/// Determine if the count is complete because the number of elected candidates equals the number of vacancies
fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
if state.num_elected >= state.election.seats {
return true;
}
return false;
}
/// Break a tie between the given candidates according to [STVOptions::ties], selecting the highest candidate
///
/// The given candidates are assumed to be tied in this round
fn choose_highest<'c, N: Number>(state: &mut CountState<N>, 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<N>, 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<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
if !opts.ties.iter().any(|t| t == &TieStrategy::Forwards) && !opts.ties.iter().any(|t| t == &TieStrategy::Backwards) {
return;
}
// Sort candidates in this stage by votes, grouping by ties
let mut sorted_candidates: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter().collect();
sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
let sorted_candidates: Vec<Vec<(&&Candidate, &CountCard<N>)>> = sorted_candidates.into_iter()
.group_by(|(_, cc)| &cc.votes)
.into_iter()
.map(|(_, candidates)| candidates.collect())
.collect();
// Update forwards tie-breaking order
if opts.ties.iter().any(|t| t == &TieStrategy::Forwards) {
let mut hm: HashMap<&Candidate, usize> = HashMap::new();
for (i, group) in sorted_candidates.iter().enumerate() {
for (candidate, _) in group.iter() {
hm.insert(candidate, i);
}
}
state.forwards_tiebreak = Some(hm);
}
// Update backwards tie-breaking order
if opts.ties.iter().any(|t| t == &TieStrategy::Backwards) {
let mut hm: HashMap<&Candidate, usize> = HashMap::new();
for (i, group) in sorted_candidates.iter().enumerate() {
for (candidate, _) in group.iter() {
hm.insert(candidate, i);
}
}
state.backwards_tiebreak = Some(hm);
}
}
/// If required, update the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties]
fn update_tiebreaks<N: Number>(state: &mut CountState<N>, _opts: &STVOptions) {
if let None = state.forwards_tiebreak {
if let None = state.backwards_tiebreak {
return;
}
}
// Sort candidates in this stage by votes, grouping by ties
let mut sorted_candidates: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter().collect();
sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
let sorted_candidates: Vec<Vec<&Candidate>> = sorted_candidates.into_iter()
.group_by(|(_, cc)| &cc.votes)
.into_iter()
.map(|(_, candidates)| candidates.map(|(c, _)| *c).collect())
.collect();
// Update forwards tie-breaking order
if let Some(hm) = state.forwards_tiebreak.as_mut() {
// TODO: Check if already completely sorted
let mut sorted_last_round: Vec<(&&Candidate, &usize)> = hm.iter().collect();
sorted_last_round.sort_unstable_by(|a, b| a.1.cmp(b.1));
let sorted_last_round: Vec<Vec<&Candidate>> = sorted_last_round.into_iter()
.group_by(|(_, v)| **v)
.into_iter()
.map(|(_, group)| group.map(|(c, _)| *c).collect())
.collect();
let mut i: usize = 0;
for mut group in sorted_last_round.into_iter() {
if group.len() == 1 {
hm.insert(group[0], i);
i += 1;
continue;
} else {
// Tied in last round - refer to this round
group.sort_unstable_by(|a, b|
sorted_candidates.iter().position(|x| x.contains(a)).unwrap()
.cmp(&sorted_candidates.iter().position(|x| x.contains(b)).unwrap())
);
let tied_last_round = group.into_iter()
.group_by(|c| sorted_candidates.iter().position(|x| x.contains(c)).unwrap());
for (_, group2) in tied_last_round.into_iter() {
for candidate in group2 {
hm.insert(candidate, i);
}
i += 1;
}
}
}
}
// Update backwards tie-breaking order
if let Some(hm) = state.backwards_tiebreak.as_mut() {
let hm_orig = hm.clone();
let mut i: usize = 0;
for group in sorted_candidates.iter() {
if group.len() == 1 {
hm.insert(group[0], i);
i += 1;
continue;
} else {
// Tied in this round - refer to last round
let mut tied_this_round: Vec<&Candidate> = group.into_iter().map(|c| *c).collect();
tied_this_round.sort_unstable_by(|a, b| hm_orig.get(a).unwrap().cmp(hm_orig.get(b).unwrap()));
let tied_this_round = tied_this_round.into_iter()
.group_by(|c| hm_orig.get(c).unwrap());
for (_, group2) in tied_this_round.into_iter() {
for candidate in group2 {
hm.insert(candidate, i);
}
i += 1;
}
}
}
}
}