/* 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 wasm_bindgen::prelude::wasm_bindgen; use std::collections::HashMap; use std::ops; #[wasm_bindgen] pub struct STVOptions { pub round_tvs: Option, pub round_weights: Option, pub round_votes: Option, pub round_quota: Option, pub surplus: SurplusMethod, pub surplus_order: SurplusOrder, pub transferable_only: bool, pub exclusion: ExclusionMethod, pub pp_decimals: usize, } #[wasm_bindgen] impl STVOptions { pub fn new( round_tvs: Option, round_weights: Option, round_votes: Option, round_quota: Option, surplus: &str, surplus_order: &str, transferable_only: bool, exclusion: &str, pp_decimals: usize, ) -> Self { return STVOptions { round_tvs, round_weights, round_votes, round_quota, 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"), }, pp_decimals, }; } } #[wasm_bindgen] #[derive(Clone, Copy)] pub enum SurplusMethod { WIG, UIG, EG, Meek, } #[wasm_bindgen] #[derive(Clone, Copy)] pub enum SurplusOrder { BySize, ByOrder, } #[wasm_bindgen] #[derive(Clone, Copy)] pub enum ExclusionMethod { SingleStage, ByValue, ParcelsByOrder, } 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); } pub fn count_one_stage(mut state: &mut CountState<'_, N>, opts: &STVOptions) -> bool 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 true; } // Continue exclusions if continue_exclusion(&mut state, &opts) { elect_meeting_quota(&mut state); return false; } // Distribute surpluses if distribute_surpluses(&mut state, &opts) { elect_meeting_quota(&mut state); return false; } // Attempt bulk election if bulk_elect(&mut state) { elect_meeting_quota(&mut state); return false; } // Exclude lowest hopeful if exclude_hopefuls(&mut state, &opts) { elect_meeting_quota(&mut state); return false; } todo!(); } 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 calculate_quota(state: &mut CountState, opts: &STVOptions) { let mut log = String::new(); // Calculate the total vote state.quota = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); log.push_str(format!("{:.dps$} usable votes, so the quota is ", state.quota, dps=opts.pp_decimals).as_str()); // TODO: Different quotas state.quota /= N::from(state.election.seats + 1); // Increment to next available increment if let Some(dps) = opts.round_quota { let mut factor = N::from(10); factor.pow_assign(dps as i32); state.quota *= &factor; state.quota.floor_mut(0); state.quota += N::one(); state.quota /= factor; } log.push_str(format!("{:.dps$}.", state.quota, dps=opts.pp_decimals).as_str()); state.logger.log_literal(log); } fn meets_quota(quota: &N, count_card: &CountCard) -> bool { // TODO: Different quota rules return count_card.votes >= *quota; } fn elect_meeting_quota(state: &mut CountState) { let quota = &state.quota; // Have to do this or else the borrow checker gets confused let mut cands_meeting_quota: Vec<(&&Candidate, &mut CountCard)> = state.candidates.iter_mut() .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL && meets_quota(quota, cc)) .collect(); if cands_meeting_quota.len() > 0 { // Sort by votes cands_meeting_quota.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); // Declare elected in descending order of votes for (candidate, count_card) in cands_meeting_quota.into_iter().rev() { 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] ); } } } fn distribute_surpluses(state: &mut CountState, opts: &STVOptions) -> bool where for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Neg { let mut has_surplus: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() .filter(|(_, cc)| cc.votes > state.quota) .collect(); if has_surplus.len() > 0 { match opts.surplus_order { SurplusOrder::BySize => { // Compare b with a to sort high-low has_surplus.sort_unstable_by(|a, b| b.1.votes.partial_cmp(&a.1.votes).unwrap()); } SurplusOrder::ByOrder => { has_surplus.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap()); } } // Distribute top candidate's surplus // TODO: Handle ties let elected_candidate = has_surplus.first_mut().unwrap().0; distribute_surplus(state, &opts, elected_candidate); return true; } return false; } /// Return the denominator of the transfer value fn calculate_transfer_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, transfer_value: &Option, transfer_denom: &Option, round_tvs: Option, rounding: Option) -> N { let mut result; match transfer_denom { Some(v) => { if let Some(_) = round_tvs { // Rounding requested: use the rounded transfer value if weighted { result = num_votes.clone() * transfer_value.as_ref().unwrap(); } else { result = num_ballots.clone() * transfer_value.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 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::Neg { let count_card = state.candidates.get(elected_candidate).unwrap(); let surplus = &count_card.votes - &state.quota; 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 transfer_denom = calculate_transfer_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only); let mut transfer_value; match transfer_denom { Some(ref v) => { transfer_value = Some(surplus.clone() / v); // Round down if requested if let Some(dps) = opts.round_tvs { transfer_value.as_mut().unwrap().floor_mut(dps); } state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps$}.", elected_candidate.name, transfer_value.as_ref().unwrap(), dps=opts.pp_decimals)); } None => { transfer_value = None; state.logger.log_literal(format!("Surplus of {} distributed at values received.", elected_candidate.name)); } } let mut checksum = N::new(); for (candidate, entry) in result.candidates.into_iter() { 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, &transfer_value, &transfer_denom, opts.round_tvs, opts.round_weights); } let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); let candidate_transfers = reweight_vote(&entry.num_votes, &entry.num_ballots, &surplus, is_weighted, &transfer_value, &transfer_denom, opts.round_tvs, opts.round_votes); 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; if opts.transferable_only { if transferable_votes > surplus { // No ballots exhaust exhausted_transfers = N::new(); } else { exhausted_transfers = &surplus - &transferable_votes; } } else { exhausted_transfers = reweight_vote(&result.exhausted.num_votes, &result.exhausted.num_ballots, &surplus, is_weighted, &transfer_value, &transfer_denom, opts.round_tvs, opts.round_votes); } if let Some(dps) = opts.round_votes { exhausted_transfers.floor_mut(dps); } state.exhausted.transfer(&exhausted_transfers); checksum += exhausted_transfers; // Finalise candidate votes let count_card = state.candidates.get_mut(elected_candidate).unwrap(); count_card.transfers = -&surplus; count_card.votes.assign(&state.quota); checksum -= surplus; // Update loss by fraction state.loss_fraction.transfer(&-checksum); } fn bulk_elect(state: &mut CountState) -> bool { 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, &mut CountCard)> = state.candidates.iter_mut() .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) .collect(); // TODO: Handle ties hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); for (candidate, count_card) in hopefuls.into_iter() { 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] ); } return true; } return false; } fn exclude_hopefuls(state: &mut CountState, opts: &STVOptions) -> bool where for<'r> &'r N: ops::Div<&'r N, Output=N>, { let mut hopefuls: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) .collect(); // Sort by votes // TODO: Handle ties hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); // Exclude lowest ranked candidate let excluded_candidate = hopefuls.first().unwrap().0; state.kind = Some("Exclusion of"); state.title = String::from(&excluded_candidate.name); state.logger.log_smart( "No surpluses to distribute, so {} is excluded.", "No surpluses to distribute, so {} are excluded.", vec![&excluded_candidate.name] ); exclude_candidate(state, opts, excluded_candidate); return true; } fn continue_exclusion(state: &mut CountState, 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.votes.is_zero()) .filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && cc.parcels.iter().any(|p| p.len() > 0)) .collect(); if excluded_with_votes.len() > 0 { excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap()); let excluded_candidate = excluded_with_votes.first().unwrap().0; state.kind = Some("Exclusion of"); state.title = String::from(&excluded_candidate.name); state.logger.log_smart( "Continuing exclusion of {}.", "Continuing exclusion of {}.", vec![&excluded_candidate.name] ); exclude_candidate(state, opts, excluded_candidate); return true; } return false; } fn exclude_candidate(state: &mut CountState, opts: &STVOptions, excluded_candidate: &Candidate) where for<'r> &'r N: ops::Div<&'r N, Output=N>, { 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 = -(state.num_excluded as isize); } // Determine votes to transfer in this stage let mut votes; let votes_remain; match opts.exclusion { ExclusionMethod::SingleStage => { // Exclude in one round votes = count_card.parcels.concat(); votes_remain = false; } ExclusionMethod::ByValue => { // Exclude by value let all_votes = count_card.parcels.concat(); // TODO: Write a multiple min/max function let min_value = all_votes.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap(); votes = Vec::new(); let mut remaining_votes = Vec::new(); // This could be implemented using Vec.drain_filter, but that is experimental currently for vote in all_votes.into_iter() { if &vote.value / &vote.ballot.orig_value == min_value { votes.push(vote); } else { remaining_votes.push(vote); } } votes_remain = remaining_votes.len() > 0; // Leave remaining votes with candidate (as one parcel) count_card.parcels = vec![remaining_votes]; } ExclusionMethod::ParcelsByOrder => { // Exclude by parcel by order votes = count_card.parcels.remove(0); votes_remain = count_card.parcels.len() > 0; } } let mut checksum = N::new(); if votes.len() > 0 { 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 {:.dps$}.", result.total_ballots, result.total_votes, value, dps=opts.pp_decimals)); } // 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 { // Subtract from candidate tally let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); checksum -= &result.total_votes; count_card.transfer(&-result.total_votes); // By definition, there is no loss by fraction } } if !votes_remain { // Finalise candidate votes let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); checksum -= &count_card.votes; count_card.transfers = -count_card.votes.clone(); count_card.votes = N::new(); // Update loss by fraction state.loss_fraction.transfer(&-checksum); if let ExclusionMethod::SingleStage = opts.exclusion { } else { state.logger.log_literal("Exclusion complete.".to_string()); } } } fn finished_before_stage(state: &CountState) -> bool { if state.num_elected >= state.election.seats { return true; } return false; }