From 3bb538e99efb6498830d287ca3a7c4ddc9a4022b Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Thu, 3 Jun 2021 15:47:19 +1000 Subject: [PATCH] Further work on HTML output --- html/index.js | 18 ++++++ html/main.css | 6 +- src/stv/wasm.rs | 143 ++++++++++++++++++++++++++++++------------------ 3 files changed, 111 insertions(+), 56 deletions(-) diff --git a/html/index.js b/html/index.js index e159d17..44c58f2 100644 --- a/html/index.js +++ b/html/index.js @@ -28,6 +28,8 @@ function clickAdvancedOptions() { var wasm = wasm_bindgen; var tblResult = document.getElementById('result'); +var divLogs2 = document.getElementById('resultLogs2'); +var olStageComments; function updateResultTable(result) { for (let i = 0; i < result.length; i++) { @@ -35,6 +37,12 @@ function updateResultTable(result) { } } +function updateStageComments(comment) { + let elLi = document.createElement('li'); + elLi.innerHTML = comment; + olStageComments.append(elLi); +} + async function clickCount() { if (document.getElementById('bltFile').files.length === 0) { return; @@ -53,6 +61,10 @@ async function clickCount() { // Init results table tblResult.innerHTML = wasm.init_results_table_Rational(election); + divLogs2.innerHTML = '

Stage comments:

'; + olStageComments = document.createElement('ol'); + divLogs2.append(olStageComments); + // Init STV options let opts = wasm.STVOptions.new( document.getElementById('chkRoundTVs').checked ? parseInt(document.getElementById('txtRoundTVs').value) : null, @@ -72,6 +84,7 @@ async function clickCount() { let state = wasm.CountStateRational.new(election); wasm.count_init_Rational(state, opts); updateResultTable(wasm.update_results_table_Rational(1, state, opts)); + updateStageComments(wasm.update_stage_comments_Rational(state)); for (let stageNum = 2;; stageNum++) { let isDone = wasm.count_one_stage_Rational(state, opts); @@ -79,5 +92,10 @@ async function clickCount() { break; } updateResultTable(wasm.update_results_table_Rational(stageNum, state, opts)); + updateStageComments(wasm.update_stage_comments_Rational(state)); } + + updateResultTable(wasm.finalise_results_table_Rational(state)); + + divLogs2.insertAdjacentHTML('beforeend', wasm.final_result_summary_Rational(state)); } diff --git a/html/main.css b/html/main.css index df9353f..f62cbe4 100644 --- a/html/main.css +++ b/html/main.css @@ -83,7 +83,7 @@ td.count sup { tr.stage-no td, tr.stage-kind td, tr.stage-comment td { text-align: center; } -tr.stage-kind td:not(:first-child) { +tr.stage-kind td { font-size: 0.75em; min-width: 5rem; color: #1b2839; @@ -106,10 +106,10 @@ tr.info td { color-adjust: exact; -webkit-print-color-adjust: exact; } -tr.stage-no td:not(:first-child), tr.transfers td { +tr.stage-no td:not(:empty), tr.transfers td { border-top: 1px solid #76858c; } -tr.info:last-child td { +tr.info:last-child td, .bb { border-bottom: 1px solid #76858c; } diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 396c6fe..3768b6a 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -81,17 +81,23 @@ macro_rules! impl_type { return update_results_table(stage_num, &state.0, opts); } - /*#[wasm_bindgen] + #[wasm_bindgen] #[allow(non_snake_case)] - pub fn [](stage_num: usize, state: &[]) { - let result = StageResult { - kind: state.0.kind, - title: &state.0.title, - logs: state.0.logger.render(), - state: CountStateOrRef::from(&state.0), - }; - print_stage(stage_num, &result); - }*/ + pub fn [](state: &[]) -> String { + return update_stage_comments(&state.0); + } + + #[wasm_bindgen] + #[allow(non_snake_case)] + pub fn [](state: &[]) -> Array { + return finalise_results_table(&state.0); + } + + #[wasm_bindgen] + #[allow(non_snake_case)] + pub fn [](state: &[]) -> String { + return final_result_summary(&state.0); + } // Wrapper structs // Required as we cannot specify &'static in wasm-bindgen: issue #1187 @@ -128,7 +134,7 @@ impl_type!(NativeFloat64); // Reporting fn init_results_table(election: &Election) -> String { - let mut result = String::from(r#""#); + let mut result = String::from(r#""#); for candidate in election.candidates.iter() { result.push_str(&format!(r#"{}"#, candidate.name)); } @@ -144,66 +150,97 @@ fn update_results_table(stage_num: usize, state: &CountState, opts for candidate in state.election.candidates.iter() { let count_card = state.candidates.get(candidate).unwrap(); if count_card.state == stv::CandidateState::ELECTED { - result.push(&format!(r#"{:.dps$}"#, count_card.transfers, dps=opts.pp_decimals).into()); - result.push(&format!(r#"{:.dps$}"#, count_card.votes, dps=opts.pp_decimals).into()); + result.push(&format!(r#"{}"#, pp(&count_card.transfers, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, pp(&count_card.votes, opts.pp_decimals)).into()); + } else if count_card.state == stv::CandidateState::EXCLUDED { + result.push(&format!(r#"{}"#, pp(&count_card.transfers, opts.pp_decimals)).into()); + if count_card.votes.is_zero() { + result.push(&r#"Ex"#.into()); + } else { + result.push(&format!(r#"{}"#, pp(&count_card.votes, opts.pp_decimals)).into()); + } } else { - result.push(&format!(r#"{:.dps$}"#, count_card.transfers, dps=opts.pp_decimals).into()); - result.push(&format!(r#"{:.dps$}"#, count_card.votes, dps=opts.pp_decimals).into()); + result.push(&format!(r#"{}"#, pp(&count_card.transfers, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, pp(&count_card.votes, opts.pp_decimals)).into()); } } - result.push(&format!(r#"{:.dps$}"#, state.exhausted.transfers, dps=opts.pp_decimals).into()); - result.push(&format!(r#"{:.dps$}"#, state.exhausted.votes, dps=opts.pp_decimals).into()); - result.push(&format!(r#"{:.dps$}"#, state.loss_fraction.transfers, dps=opts.pp_decimals).into()); - result.push(&format!(r#"{:.dps$}"#, state.loss_fraction.votes, dps=opts.pp_decimals).into()); + result.push(&format!(r#"{}"#, pp(&state.exhausted.transfers, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, pp(&state.exhausted.votes, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, pp(&state.loss_fraction.votes, opts.pp_decimals)).into()); // Calculate total votes let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); total_vote += &state.exhausted.votes; total_vote += &state.loss_fraction.votes; - result.push(&format!(r#"{:.dps$}"#, total_vote, dps=opts.pp_decimals).into()); + result.push(&format!(r#"{}"#, pp(&total_vote, opts.pp_decimals)).into()); - result.push(&format!(r#"{:.dps$}"#, state.quota, dps=opts.pp_decimals).into()); + result.push(&format!(r#"{}"#, pp(&state.quota, opts.pp_decimals)).into()); return result; } -/*fn print_candidates<'a, N: 'a + Number, I: Iterator)>>(candidates: I) { - for (candidate, count_card) in candidates { - if count_card.state == CandidateState::ELECTED { - cprintln!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=2); - } else if count_card.state == CandidateState::EXCLUDED { - cprintln!("- {}: {:.dps$} ({:.dps$}) - Excluded {}", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=2); - } else { - cprintln!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=2); - } - } +fn update_stage_comments(state: &CountState) -> String { + return state.logger.render().join(" "); } -fn print_stage(stage_num: usize, result: &StageResult) { - // Print stage details - match result.kind { - None => { cprintln!("{}. {}", stage_num, result.title); } - Some(kind) => { cprintln!("{}. {} {}", stage_num, kind, result.title); } - }; - cprintln!("{}", result.logs.join(" ")); +fn finalise_results_table(state: &CountState) -> Array { + let result = Array::new(); - let state = result.state.as_ref(); + // Header rows + result.push(&r#""#.into()); + result.push(&"".into()); + result.push(&"".into()); - // Print candidates - let candidates = state.election.candidates.iter() - .map(|c| (c, state.candidates.get(c).unwrap())); - print_candidates(candidates); + // Candidate states + for candidate in state.election.candidates.iter() { + let count_card = state.candidates.get(candidate).unwrap(); + if count_card.state == stv::CandidateState::ELECTED { + result.push(&format!(r#"ELECTED {}"#, count_card.order_elected).into()); + } else if count_card.state == stv::CandidateState::EXCLUDED { + result.push(&format!(r#"Excluded {}"#, -count_card.order_elected).into()); + } else { + result.push(&r#""#.into()); + } + result.push(&"".into()); + } - // Print summary rows - cprintln!("Exhausted: {:.dps$} ({:.dps$})", state.exhausted.votes, state.exhausted.transfers, dps=2); - cprintln!("Loss by fraction: {:.dps$} ({:.dps$})", state.loss_fraction.votes, state.loss_fraction.transfers, dps=2); + return result; +} + +fn final_result_summary(state: &CountState) -> String { + let mut result = String::from("

Count complete. The winning candidates are, in order of election:

    "); - let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); - total_vote += &state.exhausted.votes; - total_vote += &state.loss_fraction.votes; - cprintln!("Total votes: {:.dps$}", total_vote, dps=2); + 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)); + } + } + winners.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); - cprintln!("Quota: {:.dps$}", state.quota, dps=2); + for (winner, _) in winners.into_iter() { + result.push_str(&format!("
  1. {}
  2. ", winner.name)); + } - cprintln!(""); -}*/ + result.push_str("
"); + return result; +} + +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; +}