From 815055d6e6c0404712921dae0d7d5462a6e78581 Mon Sep 17 00:00:00 2001
From: RunasSudo
Date: Fri, 26 Aug 2022 02:27:25 +1000
Subject: [PATCH] Initial implementation of HTML output on CLI
---
src/cli/stv.rs | 144 +++++++++++++++-
src/stv/html.rs | 449 ++++++++++++++++++++++++++++++++++++++++++++++++
src/stv/mod.rs | 2 +
src/stv/wasm.rs | 420 ++------------------------------------------
4 files changed, 606 insertions(+), 409 deletions(-)
create mode 100644 src/stv/html.rs
diff --git a/src/cli/stv.rs b/src/cli/stv.rs
index 8b10157..a306224 100644
--- a/src/cli/stv.rs
+++ b/src/cli/stv.rs
@@ -178,7 +178,7 @@ pub struct SubcmdOptions {
// ---------------------
// -- Output settings --
- #[clap(help_heading=Some("OUTPUT"), short, long, possible_values=&["text", "csv"], default_value="text")]
+ #[clap(help_heading=Some("OUTPUT"), short, long, possible_values=&["text", "csv", "html"], default_value="text")]
output: String,
/// Hide excluded candidates from results report
@@ -327,6 +327,7 @@ where
match cmd_opts.output.as_str() {
"text" => { return count_election_text(election, &cmd_opts.filename, opts); }
"csv" => { return count_election_csv(election, opts); }
+ "html" => { return count_election_html(election, &cmd_opts.filename, opts); }
_ => unreachable!()
}
}
@@ -343,6 +344,7 @@ where
for<'r> &'r N: ops::Neg
"#);
+ }
+
+ return result;
+}
+
+/// Generate the first column of the HTML results table
+pub fn init_results_table(election: &Election, opts: &stv::STVOptions, report_style: &str) -> Vec {
+ let mut result = Vec::new();
+
+ result.push(String::from(r#" |
"#));
+ result.push(String::from(r#"
"#));
+ result.push(String::from(r#""#));
+
+ if report_style == "ballots_votes" {
+ result.push(String::from(r#" |
"#));
+ }
+
+ for candidate in election.candidates.iter() {
+ if candidate.is_dummy {
+ continue;
+ }
+
+ if report_style == "votes_transposed" {
+ result.push(format!(r#"{} |
"#, candidate.name));
+ } else {
+ result.push(format!(r#"{} |
"#, candidate.name));
+ result.push(String::from(r#"
"#));
+ }
+ }
+
+ if report_style == "votes_transposed" {
+ result.push(String::from(r#"Exhausted |
"#));
+ } else {
+ result.push(String::from(r#"Exhausted |
"#));
+ result.push(String::from(r#"
"#));
+ }
+
+ if report_style == "votes_transposed" {
+ result.push(String::from(r#"Loss by fraction |
"#));
+ result.push(String::from(r#"Total |
"#));
+ result.push(String::from(r#"Quota |
"#));
+ } else {
+ result.push(String::from(r#"Loss by fraction |
"#));
+ result.push(String::from(r#"
"#));
+ result.push(String::from(r#"Total |
"#));
+ result.push(String::from(r#"Quota |
"#));
+ }
+
+ if stv::should_show_vre(opts) {
+ result.push(String::from(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) -> Vec {
+ let mut result = Vec::new();
+
+ // Insert borders to left of new exclusions if reset-and-reiterate method applied
+ let classes_o; // Outer version
+ let classes_i; // Inner version
+ if (opts.exclusion == stv::ExclusionMethod::ResetAndReiterate && 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::ResetAndReiterate && 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));
+ result.push(format!(r#"{} | "#, classes_o, kind_str));
+ result.push(format!(r#"{} | "#, classes_o, title_str));
+ }
+ "votes_transposed" => {
+ if hide_xfers_trsp {
+ result.push(format!(r##"{1} | "##, classes_o, stage_num));
+ result.push(format!(r#"{} | "#, classes_o, kind_str));
+ result.push(format!(r#"{} | "#, classes_o, title_str));
+ } else {
+ result.push(format!(r##"{1} | "##, classes_o, stage_num));
+ result.push(format!(r#"{} | "#, classes_o, kind_str));
+ result.push(format!(r#"{} | "#, classes_o, title_str));
+ //result.push(format!(r#"X'fers | Total | "#, tdclasses1));
+ }
+ }
+ "ballots_votes" => {
+ result.push(format!(r##"{1} | "##, classes_o, stage_num));
+ result.push(format!(r#"{} | "#, classes_o, kind_str));
+ result.push(format!(r#"{} | "#, classes_o, title_str));
+ result.push(format!(r#"Ballots | Votes | "#, classes_o));
+ }
+ _ => 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)));
+ result.push(format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
+ }
+ CandidateState::Elected => {
+ result.push(format!(r#"{} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
+ result.push(format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
+ }
+ CandidateState::Doomed => {
+ result.push(format!(r#"{} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
+ result.push(format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
+ }
+ CandidateState::Withdrawn => {
+ result.push(format!(r#" | "#, classes_i));
+ result.push(format!(r#"WD | "#, classes_i));
+ }
+ CandidateState::Excluded => {
+ result.push(format!(r#"{} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
+ if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
+ result.push(format!(r#"Ex | "#, classes_i));
+ } else {
+ result.push(format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
+ }
+ }
+ }
+ }
+ "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)));
+ } else {
+ result.push(format!(r#"{} | {} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)));
+ }
+ }
+ CandidateState::Elected => {
+ if hide_xfers_trsp {
+ result.push(format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
+ } else {
+ result.push(format!(r#"{} | {} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)));
+ }
+ }
+ CandidateState::Doomed => {
+ if hide_xfers_trsp {
+ result.push(format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
+ } else {
+ result.push(format!(r#"{} | {} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)));
+ }
+ }
+ CandidateState::Withdrawn => {
+ if hide_xfers_trsp {
+ result.push(format!(r#"WD | "#, classes_i));
+ } else {
+ result.push(format!(r#" | WD | "#, classes_i));
+ }
+ }
+ 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));
+ } else {
+ result.push(format!(r#"{} | Ex | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
+ }
+ } else {
+ if hide_xfers_trsp {
+ result.push(format!(r#"{} | "#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
+ } else {
+ result.push(format!(r#"{} | {} | "#, classes_i, pps(&count_card.transfers, opts.pp_decimals), pp(&count_card.votes, opts.pp_decimals)));
+ }
+ }
+ }
+ }
+ }
+ "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)));
+ result.push(format!(r#"{} | {} | "#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)));
+ }
+ CandidateState::Elected => {
+ result.push(format!(r#"{} | {} | "#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)));
+ result.push(format!(r#"{} | {} | "#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)));
+ }
+ CandidateState::Doomed => {
+ result.push(format!(r#"{} | {} | "#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)));
+ result.push(format!(r#"{} | {} | "#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)));
+ }
+ CandidateState::Withdrawn => {
+ result.push(format!(r#" | | "#, classes_i));
+ result.push(format!(r#" | WD | "#, classes_i));
+ }
+ CandidateState::Excluded => {
+ result.push(format!(r#"{} | {} | "#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)));
+ if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
+ result.push(format!(r#" | Ex | "#, classes_i));
+ } else {
+ result.push(format!(r#"{} | {} | "#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)));
+ }
+ }
+ }
+ }
+ _ => unreachable!("Invalid report_style")
+ }
+ }
+
+ match report_style {
+ "votes" => {
+ result.push(format!(r#"{} | "#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals)));
+ result.push(format!(r#"{} | "#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)));
+ result.push(format!(r#"{} | "#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)));
+ result.push(format!(r#"{} | "#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)));
+ }
+ "votes_transposed" => {
+ if hide_xfers_trsp {
+ result.push(format!(r#"{} | "#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)));
+ result.push(format!(r#"{} | "#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)));
+ } else {
+ result.push(format!(r#"{} | {} | "#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals), pp(&state.exhausted.votes, opts.pp_decimals)));
+ result.push(format!(r#"{} | {} | "#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals), pp(&state.loss_fraction.votes, opts.pp_decimals)));
+ }
+ }
+ "ballots_votes" => {
+ result.push(format!(r#"{} | {} | "#, classes_i, pps(&state.exhausted.ballot_transfers, 0), pps(&state.exhausted.transfers, opts.pp_decimals)));
+ result.push(format!(r#"{} | {} | "#, classes_i, pp(&state.exhausted.num_ballots(), 0), pp(&state.exhausted.votes, opts.pp_decimals)));
+ result.push(format!(r#" | {} | "#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)));
+ result.push(format!(r#" | {} | "#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)));
+ }
+ _ => 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)));
+ }
+ "votes_transposed" => {
+ if hide_xfers_trsp {
+ result.push(format!(r#"{} | "#, classes_i, pp(&total_vote, opts.pp_decimals)));
+ } else {
+ result.push(format!(r#" | {} | "#, classes_i, pp(&total_vote, opts.pp_decimals)));
+ }
+ }
+ "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)));
+ }
+ _ => 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)));
+ } else {
+ result.push(format!(r#" | {} | "#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)));
+ }
+
+ 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)));
+ } else {
+ result.push(format!(r#" | {} | "#, classes_i, pp(vre, opts.pp_decimals)));
+ }
+ } else {
+ if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
+ result.push(format!(r#" | "#, classes_i));
+ } else {
+ result.push(format!(r#" | | "#, classes_i));
+ }
+ }
+ }
+
+ return result;
+}
+
+/// Generate the final column of the HTML results table
+pub fn finalise_results_table(state: &CountState, report_style: &str) -> Vec {
+ let mut result = Vec::new();
+
+ // Header rows
+ match report_style {
+ "votes" | "votes_transposed" => {
+ result.push(String::from(r#" | "#));
+ result.push(String::from(""));
+ result.push(String::from(""));
+ }
+ "ballots_votes" => {
+ result.push(String::from(r#" | "#));
+ result.push(String::from(""));
+ result.push(String::from(""));
+ result.push(String::from(""));
+ }
+ _ => 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));
+ } else if count_card.state == stv::CandidateState::Excluded {
+ result.push(format!(r#"Excluded {} | "#, rowspan, -count_card.order_elected));
+ } else if count_card.state == stv::CandidateState::Withdrawn {
+ result.push(format!(r#"Withdrawn | "#, rowspan));
+ } else {
+ result.push(format!(r#" | "#, rowspan));
+ }
+
+ if report_style != "votes_transposed" {
+ result.push(String::from(""));
+ }
+ }
+
+ 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;
+}
diff --git a/src/stv/mod.rs b/src/stv/mod.rs
index a9629e0..ab8f89c 100644
--- a/src/stv/mod.rs
+++ b/src/stv/mod.rs
@@ -19,6 +19,8 @@
pub mod gregory;
/// Meek method of surplus distributions, etc.
pub mod meek;
+/// Helper functions for HTML reporting output
+pub mod html;
/// Random sample methods of surplus distributions
pub mod sample;
diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs
index 652a419..0de5285 100644
--- a/src/stv/wasm.rs
+++ b/src/stv/wasm.rs
@@ -19,7 +19,7 @@
#![allow(unused_unsafe)] // Confuses cargo check
use crate::constraints::{self, Constraints};
-use crate::election::{CandidateState, CountState, Election, StageKind};
+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;
@@ -28,9 +28,8 @@ use crate::ties;
extern crate console_error_panic_hook;
-use itertools::Itertools;
use js_sys::Array;
-use wasm_bindgen::prelude::wasm_bindgen;
+use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
use std::cmp::max;
@@ -148,7 +147,7 @@ macro_rules! impl_type {
#[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);
+ return stv::html::describe_count(&filename, &election.0, &opts.0);
}
/// Wrapper for [update_results_table]
@@ -325,333 +324,17 @@ impl STVOptions {
// 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;
+ 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 {
- let result = Array::new();
-
- // Insert borders to left of new exclusions if reset-and-reiterate method applied
- let classes_o; // Outer version
- let classes_i; // Inner version
- if (opts.exclusion == stv::ExclusionMethod::ResetAndReiterate && 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::ResetAndReiterate && 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;
+ 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
@@ -665,49 +348,10 @@ pub fn update_stage_comments(state: &CountState, stage_num: usize)
/// 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;
+ 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
@@ -733,43 +377,3 @@ pub fn final_result_summary(state: &CountState, opts: &stv::STVOpt
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;
-}