/* OpenTally: Open-source election vote counting * Copyright © 2021–2023 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(rustdoc::private_intra_doc_links)] #![allow(unused_unsafe)] // Confuses cargo check use crate::constraints::{self, Constraints}; use crate::election::{CandidateState, CountState, Election}; //use crate::numbers::{DynNum, Fixed, GuardedFixed, NativeFloat64, Number, NumKind, Rational}; use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational}; use crate::parser::blt; use crate::stv; use crate::ties; extern crate console_error_panic_hook; use js_sys::Array; use wasm_bindgen::{JsValue, prelude::wasm_bindgen}; use std::cmp::max; // Error handling #[wasm_bindgen] extern "C" { fn wasm_error(message: String); } macro_rules! wasm_error { ($type:expr, $err:expr) => { { unsafe { wasm_error(format!("{}: {}", $type, $err)); } panic!("{}: {}", $type, $err); } } } // Init // Wrapper for [DynNum::set_kind] //#[wasm_bindgen] //pub fn dynnum_set_kind(kind: NumKind) { // DynNum::set_kind(kind); //} /// Wrapper for [Fixed::set_dps] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub fn fixed_set_dps(dps: usize) { Fixed::set_dps(dps); } /// Wrapper for [GuardedFixed::set_dps] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub fn gfixed_set_dps(dps: usize) { GuardedFixed::set_dps(dps); } // Helper macros for making functions macro_rules! impl_type { ($type:ident) => { paste::item! { // Counting /// Wrapper for [blt::parse_iterator] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[allow(non_snake_case)] pub fn [](text: String) -> [] { // Install panic! hook console_error_panic_hook::set_once(); let election: Election<$type> = match blt::parse_iterator(text.chars().peekable()) { Ok(e) => e, Err(err) => wasm_error!("Syntax Error", err), }; return [](election); } /// Call [Constraints::from_con] and set [Election::constraints] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[allow(non_snake_case)] pub fn [](election: &mut [], text: String, opts: &STVOptions) { election.0.constraints = match Constraints::from_con(text.lines()) { Ok(c) => Some(c), Err(err) => wasm_error!("Constraint Syntax Error", err), }; // Validate constraints if let Err(err) = election.0.constraints.as_ref().unwrap().validate_constraints(election.0.candidates.len(), opts.0.constraint_mode) { wasm_error!("Constraint Validation Error", err); } // Add dummy candidates if required if opts.0.constraint_mode == stv::ConstraintMode::RepeatCount { constraints::init_repeat_count(&mut election.0); } } /// Wrapper for [stv::preprocess_election] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[allow(non_snake_case)] pub fn [](election: &mut [], opts: &STVOptions) { stv::preprocess_election(&mut election.0, &opts.0); } /// Wrapper for [stv::count_init] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[allow(non_snake_case)] pub fn [](state: &mut [], opts: &STVOptions) { match stv::count_init(&mut state.0, opts.as_static()) { Ok(_) => (), Err(err) => wasm_error!("Error", err), } } /// Wrapper for [stv::count_one_stage] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[allow(non_snake_case)] pub fn [](state: &mut [], opts: &STVOptions) -> bool { match stv::count_one_stage::<[<$type>]>(&mut state.0, &opts.0) { Ok(v) => v, Err(err) => wasm_error!("Error", err), } } // Reporting /// Wrapper for [init_results_table] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[allow(non_snake_case)] pub fn [](election: &[], opts: &STVOptions, report_style: &str) -> String { return init_results_table(&election.0, &opts.0, report_style); } /// Wrapper for [describe_count] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[allow(non_snake_case)] pub fn [](filename: String, election: &[], opts: &STVOptions) -> String { return stv::html::describe_count(&filename, &election.0, &opts.0); } /// Wrapper for [update_results_table] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[allow(non_snake_case)] pub fn [](stage_num: usize, state: &[], opts: &STVOptions, report_style: &str) -> Array { return update_results_table(stage_num, &state.0, &opts.0, report_style); } /// Wrapper for [update_stage_comments] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[allow(non_snake_case)] pub fn [](state: &[], stage_num: usize) -> String { return update_stage_comments(&state.0, stage_num); } /// Wrapper for [finalise_results_table] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[allow(non_snake_case)] pub fn [](state: &[], report_style: &str) -> Array { return finalise_results_table(&state.0, report_style); } /// Wrapper for [final_result_summary] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[allow(non_snake_case)] pub fn [](state: &[], opts: &STVOptions) -> String { return final_result_summary(&state.0, &opts.0); } // Wrapper structs /// Wrapper for [CountState] /// /// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187). /// #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct [](CountState<'static, $type>); #[cfg_attr(feature = "wasm", wasm_bindgen)] impl [] { /// Create a new [CountState] wrapper pub fn new(election: &[]) -> Self { return [](CountState::new(election.as_static())); } /// Call [render_text](crate::stv::gregory::TransferTable::render_text) (as HTML) on [CountState::transfer_table] pub fn transfer_table_render_html(&self, opts: &STVOptions) -> Option { return self.0.transfer_table.as_ref().map(|tt| tt.render_text(&opts.0)); } } /// Wrapper for [Election] /// /// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187). /// #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct [](Election<$type>); #[cfg_attr(feature = "wasm", wasm_bindgen)] impl [] { /// Return [Election::seats] pub fn seats(&self) -> usize { self.0.seats } /// Return the underlying [Election] as a `&'static Election` /// /// # Safety /// This assumes that the underlying [Election] is valid for the `'static` lifetime, as it would be if the [Election] were created from Javascript. /// fn as_static(&self) -> &'static Election<$type> { unsafe { let ptr = &self.0 as *const Election<$type>; &*ptr } } } }} } //impl_type!(DynNum); impl_type!(Fixed); impl_type!(GuardedFixed); impl_type!(NativeFloat64); impl_type!(Rational); /// Wrapper for [stv::STVOptions] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct STVOptions(stv::STVOptions); #[cfg_attr(feature = "wasm", wasm_bindgen)] impl STVOptions { /// Wrapper for [stv::STVOptions::new] pub fn new( round_surplus_fractions: Option, round_values: Option, round_votes: Option, round_quota: Option, round_subtransfers: &str, meek_surplus_tolerance: String, quota: &str, quota_criterion: &str, quota_mode: &str, ties: Array, random_seed: String, surplus: &str, surplus_order: &str, papers: &str, exclusion: &str, meek_nz_exclusion: bool, sample: &str, sample_per_ballot: bool, early_bulk_elect: bool, bulk_exclude: bool, defer_surpluses: bool, immediate_elect: bool, min_threshold: String, constraints_path: Option, constraint_mode: &str, pp_decimals: usize, ) -> Self { Self(stv::STVOptions::new( round_surplus_fractions, round_values, round_votes, round_quota, round_subtransfers.into(), meek_surplus_tolerance, quota.into(), quota_criterion.into(), quota_mode.into(), ties::from_strs(ties.iter().map(|v| v.as_string().unwrap()).collect(), Some(random_seed)), surplus.into(), surplus_order.into(), if papers == "transferable" || papers == "subtract_nontransferable" { true } else { false }, if papers == "assume_progress_total" || papers == "subtract_nontransferable" { true } else { false }, exclusion.into(), meek_nz_exclusion, sample.into(), sample_per_ballot, early_bulk_elect, bulk_exclude, defer_surpluses, immediate_elect, min_threshold, constraints_path, constraint_mode.into(), false, false, false, pp_decimals, )) } /// Wrapper for [stv::STVOptions::validate] pub fn validate(&self) { match self.0.validate() { Ok(_) => {} Err(err) => { wasm_error!("Error", err) } } } } impl STVOptions { /// Return the underlying [stv::STVOptions] as a `&'static stv::STVOptions` /// /// # Safety /// This assumes that the underlying [stv::STVOptions] is valid for the `'static` lifetime, as it would be if the [stv::STVOptions] were created from Javascript. /// fn as_static(&self) -> &'static stv::STVOptions { unsafe { let ptr = &self.0 as *const stv::STVOptions; &*ptr } } } // Reporting /// Generate the first column of the HTML results table pub fn init_results_table(election: &Election, opts: &stv::STVOptions, report_style: &str) -> String { return stv::html::init_results_table(election, opts, report_style).join(""); } /// Generate subsequent columns of the HTML results table pub fn update_results_table(stage_num: usize, state: &CountState, opts: &stv::STVOptions, report_style: &str) -> Array { return stv::html::update_results_table(stage_num, state, opts, report_style) .into_iter() .map(|s| JsValue::from(s)) .collect(); } /// Get the comment for the current stage pub fn update_stage_comments(state: &CountState, stage_num: usize) -> String { let mut comments = state.logger.render().join(" "); if state.transfer_table.is_some() { comments.push_str(&format!(r##" [View detailed transfers]"##, stage_num)); } return comments; } /// Generate the final column of the HTML results table pub fn finalise_results_table(state: &CountState, report_style: &str) -> Array { return stv::html::finalise_results_table(state, report_style) .into_iter() .map(|s| JsValue::from(s)) .collect(); } /// Generate the final lead-out text summarising the result of the election pub fn final_result_summary(state: &CountState, opts: &stv::STVOptions) -> 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, &count_card.keep_value)); } } winners.sort_unstable_by(|a, b| a.1.cmp(&b.1)); for (winner, _, kv_opt) in winners.into_iter() { if let Some(kv) = kv_opt { result.push_str(&format!("
  1. {} (kv = {:.dps2$})
  2. ", winner.name, kv, dps2=max(opts.pp_decimals, 2))); } else { result.push_str(&format!("
  3. {}
  4. ", winner.name)); } } result.push_str("
"); return result; }