Initial implementation of HTML output on CLI
This commit is contained in:
parent
6bb127a124
commit
815055d6e6
144
src/cli/stv.rs
144
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<Output=N>
|
||||
{
|
||||
// Describe count
|
||||
// TODO: Can we precompute total_ballots?
|
||||
let total_ballots = election.ballots.iter().fold(N::new(), |mut acc, b| { acc += &b.orig_value; acc });
|
||||
print!("Count computed by OpenTally (revision {}). Read {:.0} ballots from \"{}\" for election \"{}\". There are {} candidates for {} vacancies. ", crate::VERSION, total_ballots, filename, election.name, election.candidates.iter().filter(|c| !c.is_dummy).count(), election.seats);
|
||||
let opts_str = opts.describe::<N>();
|
||||
|
@ -622,3 +624,143 @@ where
|
|||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// HTML report in the style of wasm UI
|
||||
|
||||
fn count_election_html<N: Number>(mut election: Election<N>, filename: &str, opts: STVOptions) -> Result<(), i32>
|
||||
where
|
||||
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
// HTML preamble, etc.
|
||||
print!(r#"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OpenTally Results</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" integrity="sha512-NhSC1YmyruXifcj/KFRWoC561YpHpc5Jtzgvbuzx5VozKpWvQ+4nXhPdFgmx8xqexRcpAglTj9sIBWINXa8x5w==" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" type="text/css" href="https://yingtongli.me/opentally/stv/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="resultsDiv">
|
||||
<div id="resultLogs1" style="white-space: pre-wrap;">"#);
|
||||
|
||||
// Describe count
|
||||
println!(r#"{}</div>"#, stv::html::describe_count(filename, &election, &opts));
|
||||
|
||||
stv::preprocess_election(&mut election, &opts);
|
||||
|
||||
// Initialise count state
|
||||
let mut state = CountState::new(&election);
|
||||
|
||||
// TODO: Enable report_style to be customised
|
||||
let mut result_rows = stv::html::init_results_table(&election, &opts, "votes_transposed");
|
||||
|
||||
let mut stage_comments = Vec::new();
|
||||
|
||||
// -----------
|
||||
// First stage
|
||||
|
||||
// Distribute first preferences
|
||||
match stv::count_init(&mut state, &opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err.describe());
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
|
||||
let stage_result = stv::html::update_results_table(1, &state, &opts, "votes_transposed");
|
||||
for (row, cell) in stage_result.into_iter().enumerate() {
|
||||
// 5 characters from end to insert before "</tr>"
|
||||
let idx = result_rows[row].len() - 5;
|
||||
result_rows[row].insert_str(idx, &cell);
|
||||
}
|
||||
|
||||
stage_comments.push(state.logger.render().join(" "));
|
||||
|
||||
// -----------------
|
||||
// Subsequent stages
|
||||
|
||||
let mut stage_num = 1;
|
||||
loop {
|
||||
match stv::count_one_stage(&mut state, &opts) {
|
||||
Ok(is_done) => {
|
||||
if is_done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err.describe());
|
||||
return Err(1);
|
||||
}
|
||||
}
|
||||
|
||||
stage_num += 1;
|
||||
|
||||
let stage_result = stv::html::update_results_table(stage_num, &state, &opts, "votes_transposed");
|
||||
for (row, cell) in stage_result.into_iter().enumerate() {
|
||||
// 5 characters from end to insert before "</tr>"
|
||||
let idx = result_rows[row].len() - 5;
|
||||
result_rows[row].insert_str(idx, &cell);
|
||||
}
|
||||
|
||||
stage_comments.push(state.logger.render().join(" "));
|
||||
}
|
||||
|
||||
// ----------------
|
||||
// Candidate states
|
||||
|
||||
for (row, cell) in stv::html::finalise_results_table(&state, "votes_transposed").into_iter().enumerate() {
|
||||
// 5 characters from end to insert before "</tr>"
|
||||
let idx = result_rows[row].len() - 5;
|
||||
result_rows[row].insert_str(idx, &cell);
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// Output table to HTML
|
||||
|
||||
println!(r#"<table id="result" class="result">"#);
|
||||
for row in result_rows {
|
||||
println!("{}", row);
|
||||
}
|
||||
println!("</table>");
|
||||
|
||||
// --------------------
|
||||
// Print stage comments
|
||||
|
||||
println!(r#"<div id="resultLogs2"><p>Stage comments:</p><ol>"#);
|
||||
for comment in stage_comments {
|
||||
println!("<li>{}</li>", comment);
|
||||
}
|
||||
println!("</ol>");
|
||||
|
||||
// -------------
|
||||
// Print summary
|
||||
|
||||
println!("<p>Count complete. The winning candidates are, in order of election:</p><ol>");
|
||||
|
||||
let mut winners = Vec::new();
|
||||
for (candidate, count_card) in state.candidates.iter() {
|
||||
if count_card.state == CandidateState::Elected {
|
||||
winners.push((candidate, count_card));
|
||||
}
|
||||
}
|
||||
winners.sort_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
|
||||
|
||||
for (_i, (winner, count_card)) in winners.into_iter().enumerate() {
|
||||
if let Some(kv) = &count_card.keep_value {
|
||||
println!("<li>{} (kv = {:.dps2$})</li>", winner.name, kv, dps2=max(opts.pp_decimals, 2));
|
||||
} else {
|
||||
println!("<li>{}</li>", winner.name);
|
||||
}
|
||||
}
|
||||
|
||||
println!("</ol></div></div></body></html>");
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,449 @@
|
|||
/* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::election::{CandidateState, Election, StageKind};
|
||||
use crate::numbers::Number;
|
||||
use crate::stv::{self, CountState};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
/// Generate the lead-in description of the count in HTML
|
||||
pub fn describe_count<N: Number>(filename: &str, election: &Election<N>, opts: &stv::STVOptions) -> String {
|
||||
let mut result = String::from("<p>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::<N>();
|
||||
if !opts_str.is_empty() {
|
||||
result.push_str(&format!(r#"Counting using options <span style="font-family: monospace;">{}</span>.</p>"#, opts_str))
|
||||
} else {
|
||||
result.push_str(r#"Counting using default options.</p>"#);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 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) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
result.push(String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr>"#));
|
||||
result.push(String::from(r#"<tr class="stage-kind"></tr>"#));
|
||||
result.push(String::from(r#"<tr class="stage-comment"></tr>"#));
|
||||
|
||||
if report_style == "ballots_votes" {
|
||||
result.push(String::from(r#"<tr class="hint-papers-votes"><td></td></tr>"#));
|
||||
}
|
||||
|
||||
for candidate in election.candidates.iter() {
|
||||
if candidate.is_dummy {
|
||||
continue;
|
||||
}
|
||||
|
||||
if report_style == "votes_transposed" {
|
||||
result.push(format!(r#"<tr class="candidate transfers"><td class="candidate-name">{}</td></tr>"#, candidate.name));
|
||||
} else {
|
||||
result.push(format!(r#"<tr class="candidate transfers"><td rowspan="2" class="candidate-name">{}</td></tr>"#, candidate.name));
|
||||
result.push(String::from(r#"<tr class="candidate votes"></tr>"#));
|
||||
}
|
||||
}
|
||||
|
||||
if report_style == "votes_transposed" {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Exhausted</td></tr>"#));
|
||||
} else {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td rowspan="2">Exhausted</td></tr>"#));
|
||||
result.push(String::from(r#"<tr class="info votes"></tr>"#));
|
||||
}
|
||||
|
||||
if report_style == "votes_transposed" {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Loss by fraction</td></tr>"#));
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Total</td></tr>"#));
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Quota</td></tr>"#));
|
||||
} else {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr>"#));
|
||||
result.push(String::from(r#"<tr class="info votes"></tr>"#));
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Total</td></tr>"#));
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Quota</td></tr>"#));
|
||||
}
|
||||
|
||||
if stv::should_show_vre(opts) {
|
||||
result.push(String::from(r#"<tr class="info transfers"><td>Vote required for election</td></tr>"#));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 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) -> Vec<String> {
|
||||
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(",<br>");
|
||||
title_str = format!("{},<br>and {} others", first_4_cands, candidates.len() - 4);
|
||||
} else {
|
||||
title_str = candidates.iter().map(|c| &c.name).join(",<br>");
|
||||
}
|
||||
}
|
||||
StageKind::BallotsOf(candidate) => {
|
||||
title_str = candidate.name.clone();
|
||||
}
|
||||
};
|
||||
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
|
||||
result.push(format!(r#"<td{}>{}</td>"#, classes_o, kind_str));
|
||||
result.push(format!(r#"<td{}>{}</td>"#, classes_o, title_str));
|
||||
}
|
||||
"votes_transposed" => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
|
||||
result.push(format!(r#"<td{}>{}</td>"#, classes_o, kind_str));
|
||||
result.push(format!(r#"<td{}>{}</td>"#, classes_o, title_str));
|
||||
} else {
|
||||
result.push(format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
|
||||
result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str));
|
||||
result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, title_str));
|
||||
//result.push(format!(r#"<td{}>X'fers</td><td>Total</td>"#, tdclasses1));
|
||||
}
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num));
|
||||
result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str));
|
||||
result.push(format!(r#"<td{} colspan="2">{}</td>"#, classes_o, title_str));
|
||||
result.push(format!(r#"<td{}>Ballots</td><td>Votes</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
result.push(format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
result.push(format!(r#"<td class="{}count excluded"></td>"#, classes_i));
|
||||
result.push(format!(r#"<td class="{}count excluded">WD</td>"#, classes_i));
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td>"#, 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#"<td class="{}count excluded">Ex</td>"#, classes_i));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, 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#"<td class="{}count elected">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, 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#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, 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#"<td class="{}count excluded">WD</td>"#, classes_i));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, 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#"<td class="{}count excluded">Ex</td>"#, classes_i));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">Ex</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)));
|
||||
}
|
||||
} else {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, 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#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
result.push(format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)));
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
result.push(format!(r#"<td class="{}count excluded"></td><td class="count excluded"></td>"#, classes_i));
|
||||
result.push(format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, classes_i));
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, 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#"<td class="{}count excluded"></td><td class="count excluded">Ex</td>"#, classes_i));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)));
|
||||
}
|
||||
"votes_transposed" => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals), pp(&state.exhausted.votes, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals), pp(&state.loss_fraction.votes, opts.pp_decimals)));
|
||||
}
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&state.exhausted.ballot_transfers, 0), pps(&state.exhausted.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pp(&state.exhausted.num_ballots(), 0), pp(&state.exhausted.votes, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)));
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)));
|
||||
}
|
||||
"votes_transposed" => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, 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#"<td class="{}count">{}</td><td class="count">{}</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pp(vre, opts.pp_decimals)));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(vre, opts.pp_decimals)));
|
||||
}
|
||||
} else {
|
||||
if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
|
||||
result.push(format!(r#"<td class="{}count"></td>"#, classes_i));
|
||||
} else {
|
||||
result.push(format!(r#"<td class="{}count"></td><td class="count"></td>"#, classes_i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generate the final column of the HTML results table
|
||||
pub fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
// Header rows
|
||||
match report_style {
|
||||
"votes" | "votes_transposed" => {
|
||||
result.push(String::from(r#"<td rowspan="3"></td>"#));
|
||||
result.push(String::from(""));
|
||||
result.push(String::from(""));
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(String::from(r#"<td rowspan="4"></td>"#));
|
||||
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#"<td{} class="bb elected">ELECTED {}</td>"#, rowspan, count_card.order_elected));
|
||||
} else if count_card.state == stv::CandidateState::Excluded {
|
||||
result.push(format!(r#"<td{} class="bb excluded">Excluded {}</td>"#, rowspan, -count_card.order_elected));
|
||||
} else if count_card.state == stv::CandidateState::Withdrawn {
|
||||
result.push(format!(r#"<td{} class="bb excluded">Withdrawn</td>"#, rowspan));
|
||||
} else {
|
||||
result.push(format!(r#"<td{} class="bb"></td>"#, 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: Number>(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(".", ".<sup>", 1);
|
||||
raw.push_str("</sup>");
|
||||
}
|
||||
|
||||
if raw.starts_with('-') {
|
||||
raw = raw.replacen("-", "−", 1);
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
/// Signed version of [pp]
|
||||
fn pps<N: Number>(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(".", ".<sup>", 1);
|
||||
raw.push_str("</sup>");
|
||||
}
|
||||
|
||||
if raw.starts_with('-') {
|
||||
raw = raw.replacen("-", "−", 1);
|
||||
} else {
|
||||
raw.insert(0, '+');
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
420
src/stv/wasm.rs
420
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 [<describe_count_$type>](filename: String, election: &[<Election$type>], 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<N: Number>(filename: String, election: &Election<N>, opts: &stv::STVOptions) -> String {
|
||||
let mut result = String::from("<p>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::<N>();
|
||||
if !opts_str.is_empty() {
|
||||
result.push_str(&format!(r#"Counting using options <span style="font-family: monospace;">{}</span>.</p>"#, opts_str))
|
||||
} else {
|
||||
result.push_str(r#"Counting using default options.</p>"#);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
|
||||
|
||||
if report_style == "ballots_votes" {
|
||||
result.push_str(r#"<tr class="hint-papers-votes"><td></td></tr>"#);
|
||||
}
|
||||
|
||||
for candidate in election.candidates.iter() {
|
||||
if candidate.is_dummy {
|
||||
continue;
|
||||
}
|
||||
|
||||
if report_style == "votes_transposed" {
|
||||
result.push_str(&format!(r#"<tr class="candidate transfers"><td class="candidate-name">{}</td></tr>"#, candidate.name));
|
||||
} else {
|
||||
result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2" class="candidate-name">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
|
||||
}
|
||||
}
|
||||
|
||||
if report_style == "votes_transposed" {
|
||||
result.push_str(r#"<tr class="info transfers"><td>Exhausted</td></tr>"#);
|
||||
} else {
|
||||
result.push_str(r#"<tr class="info transfers"><td rowspan="2">Exhausted</td></tr><tr class="info votes"></tr>"#);
|
||||
}
|
||||
|
||||
if report_style == "votes_transposed" {
|
||||
result.push_str(r#"<tr class="info transfers"><td>Loss by fraction</td></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"#);
|
||||
} else {
|
||||
result.push_str(r#"<tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr><tr class="info votes"></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"#);
|
||||
}
|
||||
|
||||
if stv::should_show_vre(opts) {
|
||||
result.push_str(r#"<tr class="info transfers"><td>Vote required for election</td></tr>"#);
|
||||
}
|
||||
|
||||
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<N: Number>(stage_num: usize, state: &CountState<N>, 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(",<br>");
|
||||
title_str = format!("{},<br>and {} others", first_4_cands, candidates.len() - 4);
|
||||
} else {
|
||||
title_str = candidates.iter().map(|c| &c.name).join(",<br>");
|
||||
}
|
||||
}
|
||||
StageKind::BallotsOf(candidate) => {
|
||||
title_str = candidate.name.clone();
|
||||
}
|
||||
};
|
||||
|
||||
match report_style {
|
||||
"votes" => {
|
||||
result.push(&format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, classes_o, kind_str).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, classes_o, title_str).into());
|
||||
}
|
||||
"votes_transposed" => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(&format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, classes_o, kind_str).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, classes_o, title_str).into());
|
||||
} else {
|
||||
result.push(&format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num).into());
|
||||
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str).into());
|
||||
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, title_str).into());
|
||||
//result.push(&format!(r#"<td{}>X'fers</td><td>Total</td>"#, tdclasses1).into());
|
||||
}
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(&format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num).into());
|
||||
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str).into());
|
||||
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, title_str).into());
|
||||
result.push(&format!(r#"<td{}>Ballots</td><td>Votes</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
result.push(&format!(r#"<td class="{}count excluded"></td>"#, classes_i).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, classes_i).into());
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, 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#"<td class="{}count excluded">Ex</td>"#, classes_i).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, 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#"<td class="{}count elected">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, 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#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, 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#"<td class="{}count excluded">WD</td>"#, classes_i).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, 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#"<td class="{}count excluded">Ex</td>"#, classes_i).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">Ex</td>"#, classes_i, pps(&count_card.transfers, opts.pp_decimals)).into());
|
||||
}
|
||||
} else {
|
||||
if hide_xfers_trsp {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, classes_i, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, 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#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pps(&count_card.ballot_transfers, 0), pps(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, classes_i, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded"></td>"#, classes_i).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, classes_i).into());
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, 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#"<td class="{}count excluded"></td><td class="count excluded">Ex</td>"#, classes_i).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
"votes_transposed" => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&state.exhausted.transfers, opts.pp_decimals), pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, 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#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pps(&state.exhausted.ballot_transfers, 0), pps(&state.exhausted.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, classes_i, pp(&state.exhausted.num_ballots(), 0), pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pps(&state.loss_fraction.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)).into());
|
||||
}
|
||||
"votes_transposed" => {
|
||||
if hide_xfers_trsp {
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, classes_i, pp(&total_vote, opts.pp_decimals)).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, 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#"<td class="{}count">{}</td><td class="count">{}</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, 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#"<td class="{}count">{}</td>"#, classes_i, pp(vre, opts.pp_decimals)).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, classes_i, pp(vre, opts.pp_decimals)).into());
|
||||
}
|
||||
} else {
|
||||
if report_style == "votes" || (report_style == "votes_transposed" && hide_xfers_trsp) {
|
||||
result.push(&format!(r#"<td class="{}count"></td>"#, classes_i).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="{}count"></td><td class="count"></td>"#, 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<N: Number>(state: &CountState<N>, stage_num: usize)
|
|||
|
||||
/// Generate the final column of the HTML results table
|
||||
pub fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Array {
|
||||
let result = Array::new();
|
||||
|
||||
// Header rows
|
||||
match report_style {
|
||||
"votes" | "votes_transposed" => {
|
||||
result.push(&r#"<td rowspan="3"></td>"#.into());
|
||||
result.push(&"".into());
|
||||
result.push(&"".into());
|
||||
}
|
||||
"ballots_votes" => {
|
||||
result.push(&r#"<td rowspan="4"></td>"#.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#"<td{} class="bb elected">ELECTED {}</td>"#, rowspan, count_card.order_elected).into());
|
||||
} else if count_card.state == stv::CandidateState::Excluded {
|
||||
result.push(&format!(r#"<td{} class="bb excluded">Excluded {}</td>"#, rowspan, -count_card.order_elected).into());
|
||||
} else if count_card.state == stv::CandidateState::Withdrawn {
|
||||
result.push(&format!(r#"<td{} class="bb excluded">Withdrawn</td>"#, rowspan).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td{} class="bb"></td>"#, 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<N: Number>(state: &CountState<N>, opts: &stv::STVOpt
|
|||
result.push_str("</ol>");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// HTML pretty-print the number to the specified decimal places
|
||||
fn pp<N: Number>(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(".", ".<sup>", 1);
|
||||
raw.push_str("</sup>");
|
||||
}
|
||||
|
||||
if raw.starts_with('-') {
|
||||
raw = raw.replacen("-", "−", 1);
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
/// Signed version of [pp]
|
||||
fn pps<N: Number>(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(".", ".<sup>", 1);
|
||||
raw.push_str("</sup>");
|
||||
}
|
||||
|
||||
if raw.starts_with('-') {
|
||||
raw = raw.replacen("-", "−", 1);
|
||||
} else {
|
||||
raw.insert(0, '+');
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue