From e4bfe45f49c9daafd53bcf024d91e9339a50ebbb Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Mon, 6 Sep 2021 02:43:33 +1000 Subject: [PATCH] Display up to 5 names only in web UI header, separate with line breaks --- src/cli/stv.rs | 5 +--- src/election.rs | 62 ++++++++++++++++++++++++++++++++++++++++------ src/stv/gregory.rs | 11 +++----- src/stv/meek.rs | 8 +++--- src/stv/mod.rs | 15 ++++------- src/stv/sample.rs | 7 +++--- src/stv/wasm.rs | 44 ++++++++++++++++++++++++-------- src/ties.rs | 10 ++------ tests/utils/mod.rs | 5 +--- 9 files changed, 106 insertions(+), 61 deletions(-) diff --git a/src/cli/stv.rs b/src/cli/stv.rs index 007baa7..959efd7 100644 --- a/src/cli/stv.rs +++ b/src/cli/stv.rs @@ -361,10 +361,7 @@ where fn print_stage(stage_num: usize, state: &CountState, opts: &STVOptions) { // Print stage details - match state.kind { - None => { println!("{}. {}", stage_num, state.title); } - Some(kind) => { println!("{}. {} {}", stage_num, kind, state.title); } - }; + println!("{}. {}", stage_num, state.title); println!("{}", state.logger.render().join(" ")); // Print candidates diff --git a/src/election.rs b/src/election.rs index febe215..f8db98d 100644 --- a/src/election.rs +++ b/src/election.rs @@ -30,6 +30,7 @@ use crate::numbers::{SerializedNumber, SerializedOptionNumber}; use std::cmp::max; use std::collections::HashMap; +use std::fmt; /// An election to be counted #[derive(Clone)] @@ -134,12 +135,8 @@ pub struct CountState<'a, N: Number> { /// [ConstraintMatrix] for constrained elections pub constraint_matrix: Option, - /// The type of stage being counted - /// - /// For example, "Surplus of", "Exclusion of" - pub kind: Option<&'a str>, - /// The description of the stage being counted, excluding [CountState::kind] - pub title: String, + /// The type of stage being counted, etc. + pub title: StageKind<'a>, /// [Logger] for this stage of the count pub logger: Logger<'a>, } @@ -161,8 +158,7 @@ impl<'a, N: Number> CountState<'a, N> { num_elected: 0, num_excluded: 0, constraint_matrix: None, - kind: None, - title: String::new(), + title: StageKind::FirstPreferences, logger: Logger { entries: Vec::new() }, }; @@ -293,6 +289,56 @@ impl<'a, N: Number> CountState<'a, N> { } } +/// The kind, title, etc. of the stage being counted +#[derive(Clone)] +pub enum StageKind<'a> { + /// First preferences + FirstPreferences, + /// Surplus of ... + SurplusOf(&'a Candidate), + /// Exclusion of ... + ExclusionOf(Vec<&'a Candidate>), + /// Surpluses distributed (Meek) + SurplusesDistributed, + /// Bulk election + BulkElection, +} + +impl<'a> StageKind<'a> { + /// Return the "kind" portion of the title + pub fn kind_as_string(&self) -> &'static str { + return match self { + StageKind::FirstPreferences => "", + StageKind::SurplusOf(_) => "Surplus of", + StageKind::ExclusionOf(_) => "Exclusion of", + StageKind::SurplusesDistributed => "", + StageKind::BulkElection => "", + }; + } +} + +impl<'a> fmt::Display for StageKind<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StageKind::FirstPreferences => { + return f.write_str("First preferences"); + } + StageKind::SurplusOf(candidate) => { + return f.write_fmt(format_args!("{} {}", self.kind_as_string(), candidate.name)); + } + StageKind::ExclusionOf(candidates) => { + return f.write_fmt(format_args!("{} {}", self.kind_as_string(), candidates.iter().map(|c| &c.name).sorted().join(", "))); + } + StageKind::SurplusesDistributed => { + return f.write_str("Surpluses distributed"); + } + StageKind::BulkElection => { + return f.write_str("Bulk election"); + } + } + } +} + /// Current state of a [Candidate] during an election count #[derive(Clone)] pub struct CountCard<'a, N> { diff --git a/src/stv/gregory.rs b/src/stv/gregory.rs index d97e909..5a58459 100644 --- a/src/stv/gregory.rs +++ b/src/stv/gregory.rs @@ -19,7 +19,7 @@ use super::{ExclusionMethod, NextPreferencesEntry, STVError, STVOptions, SumSurp use super::sample; use crate::constraints; -use crate::election::{Candidate, CandidateState, CountState, Parcel, Vote}; +use crate::election::{Candidate, CandidateState, CountState, Parcel, StageKind, Vote}; use crate::numbers::Number; use crate::ties; @@ -78,8 +78,7 @@ where state.loss_fraction.transfers = lbf; } - state.kind = None; - state.title = "First preferences".to_string(); + state.title = StageKind::FirstPreferences; state.logger.log_literal("First preferences distributed.".to_string()); } @@ -264,7 +263,7 @@ where } /// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus] -fn distribute_surplus(state: &mut CountState, opts: &STVOptions, elected_candidate: &Candidate) +fn distribute_surplus<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, elected_candidate: &'a Candidate) where for<'r> &'r N: ops::Add<&'r N, Output=N>, for<'r> &'r N: ops::Sub<&'r N, Output=N>, @@ -272,8 +271,7 @@ where for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Neg { - state.kind = Some("Surplus of"); - state.title = String::from(&elected_candidate.name); + state.title = StageKind::SurplusOf(&elected_candidate); state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name)); let count_card = &state.candidates[elected_candidate]; @@ -787,7 +785,6 @@ where // Redistribute first preferences super::distribute_first_preferences(state, opts); - state.kind = Some("Exclusion of"); state.title = orig_title; // Trigger recalculation of quota within stv::count_one_stage diff --git a/src/stv/meek.rs b/src/stv/meek.rs index 7b090d2..b0fe80c 100644 --- a/src/stv/meek.rs +++ b/src/stv/meek.rs @@ -17,7 +17,7 @@ use super::{STVError, STVOptions}; -use crate::election::{Ballot, Candidate, CandidateState, CountCard, CountState, Election}; +use crate::election::{Ballot, Candidate, CandidateState, CountCard, CountState, Election, StageKind}; use crate::numbers::Number; use itertools::Itertools; @@ -137,8 +137,7 @@ where state.loss_fraction.transfers = lbf; } - state.kind = None; - state.title = "First preferences".to_string(); + state.title = StageKind::FirstPreferences; state.logger.log_literal("First preferences distributed.".to_string()); } @@ -337,8 +336,7 @@ where // Remove intermediate logs on quota calculation state.logger.entries.clear(); - state.kind = None; - state.title = "Surpluses distributed".to_string(); + state.title = StageKind::SurplusesDistributed; if num_iterations == 1 { state.logger.log_literal("Surpluses distributed, requiring 1 iteration.".to_string()); } else { diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 8517f6b..5158c43 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -31,7 +31,7 @@ pub mod wasm; use crate::constraints; use crate::election::Election; use crate::numbers::Number; -use crate::election::{Candidate, CandidateState, CountCard, CountState, Vote}; +use crate::election::{Candidate, CandidateState, CountCard, CountState, StageKind, Vote}; use crate::sharandom::SHARandom; use crate::ties::{self, TieStrategy}; @@ -1220,9 +1220,7 @@ fn do_bulk_elect(state: &mut CountState, opts: &STVOptions, templa /// Returns `true` if any candidates were elected. fn bulk_elect(state: &mut CountState, opts: &STVOptions) -> Result { if can_bulk_elect(state, 0) { - state.kind = None; - state.title = "Bulk election".to_string(); - + state.title = StageKind::BulkElection; do_bulk_elect(state, opts, "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.")?; return Ok(true); } @@ -1257,8 +1255,7 @@ where } let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect(); - state.kind = Some("Exclusion of"); - state.title = names.join(", "); + state.title = StageKind::ExclusionOf(excluded_candidates.clone()); state.logger.log_smart( "Doomed candidate, {}, is excluded.", "Doomed candidates, {}, are excluded.", @@ -1396,8 +1393,7 @@ where } let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect(); - state.kind = Some("Exclusion of"); - state.title = names.join(", "); + state.title = StageKind::ExclusionOf(excluded_candidates.clone()); state.logger.log_smart( "No surpluses to distribute, so {} is excluded.", "No surpluses to distribute, so {} are excluded.", @@ -1450,8 +1446,7 @@ where .collect(); let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect(); - state.kind = Some("Exclusion of"); - state.title = names.join(", "); + state.title = StageKind::ExclusionOf(excluded_candidates.clone()); state.logger.log_smart( "Continuing exclusion of {}.", "Continuing exclusion of {}.", diff --git a/src/stv/sample.rs b/src/stv/sample.rs index 6b71145..180a4b0 100644 --- a/src/stv/sample.rs +++ b/src/stv/sample.rs @@ -16,7 +16,7 @@ */ use crate::constraints; -use crate::election::{Candidate, CandidateState, CountState, Parcel, Vote}; +use crate::election::{Candidate, CandidateState, CountState, Parcel, StageKind, Vote}; use crate::numbers::Number; use crate::stv::{STVOptions, SampleMethod, SurplusMethod}; @@ -45,14 +45,13 @@ where } /// Distribute the surplus of a given candidate according to the random subset method, based on [STVOptions::surplus] -pub fn distribute_surplus(state: &mut CountState, opts: &STVOptions, elected_candidate: &Candidate) -> Result<(), STVError> +pub fn distribute_surplus<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, elected_candidate: &'a Candidate) -> Result<(), STVError> where for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Neg { - state.kind = Some("Surplus of"); - state.title = String::from(&elected_candidate.name); + state.title = StageKind::SurplusOf(&elected_candidate); state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name)); let count_card = state.candidates.get_mut(elected_candidate).unwrap(); diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 91fbbc9..674c519 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -19,7 +19,7 @@ #![allow(unused_unsafe)] // Confuses cargo check use crate::constraints::Constraints; -use crate::election::{CandidateState, CountState, Election}; +use crate::election::{CandidateState, CountState, Election, StageKind}; use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational}; use crate::parser::blt; use crate::stv; @@ -27,6 +27,7 @@ use crate::ties; extern crate console_error_panic_hook; +use itertools::Itertools; use js_sys::Array; use wasm_bindgen::prelude::wasm_bindgen; @@ -363,7 +364,7 @@ fn update_results_table(stage_num: usize, state: &CountState, opts // Insert borders to left of new exclusions in Wright STV let classes_o; // Outer version let classes_i; // Inner version - if opts.exclusion == stv::ExclusionMethod::Wright && state.kind == Some("Exclusion of") { + if opts.exclusion == stv::ExclusionMethod::Wright && (if let StageKind::ExclusionOf(_) = state.title { true } else { false }) { classes_o = r#" class="blw""#; classes_i = r#"blw "#; } else { @@ -373,35 +374,56 @@ fn update_results_table(stage_num: usize, state: &CountState, opts // Hide transfers column for first preferences if transposed let hide_xfers_trsp; - if state.title == "First preferences" || (opts.exclusion == stv::ExclusionMethod::Wright && state.kind == Some("Exclusion of")) { + if let StageKind::FirstPreferences = state.title { + hide_xfers_trsp = true; + } else if opts.exclusion == stv::ExclusionMethod::Wright && (if let StageKind::ExclusionOf(_) = state.title { true } else { false }) { 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::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(",
"); + } + } + }; + match report_style { "votes" => { result.push(&format!(r##"{1}"##, classes_o, stage_num).into()); - result.push(&format!(r#"{}"#, classes_o, state.kind.unwrap_or("")).into()); - result.push(&format!(r#"{}"#, classes_o, state.title).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, state.kind.unwrap_or("")).into()); - result.push(&format!(r#"{}"#, classes_o, state.title).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, state.kind.unwrap_or("")).into()); - result.push(&format!(r#"{}"#, classes_o, state.title).into()); + result.push(&format!(r#"{}"#, classes_o, kind_str).into()); + result.push(&format!(r#"{}"#, classes_o, title_str).into()); //result.push(&format!(r#"X'fersTotal"#, tdclasses1).into()); } } "ballots_votes" => { result.push(&format!(r##"{1}"##, classes_o, stage_num).into()); - result.push(&format!(r#"{}"#, classes_o, state.kind.unwrap_or("")).into()); - result.push(&format!(r#"{}"#, classes_o, state.title).into()); + result.push(&format!(r#"{}"#, classes_o, kind_str).into()); + result.push(&format!(r#"{}"#, classes_o, title_str).into()); result.push(&format!(r#"BallotsVotes"#, classes_o).into()); } _ => unreachable!("Invalid report_style") diff --git a/src/ties.rs b/src/ties.rs index 76b23c5..ced7b9d 100644 --- a/src/ties.rs +++ b/src/ties.rs @@ -224,10 +224,7 @@ fn prompt<'c, N: Number>(state: &CountState, opts: &STVOptions, candidates: & // Show intrastage progress if required if !state.logger.entries.is_empty() { // Print stage details - match state.kind { - None => { println!("Tie during: {}", state.title); } - Some(kind) => { println!("Tie during: {} {}", kind, state.title); } - }; + println!("Tie during: {}", state.title); println!("{}", state.logger.render().join(" ")); // Print candidates @@ -279,10 +276,7 @@ fn prompt<'c, N: Number>(state: &CountState, opts: &STVOptions, candidates: & // Show intrastage progress if required if !state.logger.entries.is_empty() { // Print stage details - match state.kind { - None => { message.push_str(&format!("Tie during: {}\n", state.title)); } - Some(kind) => { message.push_str(&format!("Tie during: {} {}\n", kind, state.title)); } - }; + message.push_str(&format!("Tie during: {}\n", state.title)); message.push_str(&state.logger.render().join(" ")); message.push('\n'); diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index a5d8f99..8440066 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -92,10 +92,7 @@ where // Validate stage name let stage_name = &records.iter().nth(1).unwrap()[idx*2 + 1]; if stage_name.len() > 0 { - match state.kind { - Some(kind) => assert_eq!(format!("{} {}", kind, state.title), stage_name), - None => assert_eq!(state.title, stage_name), - } + assert_eq!(format!("{}", state.title), stage_name); } let mut candidate_votes: Vec> = records.iter().skip(2)