/* 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 . */ use crate::election::{CandidateState, CountState, Election}; use crate::numbers::{Fixed, NativeFloat64, Number, Rational}; use crate::stv; extern crate console_error_panic_hook; use js_sys::Array; use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; // Init #[wasm_bindgen] pub fn fixed_set_dps(dps: usize) { Fixed::set_dps(dps); } // Helper macros for making functions macro_rules! impl_type { ($type:ident) => { paste::item! { // Counting #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](text: String) -> [] { // Install panic! hook console_error_panic_hook::set_once(); let election: Election<$type> = Election::from_blt(text.lines().map(|s| s.to_string()).into_iter()); return [](election); } #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](election: &mut []) { election.0.normalise_ballots(); } #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &mut [], opts: &STVOptions) { stv::count_init(&mut state.0, &opts.0); } #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &mut [], opts: &STVOptions) -> Result { match stv::count_one_stage(&mut state.0, &opts.0) { Ok(v) => Ok(v), Err(stv::STVError::RequireInput) => Err("RequireInput".into()), Err(stv::STVError::UnresolvedTie) => Err("UnresolvedTie".into()), } } // Reporting #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](election: &[], opts: &STVOptions) -> String { return init_results_table(&election.0, &opts.0); } #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](filename: String, election: &[], opts: &STVOptions) -> String { return describe_count(filename, &election.0, &opts.0); } #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](stage_num: usize, state: &[], opts: &STVOptions) -> Array { return update_results_table(stage_num, &state.0, &opts.0); } #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &[]) -> String { return update_stage_comments(&state.0); } #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &[]) -> Array { return finalise_results_table(&state.0); } #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &[]) -> String { return final_result_summary(&state.0); } // Wrapper structs // Required as we cannot specify &'static in wasm-bindgen: issue #1187 #[wasm_bindgen] pub struct [](CountState<'static, $type>); #[wasm_bindgen] impl [] { pub fn new(election: &[]) -> Self { return [](CountState::new(election.as_static())); } } #[wasm_bindgen] pub struct [](Election<$type>); #[wasm_bindgen] impl [] { pub fn seats(&self) -> usize { self.0.seats } fn as_static(&self) -> &'static Election<$type> { // Need to have this as we cannot specify &'static in wasm-bindgen: issue #1187 unsafe { let ptr = &self.0 as *const Election<$type>; &*ptr } } } }} } impl_type!(Fixed); impl_type!(NativeFloat64); impl_type!(Rational); #[wasm_bindgen] pub struct STVOptions(stv::STVOptions<'static>); #[wasm_bindgen] impl STVOptions { 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: Array, surplus: &str, surplus_order: &str, transferable_only: bool, exclusion: &str, bulk_exclude: bool, defer_surpluses: bool, pp_decimals: usize, ) -> Self { Self(stv::STVOptions::new( round_tvs, round_weights, round_votes, round_quota, sum_surplus_transfers, normalise_ballots, quota, quota_criterion, quota_mode, &ties.iter().map(|v| v.as_string().unwrap()).collect(), surplus, surplus_order, transferable_only, exclusion, bulk_exclude, defer_surpluses, pp_decimals, )) } } // Reporting fn init_results_table(election: &Election, opts: &stv::STVOptions) -> String { let mut result = String::from(r#""#); for candidate in election.candidates.iter() { result.push_str(&format!(r#"{}"#, candidate.name)); } result.push_str(r#"ExhaustedLoss by fractionTotalQuota"#); if opts.quota_mode == stv::QuotaMode::ERS97 { result.push_str(r#"Vote required for election"#); } return result; } fn describe_count(filename: String, election: &Election, opts: &stv::STVOptions) -> String { let mut result = String::from("

Count computed by OpenTally (revision "); result.push_str(crate::VERSION); let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value }); result.push_str(&format!(r#"). Read {:.0} ballots from ‘{}’ for election ‘{}’. There are {} candidates for {} vacancies. "#, total_ballots, filename, election.name, election.candidates.len(), election.seats)); let opts_str = opts.describe::(); if opts_str.len() > 0 { result.push_str(&format!(r#"Counting using options {}.

"#, opts_str)) } else { result.push_str(r#"Counting using default options.

"#); } return result; } fn update_results_table(stage_num: usize, state: &CountState, opts: &stv::STVOptions) -> Array { let result = Array::new(); result.push(&format!(r#"{}"#, stage_num).into()); result.push(&format!(r#"{}"#, state.kind.unwrap_or("")).into()); result.push(&format!(r#"{}"#, state.title).into()); for candidate in state.election.candidates.iter() { let count_card = state.candidates.get(candidate).unwrap(); if count_card.state == stv::CandidateState::Elected { result.push(&format!(r#"{}"#, pp(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, pp(&count_card.votes, opts.pp_decimals)).into()); } else if count_card.state == stv::CandidateState::Excluded { result.push(&format!(r#"{}"#, pp(&count_card.transfers, opts.pp_decimals)).into()); if count_card.votes.is_zero() { result.push(&r#"Ex"#.into()); } else { result.push(&format!(r#"{}"#, pp(&count_card.votes, opts.pp_decimals)).into()); } } else if count_card.state == stv::CandidateState::Withdrawn { result.push(&r#""#.into()); result.push(&r#"WD"#.into()); } else { result.push(&format!(r#"{}"#, pp(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, pp(&count_card.votes, opts.pp_decimals)).into()); } } result.push(&format!(r#"{}"#, pp(&state.exhausted.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, pp(&state.exhausted.votes, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, pp(&state.loss_fraction.votes, opts.pp_decimals)).into()); // Calculate total votes let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); total_vote += &state.exhausted.votes; total_vote += &state.loss_fraction.votes; result.push(&format!(r#"{}"#, pp(&total_vote, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into()); if opts.quota_mode == stv::QuotaMode::ERS97 { result.push(&format!(r#"{}"#, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into()); } return result; } fn update_stage_comments(state: &CountState) -> String { return state.logger.render().join(" "); } fn finalise_results_table(state: &CountState) -> Array { let result = Array::new(); // Header rows result.push(&r#""#.into()); result.push(&"".into()); result.push(&"".into()); // Candidate states for candidate in state.election.candidates.iter() { let count_card = state.candidates.get(candidate).unwrap(); if count_card.state == stv::CandidateState::Elected { result.push(&format!(r#"ELECTED {}"#, count_card.order_elected).into()); } else if count_card.state == stv::CandidateState::Excluded { result.push(&format!(r#"Excluded {}"#, -count_card.order_elected).into()); } else if count_card.state == stv::CandidateState::Withdrawn { result.push(&r#"Withdrawn"#.into()); } else { result.push(&r#""#.into()); } result.push(&"".into()); } return result; } fn final_result_summary(state: &CountState) -> String { let mut result = String::from("

Count complete. The winning candidates are, in order of election:

    "); let mut winners = Vec::new(); for (candidate, count_card) in state.candidates.iter() { if count_card.state == CandidateState::Elected { winners.push((candidate, count_card.order_elected)); } } winners.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); for (winner, _) in winners.into_iter() { result.push_str(&format!("
  1. {}
  2. ", winner.name)); } result.push_str("
"); return result; } fn pp(n: &N, dps: usize) -> String { if n.is_zero() { return "".to_string(); } let mut raw = format!("{:.dps$}", n, dps=dps); if raw.contains('.') { raw = raw.replacen(".", ".", 1); raw.push_str(""); } if raw.starts_with('-') { raw = raw.replacen("-", "−", 1); } return raw; }