From 4c4099ee2297ea39262eee8a83842741cec7293d Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 12 Jun 2021 00:50:01 +1000 Subject: [PATCH] Handle withdrawn candidates --- src/election.rs | 29 +++++++++++++++++++++++------ src/main.rs | 10 +++++++--- src/stv/mod.rs | 30 +++++++++++++++--------------- src/stv/wasm.rs | 15 ++++++++++----- tests/ers97.rs | 6 +++--- tests/scotland.rs | 6 +++--- tests/utils/mod.rs | 6 +++--- 7 files changed, 64 insertions(+), 38 deletions(-) diff --git a/src/election.rs b/src/election.rs index a238ca3..bb2cf19 100644 --- a/src/election.rs +++ b/src/election.rs @@ -24,6 +24,7 @@ pub struct Election { pub name: String, pub seats: usize, pub candidates: Vec, + pub withdrawn_candidates: Vec, pub ballots: Vec>, } @@ -40,6 +41,7 @@ impl Election { name: String::new(), seats: seats, candidates: Vec::with_capacity(num_candidates), + withdrawn_candidates: Vec::new(), ballots: Vec::new(), }; @@ -50,6 +52,16 @@ impl Election { } let mut bits = line.split(" "); + + if line.starts_with("-") { + // Withdrawn candidates + for bit in bits.into_iter() { + let val = bit[1..bit.len()].parse::().expect("Syntax Error"); + election.withdrawn_candidates.push(val - 1); + } + continue; + } + let value = N::parse(bits.next().expect("Syntax Error")); let mut ballot = Ballot { @@ -150,6 +162,10 @@ impl<'a, N: Number> CountState<'a, N> { state.candidates.insert(candidate, CountCard::new()); } + for withdrawn_idx in election.withdrawn_candidates.iter() { + state.candidates.get_mut(&election.candidates[*withdrawn_idx]).unwrap().state = CandidateState::Withdrawn; + } + return state; } @@ -203,7 +219,7 @@ pub struct CountCard<'a, N> { impl<'a, N: Number> CountCard<'a, N> { pub fn new() -> Self { return CountCard { - state: CandidateState::HOPEFUL, + state: CandidateState::Hopeful, order_elected: 0, orig_votes: N::new(), transfers: N::new(), @@ -245,9 +261,10 @@ pub struct Ballot { #[derive(PartialEq)] #[derive(Clone)] pub enum CandidateState { - HOPEFUL, - GUARDED, - ELECTED, - DOOMED, - EXCLUDED, + Hopeful, + Guarded, + Elected, + Doomed, + Withdrawn, + Excluded, } diff --git a/src/main.rs b/src/main.rs index b9df557..187591e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -219,7 +219,7 @@ where let mut winners = Vec::new(); for (candidate, count_card) in state.candidates.iter() { - if count_card.state == CandidateState::ELECTED { + if count_card.state == CandidateState::Elected { winners.push((candidate, count_card.order_elected)); } } @@ -232,13 +232,17 @@ where fn print_candidates<'a, N: 'a + Number, I: Iterator)>>(candidates: I, cmd_opts: &STV) { for (candidate, count_card) in candidates { - if count_card.state == CandidateState::ELECTED { + if count_card.state == CandidateState::Elected { println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=cmd_opts.pp_decimals); - } else if count_card.state == CandidateState::EXCLUDED { + } else if count_card.state == CandidateState::Excluded { // If --hide-excluded, hide unless nonzero votes or nonzero transfers if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() { println!("- {}: {:.dps$} ({:.dps$}) - Excluded {}", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=cmd_opts.pp_decimals); } + } else if count_card.state == CandidateState::Withdrawn { + if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() { + println!("- {}: {:.dps$} ({:.dps$}) - Withdrawn", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals); + } } else { println!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals); } diff --git a/src/stv/mod.rs b/src/stv/mod.rs index ceeebab..cf3c42d 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -349,7 +349,7 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec(state: &mut CountState, opts: &STVOptions) { // Calculate total active vote let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| { match cc.state { - CandidateState::ELECTED => { if &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } } + CandidateState::Elected => { if &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } } _ => { acc + &cc.votes } } }); @@ -535,7 +535,7 @@ fn elect_meeting_quota(state: &mut CountState, opts: &STVOptions) let vote_req = state.vote_required_election.as_ref().unwrap(); // Have to do this or else the borrow checker gets confused let mut cands_meeting_quota: Vec<&Candidate> = state.election.candidates.iter() - .filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::HOPEFUL && meets_quota(vote_req, cc, opts) }) + .filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::Hopeful && meets_quota(vote_req, cc, opts) }) .collect(); if cands_meeting_quota.len() > 0 { @@ -545,7 +545,7 @@ fn elect_meeting_quota(state: &mut CountState, opts: &STVOptions) // Declare elected in descending order of votes for candidate in cands_meeting_quota.into_iter().rev() { let count_card = state.candidates.get_mut(candidate).unwrap(); - count_card.state = CandidateState::ELECTED; + count_card.state = CandidateState::Elected; state.num_elected += 1; count_card.order_elected = state.num_elected as isize; state.logger.log_smart( @@ -574,7 +574,7 @@ where { // Do not defer if this could change the last 2 candidates let mut hopefuls: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() - .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL || cc.state == CandidateState::GUARDED) + .filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) .collect(); hopefuls.sort_unstable_by(|(_, cc1), (_, cc2)| cc1.votes.cmp(&cc2.votes)); if total_surpluses > &(&hopefuls[1].1.votes - &hopefuls[0].1.votes) { @@ -613,7 +613,7 @@ where // Determine if surplues can be deferred if opts.defer_surpluses { if can_defer_surpluses(state, opts, &total_surpluses) { - state.logger.log_literal(format!("Distribution of surpluses totalling {:.2} votes will be deferred.", total_surpluses)); + state.logger.log_literal(format!("Distribution of surpluses totalling {:.dps$} votes will be deferred.", total_surpluses, dps=opts.pp_decimals)); return false; } } @@ -878,14 +878,14 @@ fn bulk_elect(state: &mut CountState) -> bool { // Bulk elect all remaining candidates let mut hopefuls: Vec<(&&Candidate, &mut CountCard)> = state.candidates.iter_mut() - .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) + .filter(|(_, cc)| cc.state == CandidateState::Hopeful) .collect(); // TODO: Handle ties hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); for (candidate, count_card) in hopefuls.into_iter() { - count_card.state = CandidateState::ELECTED; + count_card.state = CandidateState::Elected; state.num_elected += 1; count_card.order_elected = state.num_elected as isize; @@ -905,7 +905,7 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST let mut excluded_candidates = Vec::new(); let mut hopefuls: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() - .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) + .filter(|(_, cc)| cc.state == CandidateState::Hopeful) .collect(); // Sort by votes @@ -959,8 +959,8 @@ where // Exclude lowest ranked candidate if excluded_candidates.len() == 0 { let mut hopefuls: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() - .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) - .collect(); + .filter(|(_, cc)| cc.state == CandidateState::Hopeful) + .collect(); // Sort by votes // TODO: Handle ties @@ -990,8 +990,8 @@ where { // Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers let mut excluded_with_votes: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() - //.filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && !cc.votes.is_zero()) - .filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && cc.parcels.iter().any(|p| p.len() > 0)) + //.filter(|(_, cc)| cc.state == CandidateState::Excluded && !cc.votes.is_zero()) + .filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| p.len() > 0)) .collect(); if excluded_with_votes.len() > 0 { @@ -1031,8 +1031,8 @@ where let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); // Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??! - if count_card.state != CandidateState::EXCLUDED { - count_card.state = CandidateState::EXCLUDED; + if count_card.state != CandidateState::Excluded { + count_card.state = CandidateState::Excluded; state.num_excluded += 1; count_card.order_elected = -(order_excluded as isize); } diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 2002e12..9c7efcf 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -165,16 +165,19 @@ fn update_results_table(stage_num: usize, state: &CountState, opts result.push(&format!(r#"{}"#, state.title).into()); for candidate in state.election.candidates.iter() { let count_card = state.candidates.get(candidate).unwrap(); - if count_card.state == stv::CandidateState::ELECTED { + if count_card.state == stv::CandidateState::Elected { 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 { + } 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 if count_card.state == stv::CandidateState::Withdrawn { + result.push(&r#""#.into()); + result.push(&r#"WD"#.into()); } else { result.push(&format!(r#"{}"#, pp(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, pp(&count_card.votes, opts.pp_decimals)).into()); @@ -214,10 +217,12 @@ fn finalise_results_table(state: &CountState) -> Array { // Candidate states for candidate in state.election.candidates.iter() { let count_card = state.candidates.get(candidate).unwrap(); - if count_card.state == stv::CandidateState::ELECTED { + 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 { + } else if count_card.state == stv::CandidateState::Excluded { result.push(&format!(r#"Excluded {}"#, -count_card.order_elected).into()); + } else if count_card.state == stv::CandidateState::Withdrawn { + result.push(&r#"Withdrawn"#.into()); } else { result.push(&r#""#.into()); } @@ -232,7 +237,7 @@ fn final_result_summary(state: &CountState) -> String { let mut winners = Vec::new(); for (candidate, count_card) in state.candidates.iter() { - if count_card.state == CandidateState::ELECTED { + if count_card.state == CandidateState::Elected { winners.push((candidate, count_card.order_elected)); } } diff --git a/tests/ers97.rs b/tests/ers97.rs index aa06fee..d2c21c1 100644 --- a/tests/ers97.rs +++ b/tests/ers97.rs @@ -123,11 +123,11 @@ fn ers97_rational() { for (candidate, candidate_state) in state.election.candidates.iter().zip(candidate_states) { let count_card = state.candidates.get(candidate).unwrap(); if candidate_state == "" { - assert!(count_card.state == CandidateState::HOPEFUL); + assert!(count_card.state == CandidateState::Hopeful); } else if candidate_state == "EL" || candidate_state == "PEL" { - assert!(count_card.state == CandidateState::ELECTED); + assert!(count_card.state == CandidateState::Elected); } else if candidate_state == "EX" || candidate_state == "EXCLUDING" { - assert!(count_card.state == CandidateState::EXCLUDED); + assert!(count_card.state == CandidateState::Excluded); } else { panic!("Unknown state descriptor {}", candidate_state); } diff --git a/tests/scotland.rs b/tests/scotland.rs index 0ebec2a..d815218 100644 --- a/tests/scotland.rs +++ b/tests/scotland.rs @@ -109,11 +109,11 @@ fn scotland_linn07_fixed5() { .get_text().unwrap() .to_string(); if cand_state == "Continuing" { - assert!(count_card.state == CandidateState::HOPEFUL); + assert!(count_card.state == CandidateState::Hopeful); } else if cand_state == "Elected" { - assert!(count_card.state == CandidateState::ELECTED); + assert!(count_card.state == CandidateState::Elected); } else if cand_state == "Excluded" { - assert!(count_card.state == CandidateState::EXCLUDED); + assert!(count_card.state == CandidateState::Excluded); } else { panic!("Unknown state descriptor {}", cand_state); } diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 19b9397..81b9bff 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -104,11 +104,11 @@ fn validate_stage(idx: usize, state: &CountState, records: &Vec