/* OpenTally: Open-source election vote counting
* Copyright © 2021–2022 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, StageKind};
//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 itertools::Itertools;
use js_sys::Array;
use wasm_bindgen::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 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_html](crate::stv::transfers::TransferTable::render_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 == "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 lead-in description of the count in HTML
pub 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::new(), |mut acc, b| { acc += &b.orig_value; acc });
result.push_str(&format!(r#"). Read {:.0} ballots from ‘{}’ for election ‘{}’. There are {} candidates for {} vacancies. "#, total_ballots, filename, election.name, election.candidates.iter().filter(|c| !c.is_dummy).count(), election.seats));
let opts_str = opts.describe::();
if !opts_str.is_empty() {
result.push_str(&format!(r#"Counting using options {}.
"#, opts_str))
} else {
result.push_str(r#"Counting using default options.
"#);
}
return result;
}
/// Generate the first column of the HTML results table
pub fn init_results_table(election: &Election, opts: &stv::STVOptions, report_style: &str) -> String {
let mut result = String::from(r#" |
"#);
if report_style == "ballots_votes" {
result.push_str(r#" |
"#);
}
for candidate in election.candidates.iter() {
if candidate.is_dummy {
continue;
}
if report_style == "votes_transposed" {
result.push_str(&format!(r#"{} |
"#, candidate.name));
} else {
result.push_str(&format!(r#"{} |
"#, candidate.name));
}
}
if report_style == "votes_transposed" {
result.push_str(r#"Exhausted |
"#);
} else {
result.push_str(r#"Exhausted |
"#);
}
if report_style == "votes_transposed" {
result.push_str(r#"Loss by fraction |
Total |
Quota |
"#);
} else {
result.push_str(r#"Loss by fraction |
|
Total |
Quota |
"#);
}
if stv::should_show_vre(opts) {
result.push_str(r#"Vote required for election |
"#);
}
return result;
}
/// 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 {
let result = Array::new();
// Insert borders to left of new exclusions in Wright STV
let classes_o; // Outer version
let classes_i; // Inner version
if (opts.exclusion == stv::ExclusionMethod::Wright && matches!(state.title, StageKind::ExclusionOf(_))) || matches!(state.title, StageKind::Rollback) {
classes_o = r#" class="blw""#;
classes_i = r#"blw "#;
} else {
classes_o = "";
classes_i = "";
}
// Hide transfers column for first preferences if transposed
let hide_xfers_trsp;
if let StageKind::FirstPreferences = state.title {
hide_xfers_trsp = true;
} else if opts.exclusion == stv::ExclusionMethod::Wright && matches!(state.title, StageKind::ExclusionOf(_)) {
hide_xfers_trsp = true;
} else if let StageKind::Rollback = state.title {
hide_xfers_trsp = true;
} else if let StageKind::BulkElection = state.title {
hide_xfers_trsp = true;
} else if state.candidates.values().all(|cc| cc.transfers.is_zero()) && state.exhausted.transfers.is_zero() && state.loss_fraction.transfers.is_zero() {
hide_xfers_trsp = true;
} else {
hide_xfers_trsp = false;
}
// Header rows
let kind_str = state.title.kind_as_string();
let title_str;
match &state.title {
StageKind::FirstPreferences | StageKind::Rollback | StageKind::RollbackExhausted | StageKind::SurplusesDistributed | StageKind::BulkElection => {
title_str = format!("{}", state.title);
}
StageKind::SurplusOf(candidate) => {
title_str = candidate.name.clone();
}
StageKind::ExclusionOf(candidates) => {
if candidates.len() > 5 {
let first_4_cands = candidates.iter().map(|c| &c.name).sorted().take(4).join(",
");
title_str = format!("{},
and {} others", first_4_cands, candidates.len() - 4);
} else {
title_str = candidates.iter().map(|c| &c.name).join(",
");
}
}
StageKind::BallotsOf(candidate) => {
title_str = candidate.name.clone();
}
};
match report_style {
"votes" => {
result.push(&format!(r##"{1} | "##, classes_o, stage_num).into());
result.push(&format!(r#"{} | "#, classes_o, kind_str).into());
result.push(&format!(r#"{} | "#, classes_o, title_str).into());
}
"votes_transposed" => {
if hide_xfers_trsp {
result.push(&format!(r##"{1} | "##, classes_o, stage_num).into());
result.push(&format!(r#"{} | "#, classes_o, kind_str).into());
result.push(&format!(r#"{} | "#, classes_o, title_str).into());
} else {
result.push(&format!(r##"{1} | "##, classes_o, stage_num).into());
result.push(&format!(r#"{} | "#, classes_o, kind_str).into());
result.push(&format!(r#"{} | "#, classes_o, title_str).into());
//result.push(&format!(r#"X'fers | Total | "#, tdclasses1).into());
}
}
"ballots_votes" => {
result.push(&format!(r##"{1} | "##, classes_o, stage_num).into());
result.push(&format!(r#"{} | "#, classes_o, kind_str).into());
result.push(&format!(r#"{} | "#, classes_o, title_str).into());
result.push(&format!(r#"Ballots | Votes | "#, classes_o).into());
}
_ => unreachable!("Invalid report_style")
}
for candidate in state.election.candidates.iter() {
if candidate.is_dummy {
continue;
}
let count_card = &state.candidates[candidate];
// TODO: REFACTOR THIS!!
match report_style {
"votes" => {
match count_card.state {
CandidateState::Hopeful | CandidateState::Guarded => {
result.push(&format!(r#"{} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
}
CandidateState::Elected => {
result.push(&format!(r#"{} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
}
CandidateState::Doomed => {
result.push(&format!(r#"{} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
}
CandidateState::Withdrawn => {
result.push(&format!(r#" | "#, classes_i).into());
result.push(&format!(r#"WD | "#, classes_i).into());
}
CandidateState::Excluded => {
result.push(&format!(r#"{} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
result.push(&format!(r#"Ex | "#, classes_i).into());
} else {
result.push(&format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
}
}
}
}
"votes_transposed" => {
match count_card.state {
CandidateState::Hopeful | CandidateState::Guarded => {
if hide_xfers_trsp {
result.push(&format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
} else {
result.push(&format!(r#"{} | {} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into());
}
}
CandidateState::Elected => {
if hide_xfers_trsp {
result.push(&format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
} else {
result.push(&format!(r#"{} | {} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into());
}
}
CandidateState::Doomed => {
if hide_xfers_trsp {
result.push(&format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
} else {
result.push(&format!(r#"{} | {} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into());
}
}
CandidateState::Withdrawn => {
if hide_xfers_trsp {
result.push(&format!(r#"WD | "#, classes_i).into());
} else {
result.push(&format!(r#" | WD | "#, classes_i).into());
}
}
CandidateState::Excluded => {
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
if hide_xfers_trsp {
result.push(&format!(r#"Ex | "#, classes_i).into());
} else {
result.push(&format!(r#"{} | Ex | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
}
} else {
if hide_xfers_trsp {
result.push(&format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
} else {
result.push(&format!(r#"{} | {} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)).into());
}
}
}
}
}
"ballots_votes" => {
match count_card.state {
CandidateState::Hopeful | CandidateState::Guarded => {
result.push(&format!(r#"{} | {} | "#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"{} | {} | "#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
}
CandidateState::Elected => {
result.push(&format!(r#"{} | {} | "#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"{} | {} | "#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
}
CandidateState::Doomed => {
result.push(&format!(r#"{} | {} | "#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"{} | {} | "#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
}
CandidateState::Withdrawn => {
result.push(&format!(r#" | | "#, classes_i).into());
result.push(&format!(r#" | WD | "#, classes_i).into());
}
CandidateState::Excluded => {
result.push(&format!(r#"{} | {} | "#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into());
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
result.push(&format!(r#" | Ex | "#, classes_i).into());
} else {
result.push(&format!(r#"{} | {} | "#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
}
}
}
}
_ => unreachable!("Invalid report_style")
}
}
match report_style {
"votes" => {
result.push(&format!(r#"{} | "#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"{} | "#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)).into());
result.push(&format!(r#"{} | "#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"{} | "#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
}
"votes_transposed" => {
if hide_xfers_trsp {
result.push(&format!(r#"{} | "#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)).into());
result.push(&format!(r#"{} | "#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
} else {
result.push(&format!(r#"{} | {} | "#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals), pp(&state.exhausted.votes, opts.pp_decimals)).into());
result.push(&format!(r#"{} | {} | "#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals), pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
}
}
"ballots_votes" => {
result.push(&format!(r#"{} | {} | "#, classes_i, pps(&state.exhausted.ballot_transfers, 0), pps(&state.exhausted.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"{} | {} | "#, classes_i, pp(&state.exhausted.num_ballots(), 0), pp(&state.exhausted.votes, opts.pp_decimals)).into());
result.push(&format!(r#" | {} | "#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)).into());
result.push(&format!(r#" | {} | "#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
}
_ => unreachable!("Invalid report_style")
}
// Calculate total votes
let mut total_vote = state.candidates.iter().filter_map(|(c, cc)| if c.is_dummy { None } else { Some(cc) }).fold(N::new(), |mut acc, cc| { acc += &cc.votes; acc });
total_vote += &state.exhausted.votes;
total_vote += &state.loss_fraction.votes;
match report_style {
"votes" => {
result.push(&format!(r#"{} | "#, classes_i, pp(&total_vote, opts.pp_decimals)).into());
}
"votes_transposed" => {
if hide_xfers_trsp {
result.push(&format!(r#"{} | "#, classes_i, pp(&total_vote, opts.pp_decimals)).into());
} else {
result.push(&format!(r#" | {} | "#, classes_i, pp(&total_vote, opts.pp_decimals)).into());
}
}
"ballots_votes" => {
// Calculate total ballots
let mut total_ballots = state.candidates.values().fold(N::new(), |mut acc, cc| { acc += cc.num_ballots(); acc });
total_ballots += state.exhausted.num_ballots();
result.push(&format!(r#"{} | {} | "#, classes_i, pp(&total_ballots, 0), pp(&total_vote, opts.pp_decimals)).into());
}
_ => unreachable!("Invalid report_style")
}
if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
result.push(&format!(r#"{} | "#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
} else {
result.push(&format!(r#" | {} | "#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
}
if stv::should_show_vre(opts) {
if let Some(vre) = &state.vote_required_election {
if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
result.push(&format!(r#"{} | "#, classes_i, pp(vre, opts.pp_decimals)).into());
} else {
result.push(&format!(r#" | {} | "#, classes_i, pp(vre, opts.pp_decimals)).into());
}
} else {
if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
result.push(&format!(r#" | "#, classes_i).into());
} else {
result.push(&format!(r#" | | "#, classes_i).into());
}
}
}
return result;
}
/// 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 {
let result = Array::new();
// Header rows
match report_style {
"votes" | "votes_transposed" => {
result.push(&r#" | "#.into());
result.push(&"".into());
result.push(&"".into());
}
"ballots_votes" => {
result.push(&r#" | "#.into());
result.push(&"".into());
result.push(&"".into());
result.push(&"".into());
}
_ => unreachable!("Invalid report_style")
}
let rowspan = if report_style == "votes_transposed" { "" } else { r#" rowspan="2""# };
// Candidate states
for candidate in state.election.candidates.iter() {
if candidate.is_dummy {
continue;
}
let count_card = &state.candidates[candidate];
if count_card.state == stv::CandidateState::Elected {
result.push(&format!(r#"ELECTED {} | "#, rowspan, count_card.order_elected).into());
} else if count_card.state == stv::CandidateState::Excluded {
result.push(&format!(r#"Excluded {} | "#, rowspan, -count_card.order_elected).into());
} else if count_card.state == stv::CandidateState::Withdrawn {
result.push(&format!(r#"Withdrawn | "#, rowspan).into());
} else {
result.push(&format!(r#" | "#, rowspan).into());
}
if report_style != "votes_transposed" {
result.push(&"".into());
}
}
return result;
}
/// 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!("- {} (kv = {:.dps2$})
", winner.name, kv, dps2=max(opts.pp_decimals, 2)));
} else {
result.push_str(&format!("- {}
", winner.name));
}
}
result.push_str("
");
return result;
}
/// HTML pretty-print the number to the specified decimal places
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;
}
/// Signed version of [pp]
fn pps(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);
} else {
raw.insert(0, '+');
}
return raw;
}