Display up to 5 names only in web UI header, separate with line breaks

This commit is contained in:
RunasSudo 2021-09-06 02:43:33 +10:00
parent 18c974117e
commit e4bfe45f49
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
9 changed files with 106 additions and 61 deletions

View File

@ -361,10 +361,7 @@ where
fn print_stage<N: Number>(stage_num: usize, state: &CountState<N>, opts: &STVOptions) { fn print_stage<N: Number>(stage_num: usize, state: &CountState<N>, opts: &STVOptions) {
// Print stage details // Print stage details
match state.kind { println!("{}. {}", stage_num, state.title);
None => { println!("{}. {}", stage_num, state.title); }
Some(kind) => { println!("{}. {} {}", stage_num, kind, state.title); }
};
println!("{}", state.logger.render().join(" ")); println!("{}", state.logger.render().join(" "));
// Print candidates // Print candidates

View File

@ -30,6 +30,7 @@ use crate::numbers::{SerializedNumber, SerializedOptionNumber};
use std::cmp::max; use std::cmp::max;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt;
/// An election to be counted /// An election to be counted
#[derive(Clone)] #[derive(Clone)]
@ -134,12 +135,8 @@ pub struct CountState<'a, N: Number> {
/// [ConstraintMatrix] for constrained elections /// [ConstraintMatrix] for constrained elections
pub constraint_matrix: Option<ConstraintMatrix>, pub constraint_matrix: Option<ConstraintMatrix>,
/// The type of stage being counted /// The type of stage being counted, etc.
/// pub title: StageKind<'a>,
/// For example, "Surplus of", "Exclusion of"
pub kind: Option<&'a str>,
/// The description of the stage being counted, excluding [CountState::kind]
pub title: String,
/// [Logger] for this stage of the count /// [Logger] for this stage of the count
pub logger: Logger<'a>, pub logger: Logger<'a>,
} }
@ -161,8 +158,7 @@ impl<'a, N: Number> CountState<'a, N> {
num_elected: 0, num_elected: 0,
num_excluded: 0, num_excluded: 0,
constraint_matrix: None, constraint_matrix: None,
kind: None, title: StageKind::FirstPreferences,
title: String::new(),
logger: Logger { entries: Vec::new() }, 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 /// Current state of a [Candidate] during an election count
#[derive(Clone)] #[derive(Clone)]
pub struct CountCard<'a, N> { pub struct CountCard<'a, N> {

View File

@ -19,7 +19,7 @@ use super::{ExclusionMethod, NextPreferencesEntry, STVError, STVOptions, SumSurp
use super::sample; use super::sample;
use crate::constraints; 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::numbers::Number;
use crate::ties; use crate::ties;
@ -78,8 +78,7 @@ where
state.loss_fraction.transfers = lbf; state.loss_fraction.transfers = lbf;
} }
state.kind = None; state.title = StageKind::FirstPreferences;
state.title = "First preferences".to_string();
state.logger.log_literal("First preferences distributed.".to_string()); 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] /// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus]
fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate) fn distribute_surplus<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, elected_candidate: &'a Candidate)
where where
for<'r> &'r N: ops::Add<&'r N, Output=N>, 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::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::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N> for<'r> &'r N: ops::Neg<Output=N>
{ {
state.kind = Some("Surplus of"); state.title = StageKind::SurplusOf(&elected_candidate);
state.title = String::from(&elected_candidate.name);
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name)); state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
let count_card = &state.candidates[elected_candidate]; let count_card = &state.candidates[elected_candidate];
@ -787,7 +785,6 @@ where
// Redistribute first preferences // Redistribute first preferences
super::distribute_first_preferences(state, opts); super::distribute_first_preferences(state, opts);
state.kind = Some("Exclusion of");
state.title = orig_title; state.title = orig_title;
// Trigger recalculation of quota within stv::count_one_stage // Trigger recalculation of quota within stv::count_one_stage

View File

@ -17,7 +17,7 @@
use super::{STVError, STVOptions}; 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 crate::numbers::Number;
use itertools::Itertools; use itertools::Itertools;
@ -137,8 +137,7 @@ where
state.loss_fraction.transfers = lbf; state.loss_fraction.transfers = lbf;
} }
state.kind = None; state.title = StageKind::FirstPreferences;
state.title = "First preferences".to_string();
state.logger.log_literal("First preferences distributed.".to_string()); state.logger.log_literal("First preferences distributed.".to_string());
} }
@ -337,8 +336,7 @@ where
// Remove intermediate logs on quota calculation // Remove intermediate logs on quota calculation
state.logger.entries.clear(); state.logger.entries.clear();
state.kind = None; state.title = StageKind::SurplusesDistributed;
state.title = "Surpluses distributed".to_string();
if num_iterations == 1 { if num_iterations == 1 {
state.logger.log_literal("Surpluses distributed, requiring 1 iteration.".to_string()); state.logger.log_literal("Surpluses distributed, requiring 1 iteration.".to_string());
} else { } else {

View File

@ -31,7 +31,7 @@ pub mod wasm;
use crate::constraints; use crate::constraints;
use crate::election::Election; use crate::election::Election;
use crate::numbers::Number; 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::sharandom::SHARandom;
use crate::ties::{self, TieStrategy}; use crate::ties::{self, TieStrategy};
@ -1220,9 +1220,7 @@ fn do_bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions, templa
/// Returns `true` if any candidates were elected. /// Returns `true` if any candidates were elected.
fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> { fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> {
if can_bulk_elect(state, 0) { if can_bulk_elect(state, 0) {
state.kind = None; state.title = StageKind::BulkElection;
state.title = "Bulk election".to_string();
do_bulk_elect(state, opts, "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.")?; do_bulk_elect(state, opts, "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.")?;
return Ok(true); return Ok(true);
} }
@ -1257,8 +1255,7 @@ where
} }
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect(); let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
state.kind = Some("Exclusion of"); state.title = StageKind::ExclusionOf(excluded_candidates.clone());
state.title = names.join(", ");
state.logger.log_smart( state.logger.log_smart(
"Doomed candidate, {}, is excluded.", "Doomed candidate, {}, is excluded.",
"Doomed candidates, {}, are 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(); let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
state.kind = Some("Exclusion of"); state.title = StageKind::ExclusionOf(excluded_candidates.clone());
state.title = names.join(", ");
state.logger.log_smart( state.logger.log_smart(
"No surpluses to distribute, so {} is excluded.", "No surpluses to distribute, so {} is excluded.",
"No surpluses to distribute, so {} are excluded.", "No surpluses to distribute, so {} are excluded.",
@ -1450,8 +1446,7 @@ where
.collect(); .collect();
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect(); let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).sorted().collect();
state.kind = Some("Exclusion of"); state.title = StageKind::ExclusionOf(excluded_candidates.clone());
state.title = names.join(", ");
state.logger.log_smart( state.logger.log_smart(
"Continuing exclusion of {}.", "Continuing exclusion of {}.",
"Continuing exclusion of {}.", "Continuing exclusion of {}.",

View File

@ -16,7 +16,7 @@
*/ */
use crate::constraints; 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::numbers::Number;
use crate::stv::{STVOptions, SampleMethod, SurplusMethod}; 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] /// Distribute the surplus of a given candidate according to the random subset method, based on [STVOptions::surplus]
pub fn distribute_surplus<N: Number>(state: &mut CountState<N>, 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 where
for<'r> &'r N: ops::Sub<&'r N, Output=N>, 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::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N> for<'r> &'r N: ops::Neg<Output=N>
{ {
state.kind = Some("Surplus of"); state.title = StageKind::SurplusOf(&elected_candidate);
state.title = String::from(&elected_candidate.name);
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name)); state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
let count_card = state.candidates.get_mut(elected_candidate).unwrap(); let count_card = state.candidates.get_mut(elected_candidate).unwrap();

View File

@ -19,7 +19,7 @@
#![allow(unused_unsafe)] // Confuses cargo check #![allow(unused_unsafe)] // Confuses cargo check
use crate::constraints::Constraints; 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::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
use crate::parser::blt; use crate::parser::blt;
use crate::stv; use crate::stv;
@ -27,6 +27,7 @@ use crate::ties;
extern crate console_error_panic_hook; extern crate console_error_panic_hook;
use itertools::Itertools;
use js_sys::Array; use js_sys::Array;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
@ -363,7 +364,7 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
// Insert borders to left of new exclusions in Wright STV // Insert borders to left of new exclusions in Wright STV
let classes_o; // Outer version let classes_o; // Outer version
let classes_i; // Inner 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_o = r#" class="blw""#;
classes_i = r#"blw "#; classes_i = r#"blw "#;
} else { } else {
@ -373,35 +374,56 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
// Hide transfers column for first preferences if transposed // Hide transfers column for first preferences if transposed
let hide_xfers_trsp; 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; hide_xfers_trsp = true;
} else { } else {
hide_xfers_trsp = false; hide_xfers_trsp = false;
} }
// Header rows // 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(",<br>");
title_str = format!("{},<br>and {} others", first_4_cands, candidates.len() - 4);
} else {
title_str = candidates.iter().map(|c| &c.name).join(",<br>");
}
}
};
match report_style { match report_style {
"votes" => { "votes" => {
result.push(&format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num).into()); 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, state.kind.unwrap_or("")).into()); result.push(&format!(r#"<td{}>{}</td>"#, classes_o, kind_str).into());
result.push(&format!(r#"<td{}>{}</td>"#, classes_o, state.title).into()); result.push(&format!(r#"<td{}>{}</td>"#, classes_o, title_str).into());
} }
"votes_transposed" => { "votes_transposed" => {
if hide_xfers_trsp { 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{0}><a href="#stage{1}">{1}</a></td>"##, classes_o, stage_num).into());
result.push(&format!(r#"<td{}>{}</td>"#, classes_o, state.kind.unwrap_or("")).into()); result.push(&format!(r#"<td{}>{}</td>"#, classes_o, kind_str).into());
result.push(&format!(r#"<td{}>{}</td>"#, classes_o, state.title).into()); result.push(&format!(r#"<td{}>{}</td>"#, classes_o, title_str).into());
} else { } 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{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, state.kind.unwrap_or("")).into()); result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str).into());
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, state.title).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()); //result.push(&format!(r#"<td{}>X'fers</td><td>Total</td>"#, tdclasses1).into());
} }
} }
"ballots_votes" => { "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{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, state.kind.unwrap_or("")).into()); result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, kind_str).into());
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, classes_o, state.title).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()); result.push(&format!(r#"<td{}>Ballots</td><td>Votes</td>"#, classes_o).into());
} }
_ => unreachable!("Invalid report_style") _ => unreachable!("Invalid report_style")

View File

@ -224,10 +224,7 @@ fn prompt<'c, N: Number>(state: &CountState<N>, opts: &STVOptions, candidates: &
// Show intrastage progress if required // Show intrastage progress if required
if !state.logger.entries.is_empty() { if !state.logger.entries.is_empty() {
// Print stage details // Print stage details
match state.kind { println!("Tie during: {}", state.title);
None => { println!("Tie during: {}", state.title); }
Some(kind) => { println!("Tie during: {} {}", kind, state.title); }
};
println!("{}", state.logger.render().join(" ")); println!("{}", state.logger.render().join(" "));
// Print candidates // Print candidates
@ -279,10 +276,7 @@ fn prompt<'c, N: Number>(state: &CountState<N>, opts: &STVOptions, candidates: &
// Show intrastage progress if required // Show intrastage progress if required
if !state.logger.entries.is_empty() { if !state.logger.entries.is_empty() {
// Print stage details // Print stage details
match state.kind { message.push_str(&format!("Tie during: {}\n", state.title));
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(&state.logger.render().join(" ")); message.push_str(&state.logger.render().join(" "));
message.push('\n'); message.push('\n');

View File

@ -92,10 +92,7 @@ where
// Validate stage name // Validate stage name
let stage_name = &records.iter().nth(1).unwrap()[idx*2 + 1]; let stage_name = &records.iter().nth(1).unwrap()[idx*2 + 1];
if stage_name.len() > 0 { if stage_name.len() > 0 {
match state.kind { assert_eq!(format!("{}", state.title), stage_name);
Some(kind) => assert_eq!(format!("{} {}", kind, state.title), stage_name),
None => assert_eq!(state.title, stage_name),
}
} }
let mut candidate_votes: Vec<Option<N>> = records.iter().skip(2) let mut candidate_votes: Vec<Option<N>> = records.iter().skip(2)