Handle withdrawn candidates

This commit is contained in:
RunasSudo 2021-06-12 00:50:01 +10:00
parent 59539d807a
commit 4c4099ee22
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
7 changed files with 64 additions and 38 deletions

View File

@ -24,6 +24,7 @@ pub struct Election<N> {
pub name: String, pub name: String,
pub seats: usize, pub seats: usize,
pub candidates: Vec<Candidate>, pub candidates: Vec<Candidate>,
pub withdrawn_candidates: Vec<usize>,
pub ballots: Vec<Ballot<N>>, pub ballots: Vec<Ballot<N>>,
} }
@ -40,6 +41,7 @@ impl<N: Number> Election<N> {
name: String::new(), name: String::new(),
seats: seats, seats: seats,
candidates: Vec::with_capacity(num_candidates), candidates: Vec::with_capacity(num_candidates),
withdrawn_candidates: Vec::new(),
ballots: Vec::new(), ballots: Vec::new(),
}; };
@ -50,6 +52,16 @@ impl<N: Number> Election<N> {
} }
let mut bits = line.split(" "); let mut bits = line.split(" ");
if line.starts_with("-") {
// Withdrawn candidates
for bit in bits.into_iter() {
let val = bit[1..bit.len()].parse::<usize>().expect("Syntax Error");
election.withdrawn_candidates.push(val - 1);
}
continue;
}
let value = N::parse(bits.next().expect("Syntax Error")); let value = N::parse(bits.next().expect("Syntax Error"));
let mut ballot = Ballot { let mut ballot = Ballot {
@ -150,6 +162,10 @@ impl<'a, N: Number> CountState<'a, N> {
state.candidates.insert(candidate, CountCard::new()); 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; return state;
} }
@ -203,7 +219,7 @@ pub struct CountCard<'a, N> {
impl<'a, N: Number> CountCard<'a, N> { impl<'a, N: Number> CountCard<'a, N> {
pub fn new() -> Self { pub fn new() -> Self {
return CountCard { return CountCard {
state: CandidateState::HOPEFUL, state: CandidateState::Hopeful,
order_elected: 0, order_elected: 0,
orig_votes: N::new(), orig_votes: N::new(),
transfers: N::new(), transfers: N::new(),
@ -245,9 +261,10 @@ pub struct Ballot<N> {
#[derive(PartialEq)] #[derive(PartialEq)]
#[derive(Clone)] #[derive(Clone)]
pub enum CandidateState { pub enum CandidateState {
HOPEFUL, Hopeful,
GUARDED, Guarded,
ELECTED, Elected,
DOOMED, Doomed,
EXCLUDED, Withdrawn,
Excluded,
} }

View File

@ -219,7 +219,7 @@ where
let mut winners = Vec::new(); let mut winners = Vec::new();
for (candidate, count_card) in state.candidates.iter() { 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)); winners.push((candidate, count_card.order_elected));
} }
} }
@ -232,13 +232,17 @@ where
fn print_candidates<'a, N: 'a + Number, I: Iterator<Item=(&'a Candidate, &'a CountCard<'a, N>)>>(candidates: I, cmd_opts: &STV) { fn print_candidates<'a, N: 'a + Number, I: Iterator<Item=(&'a Candidate, &'a CountCard<'a, N>)>>(candidates: I, cmd_opts: &STV) {
for (candidate, count_card) in candidates { 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); 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 --hide-excluded, hide unless nonzero votes or nonzero transfers
if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() { 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); 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 { } else {
println!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals); println!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
} }

View File

@ -349,7 +349,7 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a
let candidate = &state.election.candidates[*preference]; let candidate = &state.election.candidates[*preference];
let count_card = state.candidates.get(candidate).unwrap(); let count_card = state.candidates.get(candidate).unwrap();
if let CandidateState::HOPEFUL | CandidateState::GUARDED = count_card.state { if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state {
next_candidate = Some(candidate); next_candidate = Some(candidate);
vote.up_to_pref = i + 1; vote.up_to_pref = i + 1;
break; break;
@ -486,7 +486,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
// Calculate total active vote // Calculate total active vote
let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| { let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
match cc.state { 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 } _ => { acc + &cc.votes }
} }
}); });
@ -535,7 +535,7 @@ fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
let vote_req = state.vote_required_election.as_ref().unwrap(); // Have to do this or else the borrow checker gets confused 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() 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(); .collect();
if cands_meeting_quota.len() > 0 { if cands_meeting_quota.len() > 0 {
@ -545,7 +545,7 @@ fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
// Declare elected in descending order of votes // Declare elected in descending order of votes
for candidate in cands_meeting_quota.into_iter().rev() { for candidate in cands_meeting_quota.into_iter().rev() {
let count_card = state.candidates.get_mut(candidate).unwrap(); let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.state = CandidateState::ELECTED; count_card.state = CandidateState::Elected;
state.num_elected += 1; state.num_elected += 1;
count_card.order_elected = state.num_elected as isize; count_card.order_elected = state.num_elected as isize;
state.logger.log_smart( state.logger.log_smart(
@ -574,7 +574,7 @@ where
{ {
// Do not defer if this could change the last 2 candidates // Do not defer if this could change the last 2 candidates
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter() let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL || cc.state == CandidateState::GUARDED) .filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded)
.collect(); .collect();
hopefuls.sort_unstable_by(|(_, cc1), (_, cc2)| cc1.votes.cmp(&cc2.votes)); hopefuls.sort_unstable_by(|(_, cc1), (_, cc2)| cc1.votes.cmp(&cc2.votes));
if total_surpluses > &(&hopefuls[1].1.votes - &hopefuls[0].1.votes) { if total_surpluses > &(&hopefuls[1].1.votes - &hopefuls[0].1.votes) {
@ -613,7 +613,7 @@ where
// Determine if surplues can be deferred // Determine if surplues can be deferred
if opts.defer_surpluses { if opts.defer_surpluses {
if can_defer_surpluses(state, opts, &total_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; return false;
} }
} }
@ -878,14 +878,14 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
// Bulk elect all remaining candidates // Bulk elect all remaining candidates
let mut hopefuls: Vec<(&&Candidate, &mut CountCard<N>)> = state.candidates.iter_mut() let mut hopefuls: Vec<(&&Candidate, &mut CountCard<N>)> = state.candidates.iter_mut()
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) .filter(|(_, cc)| cc.state == CandidateState::Hopeful)
.collect(); .collect();
// TODO: Handle ties // TODO: Handle ties
hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
for (candidate, count_card) in hopefuls.into_iter() { for (candidate, count_card) in hopefuls.into_iter() {
count_card.state = CandidateState::ELECTED; count_card.state = CandidateState::Elected;
state.num_elected += 1; state.num_elected += 1;
count_card.order_elected = state.num_elected as isize; 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 excluded_candidates = Vec::new();
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter() let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) .filter(|(_, cc)| cc.state == CandidateState::Hopeful)
.collect(); .collect();
// Sort by votes // Sort by votes
@ -959,8 +959,8 @@ where
// Exclude lowest ranked candidate // Exclude lowest ranked candidate
if excluded_candidates.len() == 0 { if excluded_candidates.len() == 0 {
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter() let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) .filter(|(_, cc)| cc.state == CandidateState::Hopeful)
.collect(); .collect();
// Sort by votes // Sort by votes
// TODO: Handle ties // 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 // 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<N>)> = state.candidates.iter() let mut excluded_with_votes: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
//.filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && !cc.votes.is_zero()) //.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.parcels.iter().any(|p| p.len() > 0))
.collect(); .collect();
if excluded_with_votes.len() > 0 { if excluded_with_votes.len() > 0 {
@ -1031,8 +1031,8 @@ where
let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??! // Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
if count_card.state != CandidateState::EXCLUDED { if count_card.state != CandidateState::Excluded {
count_card.state = CandidateState::EXCLUDED; count_card.state = CandidateState::Excluded;
state.num_excluded += 1; state.num_excluded += 1;
count_card.order_elected = -(order_excluded as isize); count_card.order_elected = -(order_excluded as isize);
} }

View File

@ -165,16 +165,19 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
result.push(&format!(r#"<td>{}</td>"#, state.title).into()); result.push(&format!(r#"<td>{}</td>"#, state.title).into());
for candidate in state.election.candidates.iter() { for candidate in state.election.candidates.iter() {
let count_card = state.candidates.get(candidate).unwrap(); 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#"<td class="count elected">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"<td class="count elected">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="count elected">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into()); result.push(&format!(r#"<td class="count elected">{}</td>"#, 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#"<td class="count excluded">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"<td class="count excluded">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
if count_card.votes.is_zero() { if count_card.votes.is_zero() {
result.push(&r#"<td class="count excluded">Ex</td>"#.into()); result.push(&r#"<td class="count excluded">Ex</td>"#.into());
} else { } else {
result.push(&format!(r#"<td class="count excluded">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into()); result.push(&format!(r#"<td class="count excluded">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into());
} }
} else if count_card.state == stv::CandidateState::Withdrawn {
result.push(&r#"<td class="count excluded"></td>"#.into());
result.push(&r#"<td class="count excluded">WD</td>"#.into());
} else { } else {
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"<td class="count">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into()); result.push(&format!(r#"<td class="count">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into());
@ -214,10 +217,12 @@ fn finalise_results_table<N: Number>(state: &CountState<N>) -> Array {
// Candidate states // Candidate states
for candidate in state.election.candidates.iter() { for candidate in state.election.candidates.iter() {
let count_card = state.candidates.get(candidate).unwrap(); 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#"<td rowspan="2" class="bb elected">ELECTED {}</td>"#, count_card.order_elected).into()); result.push(&format!(r#"<td rowspan="2" class="bb elected">ELECTED {}</td>"#, 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#"<td rowspan="2" class="bb excluded">Excluded {}</td>"#, -count_card.order_elected).into()); result.push(&format!(r#"<td rowspan="2" class="bb excluded">Excluded {}</td>"#, -count_card.order_elected).into());
} else if count_card.state == stv::CandidateState::Withdrawn {
result.push(&r#"<td rowspan="2" class="bb excluded">Withdrawn</td>"#.into());
} else { } else {
result.push(&r#"<td rowspan="2" class="bb"></td>"#.into()); result.push(&r#"<td rowspan="2" class="bb"></td>"#.into());
} }
@ -232,7 +237,7 @@ fn final_result_summary<N: Number>(state: &CountState<N>) -> String {
let mut winners = Vec::new(); let mut winners = Vec::new();
for (candidate, count_card) in state.candidates.iter() { 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)); winners.push((candidate, count_card.order_elected));
} }
} }

View File

@ -123,11 +123,11 @@ fn ers97_rational() {
for (candidate, candidate_state) in state.election.candidates.iter().zip(candidate_states) { for (candidate, candidate_state) in state.election.candidates.iter().zip(candidate_states) {
let count_card = state.candidates.get(candidate).unwrap(); let count_card = state.candidates.get(candidate).unwrap();
if candidate_state == "" { if candidate_state == "" {
assert!(count_card.state == CandidateState::HOPEFUL); assert!(count_card.state == CandidateState::Hopeful);
} else if candidate_state == "EL" || candidate_state == "PEL" { } 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" { } else if candidate_state == "EX" || candidate_state == "EXCLUDING" {
assert!(count_card.state == CandidateState::EXCLUDED); assert!(count_card.state == CandidateState::Excluded);
} else { } else {
panic!("Unknown state descriptor {}", candidate_state); panic!("Unknown state descriptor {}", candidate_state);
} }

View File

@ -109,11 +109,11 @@ fn scotland_linn07_fixed5() {
.get_text().unwrap() .get_text().unwrap()
.to_string(); .to_string();
if cand_state == "Continuing" { if cand_state == "Continuing" {
assert!(count_card.state == CandidateState::HOPEFUL); assert!(count_card.state == CandidateState::Hopeful);
} else if cand_state == "Elected" { } else if cand_state == "Elected" {
assert!(count_card.state == CandidateState::ELECTED); assert!(count_card.state == CandidateState::Elected);
} else if cand_state == "Excluded" { } else if cand_state == "Excluded" {
assert!(count_card.state == CandidateState::EXCLUDED); assert!(count_card.state == CandidateState::Excluded);
} else { } else {
panic!("Unknown state descriptor {}", cand_state); panic!("Unknown state descriptor {}", cand_state);
} }

View File

@ -104,11 +104,11 @@ fn validate_stage<N: Number>(idx: usize, state: &CountState<N>, records: &Vec<St
for (candidate, candidate_state) in state.election.candidates.iter().zip(candidate_states) { for (candidate, candidate_state) in state.election.candidates.iter().zip(candidate_states) {
let count_card = state.candidates.get(candidate).unwrap(); let count_card = state.candidates.get(candidate).unwrap();
if candidate_state == "" { if candidate_state == "" {
assert!(count_card.state == CandidateState::HOPEFUL); assert!(count_card.state == CandidateState::Hopeful);
} else if candidate_state == "EL" || candidate_state == "PEL" { } 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" { } else if candidate_state == "EX" || candidate_state == "EXCLUDING" {
assert!(count_card.state == CandidateState::EXCLUDED); assert!(count_card.state == CandidateState::Excluded);
} else { } else {
panic!("Unknown state descriptor {}", candidate_state); panic!("Unknown state descriptor {}", candidate_state);
} }