2021-05-30 18:28:39 +10:00
|
|
|
/* 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 <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
use crate::election::{Candidate, CandidateState, CountCard, CountState, CountStateOrRef, Election, StageResult};
|
2021-05-30 23:00:28 +10:00
|
|
|
use crate::numbers::{NativeFloat64, Number, Rational};
|
2021-05-30 18:28:39 +10:00
|
|
|
use crate::stv;
|
|
|
|
|
|
|
|
extern crate console_error_panic_hook;
|
|
|
|
|
|
|
|
use wasm_bindgen::prelude::wasm_bindgen;
|
|
|
|
|
|
|
|
// Logging
|
|
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
extern "C" {
|
|
|
|
#[wasm_bindgen(js_namespace = console)]
|
|
|
|
fn log(s: &str);
|
|
|
|
}
|
|
|
|
macro_rules! cprintln {
|
|
|
|
($($t:tt)*) => (
|
|
|
|
#[allow(unused_unsafe)]
|
|
|
|
unsafe { log(&format_args!($($t)*).to_string()) }
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-05-30 23:00:28 +10:00
|
|
|
// Helper macros for making functions
|
|
|
|
|
|
|
|
macro_rules! impl_type {
|
|
|
|
($type:ident) => { paste::item! {
|
|
|
|
// Exported functions
|
|
|
|
|
|
|
|
#[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> = Election::from_blt(text.lines().map(|s| s.to_string()).into_iter());
|
|
|
|
return [<Election$type>](election);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &STVOptions) {
|
|
|
|
stv::count_init(&mut state.0, &opts.0);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &STVOptions) -> bool {
|
|
|
|
return stv::count_one_stage(&mut state.0, &opts.0);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reporting
|
|
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
pub fn [<make_and_print_result_$type>](stage_num: usize, state: &[<CountState$type>]) {
|
|
|
|
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$type>](CountState<'static, $type>);
|
|
|
|
#[wasm_bindgen]
|
|
|
|
impl [<CountState$type>] {
|
|
|
|
pub fn new(election: &[<Election$type>]) -> Self {
|
|
|
|
return [<CountState$type>](CountState::new(election.as_static()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
pub struct [<Election$type>](Election<$type>);
|
|
|
|
#[wasm_bindgen]
|
|
|
|
impl [<Election$type>] {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}}
|
2021-05-30 18:28:39 +10:00
|
|
|
}
|
|
|
|
|
2021-05-30 23:00:28 +10:00
|
|
|
impl_type!(Rational);
|
|
|
|
impl_type!(NativeFloat64);
|
2021-05-30 18:28:39 +10:00
|
|
|
|
|
|
|
// Reporting
|
|
|
|
|
|
|
|
fn print_candidates<'a, N: 'a + Number, I: Iterator<Item=(&'a Candidate, &'a CountCard<'a, N>)>>(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<N: Number>(stage_num: usize, result: &StageResult<N>) {
|
|
|
|
// 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!("");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
pub struct STVOptions(stv::STVOptions<'static>);
|
|
|
|
#[wasm_bindgen]
|
|
|
|
impl STVOptions {
|
|
|
|
pub fn new(round_votes: Option<usize>, exclusion: String) -> Self {
|
|
|
|
if exclusion == "one_round" {
|
|
|
|
return STVOptions(stv::STVOptions {
|
|
|
|
round_votes: round_votes,
|
|
|
|
exclusion: &"one_round",
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
panic!("Unknown --exclusion");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|