OpenTally/src/stv/wasm.rs

380 lines
12 KiB
Rust
Raw Normal View History

2021-05-30 18:28:39 +10:00
/* OpenTally: Open-source election vote counting
* Copyright © 20212023 Lee Yingtong Li (RunasSudo)
2021-05-30 18:28:39 +10:00
*
* 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/>.
*/
2021-06-14 20:43:36 +10:00
#![allow(rustdoc::private_intra_doc_links)]
2021-07-31 15:24:23 +10:00
#![allow(unused_unsafe)] // Confuses cargo check
2021-06-14 20:43:36 +10:00
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;
2021-05-30 18:28:39 +10:00
use crate::stv;
use crate::ties;
2021-05-30 18:28:39 +10:00
extern crate console_error_panic_hook;
2021-06-02 22:46:36 +10:00
use js_sys::Array;
use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
2021-05-30 18:28:39 +10:00
2021-06-16 13:00:54 +10:00
use std::cmp::max;
2021-07-31 15:24:23 +10:00
// 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);
} }
}
2021-06-04 22:05:48 +10:00
// Init
// Wrapper for [DynNum::set_kind]
//#[wasm_bindgen]
//pub fn dynnum_set_kind(kind: NumKind) {
// DynNum::set_kind(kind);
//}
2021-06-14 20:43:36 +10:00
/// Wrapper for [Fixed::set_dps]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
2021-06-04 22:05:48 +10:00
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! {
2021-06-04 22:05:48 +10:00
// Counting
/// Wrapper for [blt::parse_iterator]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[allow(non_snake_case)]
pub fn [<election_from_blt_$type>](text: String) -> [<Election$type>] {
// Install panic! hook
console_error_panic_hook::set_once();
let election: Election<$type> = match blt::parse_iterator(text.chars().peekable()) {
2021-07-31 15:24:23 +10:00
Ok(e) => e,
Err(err) => wasm_error!("Syntax Error", err),
};
return [<Election$type>](election);
}
2021-09-04 22:46:29 +10:00
/// Call [Constraints::from_con] and set [Election::constraints]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
2021-09-03 23:53:15 +10:00
#[allow(non_snake_case)]
pub fn [<election_load_constraints_$type>](election: &mut [<Election$type>], 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);
}
2021-09-03 23:53:15 +10:00
}
2021-09-04 22:46:29 +10:00
/// Wrapper for [stv::preprocess_election]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
2021-06-27 22:09:34 +10:00
#[allow(non_snake_case)]
2021-09-04 22:46:29 +10:00
pub fn [<preprocess_election_$type>](election: &mut [<Election$type>], opts: &STVOptions) {
stv::preprocess_election(&mut election.0, &opts.0);
2021-06-27 22:09:34 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::count_init]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[allow(non_snake_case)]
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &STVOptions) {
match stv::count_init(&mut state.0, opts.as_static()) {
Ok(_) => (),
2021-07-31 15:24:23 +10:00
Err(err) => wasm_error!("Error", err),
}
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::count_one_stage]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[allow(non_snake_case)]
2021-07-31 15:24:23 +10:00
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &STVOptions) -> bool {
match stv::count_one_stage::<[<$type>]>(&mut state.0, &opts.0) {
2021-07-31 15:24:23 +10:00
Ok(v) => v,
Err(err) => wasm_error!("Error", err),
}
}
// Reporting
2021-06-14 20:43:36 +10:00
/// Wrapper for [init_results_table]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[allow(non_snake_case)]
2021-08-16 18:48:49 +10:00
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions, report_style: &str) -> String {
return init_results_table(&election.0, &opts.0, report_style);
2021-06-02 22:46:36 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [describe_count]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[allow(non_snake_case)]
pub fn [<describe_count_$type>](filename: String, election: &[<Election$type>], opts: &STVOptions) -> String {
return stv::html::describe_count(&filename, &election.0, &opts.0);
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [update_results_table]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
2021-06-02 22:46:36 +10:00
#[allow(non_snake_case)]
2021-08-16 18:48:49 +10:00
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions, report_style: &str) -> Array {
return update_results_table(stage_num, &state.0, &opts.0, report_style);
2021-06-02 22:46:36 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [update_stage_comments]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
2021-06-03 15:47:19 +10:00
#[allow(non_snake_case)]
2021-09-11 21:08:36 +10:00
pub fn [<update_stage_comments_$type>](state: &[<CountState$type>], stage_num: usize) -> String {
return update_stage_comments(&state.0, stage_num);
2021-06-03 15:47:19 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [finalise_results_table]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
2021-06-03 15:47:19 +10:00
#[allow(non_snake_case)]
2021-08-16 18:48:49 +10:00
pub fn [<finalise_results_table_$type>](state: &[<CountState$type>], report_style: &str) -> Array {
return finalise_results_table(&state.0, report_style);
2021-06-03 15:47:19 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [final_result_summary]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
2021-06-02 22:46:36 +10:00
#[allow(non_snake_case)]
2021-06-16 13:00:54 +10:00
pub fn [<final_result_summary_$type>](state: &[<CountState$type>], opts: &STVOptions) -> String {
return final_result_summary(&state.0, &opts.0);
2021-06-03 15:47:19 +10:00
}
// Wrapper structs
2021-06-14 20:43:36 +10:00
/// Wrapper for [CountState]
2021-06-16 17:20:29 +10:00
///
/// 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$type>](CountState<'static, $type>);
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl [<CountState$type>] {
2021-06-16 17:20:29 +10:00
/// Create a new [CountState] wrapper
pub fn new(election: &[<Election$type>]) -> Self {
return [<CountState$type>](CountState::new(election.as_static()));
}
2021-09-11 21:08:36 +10:00
/// Call [render_text](crate::stv::gregory::TransferTable::render_text) (as HTML) on [CountState::transfer_table]
2021-09-11 21:08:36 +10:00
pub fn transfer_table_render_html(&self, opts: &STVOptions) -> Option<String> {
2021-10-27 19:52:51 +11:00
return self.0.transfer_table.as_ref().map(|tt| tt.render_text(&opts.0));
2021-09-11 21:08:36 +10:00
}
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [Election]
2021-06-16 17:20:29 +10:00
///
/// 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>](Election<$type>);
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl [<Election$type>] {
2021-06-16 17:20:29 +10:00
/// Return [Election::seats]
pub fn seats(&self) -> usize { self.0.seats }
2021-06-16 17:20:29 +10:00
/// 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
}
}
}
}}
2021-05-30 18:28:39 +10:00
}
//impl_type!(DynNum);
2021-06-04 22:05:48 +10:00
impl_type!(Fixed);
impl_type!(GuardedFixed);
impl_type!(NativeFloat64);
2021-06-04 22:05:48 +10:00
impl_type!(Rational);
2021-05-30 18:28:39 +10:00
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::STVOptions]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
2021-06-13 03:15:15 +10:00
pub struct STVOptions(stv::STVOptions);
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl STVOptions {
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::STVOptions::new]
pub fn new(
round_surplus_fractions: Option<usize>,
round_values: Option<usize>,
round_votes: Option<usize>,
round_quota: Option<usize>,
round_subtransfers: &str,
meek_surplus_tolerance: String,
quota: &str,
quota_criterion: &str,
quota_mode: &str,
ties: Array,
2021-06-13 03:15:15 +10:00
random_seed: String,
surplus: &str,
surplus_order: &str,
papers: &str,
exclusion: &str,
meek_nz_exclusion: bool,
sample: &str,
sample_per_ballot: bool,
2021-06-23 00:52:25 +10:00
early_bulk_elect: bool,
bulk_exclude: bool,
defer_surpluses: bool,
2021-08-07 18:51:48 +10:00
immediate_elect: bool,
min_threshold: String,
constraints_path: Option<String>,
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,
2021-06-23 00:52:25 +10:00
early_bulk_elect,
bulk_exclude,
defer_surpluses,
2021-08-07 18:51:48 +10:00
immediate_elect,
min_threshold,
constraints_path,
constraint_mode.into(),
false,
false,
2021-09-11 18:42:15 +10:00
false,
pp_decimals,
))
}
2021-06-22 14:34:26 +10:00
/// Wrapper for [stv::STVOptions::validate]
pub fn validate(&self) {
2021-07-31 15:24:23 +10:00
match self.0.validate() {
Ok(_) => {}
Err(err) => { wasm_error!("Error", err) }
}
2021-06-22 14:34:26 +10:00
}
}
2021-06-13 03:15:15 +10:00
impl STVOptions {
2021-06-14 20:43:36 +10:00
/// Return the underlying [stv::STVOptions] as a `&'static stv::STVOptions`
///
/// # Safety
2021-06-16 17:20:29 +10:00
/// 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.
2021-06-14 20:43:36 +10:00
///
2021-06-13 03:15:15 +10:00
fn as_static(&self) -> &'static stv::STVOptions {
unsafe {
let ptr = &self.0 as *const stv::STVOptions;
&*ptr
}
}
}
2021-05-30 18:28:39 +10:00
// Reporting
2021-06-14 20:43:36 +10:00
/// Generate the first column of the HTML results table
pub fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions, report_style: &str) -> String {
return stv::html::init_results_table(election, opts, report_style).join("");
2021-06-14 20:43:36 +10:00
}
/// Generate subsequent columns of the HTML results table
pub fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, 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();
2021-06-02 22:46:36 +10:00
}
2021-06-14 20:43:36 +10:00
/// Get the comment for the current stage
pub fn update_stage_comments<N: Number>(state: &CountState<N>, stage_num: usize) -> String {
2021-09-11 21:08:36 +10:00
let mut comments = state.logger.render().join(" ");
2021-10-27 19:52:51 +11:00
if state.transfer_table.is_some() {
2021-09-11 21:08:36 +10:00
comments.push_str(&format!(r##" <a href="#" class="detailedTransfersLink" onclick="viewDetailedTransfers({});return false;">[View detailed transfers]</a>"##, stage_num));
}
return comments;
2021-06-03 15:47:19 +10:00
}
2021-06-14 20:43:36 +10:00
/// Generate the final column of the HTML results table
pub fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Array {
return stv::html::finalise_results_table(state, report_style)
.into_iter()
.map(|s| JsValue::from(s))
.collect();
2021-05-30 18:28:39 +10:00
}
2021-06-14 20:43:36 +10:00
/// Generate the final lead-out text summarising the result of the election
pub fn final_result_summary<N: Number>(state: &CountState<N>, opts: &stv::STVOptions) -> String {
2021-06-03 15:47:19 +10:00
let mut result = String::from("<p>Count complete. The winning candidates are, in order of election:</p><ol>");
2021-05-30 18:28:39 +10:00
2021-06-03 15:47:19 +10:00
let mut winners = Vec::new();
for (candidate, count_card) in state.candidates.iter() {
2021-06-12 00:50:01 +10:00
if count_card.state == CandidateState::Elected {
2021-06-16 13:00:54 +10:00
winners.push((candidate, count_card.order_elected, &count_card.keep_value));
2021-06-03 15:47:19 +10:00
}
}
2021-06-29 15:31:38 +10:00
winners.sort_unstable_by(|a, b| a.1.cmp(&b.1));
2021-05-30 18:28:39 +10:00
2021-06-16 13:00:54 +10:00
for (winner, _, kv_opt) in winners.into_iter() {
if let Some(kv) = kv_opt {
result.push_str(&format!("<li>{} (<i>kv</i> = {:.dps2$})</li>", winner.name, kv, dps2=max(opts.pp_decimals, 2)));
} else {
result.push_str(&format!("<li>{}</li>", winner.name));
}
2021-06-03 15:47:19 +10:00
}
2021-05-30 18:28:39 +10:00
2021-06-03 15:47:19 +10:00
result.push_str("</ol>");
return result;
}