/* 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::{Candidate, CandidateState, CountCard, CountState, CountStateOrRef, Election, StageResult}; use crate::numbers::{NativeFloat64, Number, Rational}; use crate::stv; extern crate console_error_panic_hook; use js_sys::Array; use wasm_bindgen::prelude::wasm_bindgen; // Logging #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] fn log(s: &str); } /// println! to console macro_rules! cprintln { ($($t:tt)*) => ( #[allow(unused_unsafe)] unsafe { log(&format_args!($($t)*).to_string()) } ) } // Helper macros for making functions macro_rules! impl_type { ($type:ident) => { paste::item! { // Exported functions #[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 [](state: &mut [], opts: &stv::STVOptions) { stv::count_init(&mut state.0, &opts); } #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](state: &mut [], opts: &stv::STVOptions) -> bool { return stv::count_one_stage(&mut state.0, &opts); } // Reporting #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](election: &[]) -> String { return init_results_table(&election.0); } #[wasm_bindgen] #[allow(non_snake_case)] pub fn [](stage_num: usize, state: &[], opts: &stv::STVOptions) -> Array { return update_results_table(stage_num, &state.0, opts); } /*#[wasm_bindgen] #[allow(non_snake_case)] pub fn [](stage_num: usize, state: &[]) { let result = StageResult { kind: state.0.kind, title: &state.0.title, logs: state.0.logger.render(), state: CountStateOrRef::from(&state.0), }; print_stage(stage_num, &result); }*/ // 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!(Rational); impl_type!(NativeFloat64); // Reporting fn init_results_table(election: &Election) -> 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"#); 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#"{:.dps$}"#, count_card.transfers, dps=opts.pp_decimals).into()); result.push(&format!(r#"{:.dps$}"#, count_card.votes, dps=opts.pp_decimals).into()); } else { result.push(&format!(r#"{:.dps$}"#, count_card.transfers, dps=opts.pp_decimals).into()); result.push(&format!(r#"{:.dps$}"#, count_card.votes, dps=opts.pp_decimals).into()); } } result.push(&format!(r#"{:.dps$}"#, state.exhausted.transfers, dps=opts.pp_decimals).into()); result.push(&format!(r#"{:.dps$}"#, state.exhausted.votes, dps=opts.pp_decimals).into()); result.push(&format!(r#"{:.dps$}"#, state.loss_fraction.transfers, dps=opts.pp_decimals).into()); result.push(&format!(r#"{:.dps$}"#, state.loss_fraction.votes, dps=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#"{:.dps$}"#, total_vote, dps=opts.pp_decimals).into()); result.push(&format!(r#"{:.dps$}"#, state.quota, dps=opts.pp_decimals).into()); return result; } /*fn print_candidates<'a, N: 'a + Number, I: Iterator)>>(candidates: I) { for (candidate, count_card) in candidates { if count_card.state == CandidateState::ELECTED { cprintln!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=2); } else if count_card.state == CandidateState::EXCLUDED { cprintln!("- {}: {:.dps$} ({:.dps$}) - Excluded {}", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=2); } else { cprintln!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=2); } } } fn print_stage(stage_num: usize, result: &StageResult) { // Print stage details match result.kind { None => { cprintln!("{}. {}", stage_num, result.title); } Some(kind) => { cprintln!("{}. {} {}", stage_num, kind, result.title); } }; cprintln!("{}", result.logs.join(" ")); let state = result.state.as_ref(); // Print candidates let candidates = state.election.candidates.iter() .map(|c| (c, state.candidates.get(c).unwrap())); print_candidates(candidates); // Print summary rows cprintln!("Exhausted: {:.dps$} ({:.dps$})", state.exhausted.votes, state.exhausted.transfers, dps=2); cprintln!("Loss by fraction: {:.dps$} ({:.dps$})", state.loss_fraction.votes, state.loss_fraction.transfers, dps=2); 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; cprintln!("Total votes: {:.dps$}", total_vote, dps=2); cprintln!("Quota: {:.dps$}", state.quota, dps=2); cprintln!(""); }*/