From 2ef7bf24f295c19f0dfd394c257b86837ffd9de9 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 Jul 2021 13:43:16 +1000 Subject: [PATCH] Correctly compute vote required for election when using different quotas/quota criteria --- src/election.rs | 2 +- src/stv/mod.rs | 35 +++++++++++++++++++++++------------ src/stv/wasm.rs | 9 +++++++-- tests/utils/mod.rs | 3 ++- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/election.rs b/src/election.rs index 69a3997..e5a14dc 100644 --- a/src/election.rs +++ b/src/election.rs @@ -165,7 +165,7 @@ pub struct CountState<'a, N: Number> { pub quota: Option, /// Vote required for election /// - /// With a static quota, this is equal to the quota. With ERS97 rules, this may vary from the quota. + /// Only used in ERS97/ERS76, or if early bulk election is enabled and there is 1 vacancy remaining. pub vote_required_election: Option, /// Number of candidates who have been declared elected diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 036e2c7..4c57aff 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -670,7 +670,8 @@ fn update_vre(state: &mut CountState, opts: &STVOptions) { }); log.push_str(format!("Total active vote is {:.dps$}, so the vote required for election is ", total_active_vote, dps=opts.pp_decimals).as_str()); - let vote_req = total_to_quota(total_active_vote, state.election.seats - state.num_elected, opts); // FIXME: This is incorrect + //let vote_req = total_to_quota(total_active_vote, state.election.seats - state.num_elected, opts); + let vote_req = total_active_vote / N::from(state.election.seats - state.num_elected + 1); if &vote_req < state.quota.as_ref().unwrap() { // VRE is less than the quota @@ -746,17 +747,14 @@ fn calculate_quota(state: &mut CountState, opts: &STVOptions) { // Early bulk election and one seat remains: VRE is majority of total active vote update_vre(state, opts); } else { - // VRE is quota - if state.vote_required_election.is_none() { - state.vote_required_election = state.quota.clone(); - } + // No use of VRE } } } } -/// Determine if the given candidate meets the quota, according to [STVOptions::quota_criterion] -fn meets_quota(quota: &N, count_card: &CountCard, opts: &STVOptions) -> bool { +/// Compare the candidate's votes with the specified target according to [STVOptions::quota_criterion] +fn cmp_quota_criterion(quota: &N, count_card: &CountCard, opts: &STVOptions) -> bool { match opts.quota_criterion { QuotaCriterion::GreaterOrEqual => { return count_card.votes >= *quota; @@ -767,13 +765,26 @@ fn meets_quota(quota: &N, count_card: &CountCard, opts: &STVOption } } +/// Determine if the given candidate meets the vote required to be elected, according to [STVOptions::quota_criterion] and [STVOptions::quota_mode] +fn meets_vre(state: &CountState, count_card: &CountCard, opts: &STVOptions) -> bool { + if let Some(vre) = &state.vote_required_election { + if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 { + // VRE is set because ERS97/ERS76 rules + return cmp_quota_criterion(vre, count_card, opts); + } else { + // VRE is set because early bulk election is enabled and 1 vacancy remains + return count_card.votes > *vre; + } + } else { + return cmp_quota_criterion(state.quota.as_ref().unwrap(), count_card, opts); + } +} + /// Declare elected all candidates meeting the quota fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result { - let vote_req = state.vote_required_election.as_ref().unwrap().clone(); // Have to do this or else the borrow checker gets confused - let mut cands_meeting_quota: Vec<(&Candidate, &CountCard)> = state.election.candidates.iter() // Present in order in case of tie .map(|c| (c, &state.candidates[c])) - .filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_quota(&vote_req, cc, opts) }) + .filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_vre(state, cc, opts) }) .collect(); // Sort by votes @@ -795,7 +806,7 @@ fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVO count_card.state = CandidateState::Elected; state.num_elected += 1; count_card.order_elected = state.num_elected as isize; - if meets_quota(state.quota.as_ref().unwrap(), count_card, opts) { + if cmp_quota_criterion(state.quota.as_ref().unwrap(), count_card, opts) { // Elected with a quota state.logger.log_smart( "{} meets the quota and is elected.", @@ -815,7 +826,7 @@ fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVO // Recheck as some candidates may have been doomed let mut cmq: Vec<(&Candidate, &CountCard)> = state.election.candidates.iter() // Present in order in case of tie .map(|c| (c, &state.candidates[c])) - .filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_quota(&vote_req, cc, opts) }) + .filter(|(_, cc)| { (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_vre(state, cc, opts) }) .collect(); cmq.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes)); cands_meeting_quota = cmq.iter().map(|(c, _)| *c).collect(); diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 3c83cdf..272ac9d 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -287,7 +287,8 @@ fn describe_count(filename: String, election: &Election, opts: &st #[inline] fn should_show_vre(opts: &stv::STVOptions) -> bool { - return opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76 || opts.early_bulk_elect; + //return opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76 || opts.early_bulk_elect; + return opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76; } /// Generate the first column of the HTML results table @@ -360,7 +361,11 @@ fn update_results_table(stage_num: usize, state: &CountState, opts result.push(&format!(r#"{}"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into()); if should_show_vre(opts) { - result.push(&format!(r#"{}"#, tdclasses2, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into()); + if let Some(vre) = &state.vote_required_election { + result.push(&format!(r#"{}"#, tdclasses2, pp(vre, opts.pp_decimals)).into()); + } else { + result.push(&format!(r#""#, tdclasses2).into()); + } } return result; diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 062d316..3e4599a 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -118,7 +118,8 @@ where &"lbf" => approx_eq(&state.loss_fraction.votes, &votes, cmp_dps, idx, "LBF"), &"nt" => approx_eq(&(&state.exhausted.votes + &state.loss_fraction.votes), &votes, cmp_dps, idx, "NTs"), &"quota" => approx_eq(state.quota.as_ref().unwrap(), &votes, cmp_dps, idx, "quota"), - &"vre" => approx_eq(state.vote_required_election.as_ref().unwrap(), &votes, cmp_dps, idx, "VRE"), + //&"vre" => approx_eq(state.vote_required_election.as_ref().unwrap(), &votes, cmp_dps, idx, "VRE"), + &"vre" => approx_eq(state.vote_required_election.as_ref().unwrap(), &votes, Some(2), idx, "VRE"), _ => panic!("Unknown sum_rows"), } }