Correctly compute vote required for election when using different quotas/quota criteria

This commit is contained in:
RunasSudo 2021-07-21 13:43:16 +10:00
parent b5ee76f159
commit 2ef7bf24f2
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 33 additions and 16 deletions

View File

@ -165,7 +165,7 @@ pub struct CountState<'a, N: Number> {
pub quota: Option<N>, pub quota: Option<N>,
/// Vote required for election /// 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<N>, pub vote_required_election: Option<N>,
/// Number of candidates who have been declared elected /// Number of candidates who have been declared elected

View File

@ -670,7 +670,8 @@ fn update_vre<N: Number>(state: &mut CountState<N>, 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()); 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() { if &vote_req < state.quota.as_ref().unwrap() {
// VRE is less than the quota // VRE is less than the quota
@ -746,17 +747,14 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
// Early bulk election and one seat remains: VRE is majority of total active vote // Early bulk election and one seat remains: VRE is majority of total active vote
update_vre(state, opts); update_vre(state, opts);
} else { } else {
// VRE is quota // No use of VRE
if state.vote_required_election.is_none() {
state.vote_required_election = state.quota.clone();
}
} }
} }
} }
} }
/// Determine if the given candidate meets the quota, according to [STVOptions::quota_criterion] /// Compare the candidate's votes with the specified target according to [STVOptions::quota_criterion]
fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOptions) -> bool { fn cmp_quota_criterion<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
match opts.quota_criterion { match opts.quota_criterion {
QuotaCriterion::GreaterOrEqual => { QuotaCriterion::GreaterOrEqual => {
return count_card.votes >= *quota; return count_card.votes >= *quota;
@ -767,13 +765,26 @@ fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>, 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<N: Number>(state: &CountState<N>, count_card: &CountCard<N>, 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 /// Declare elected all candidates meeting the quota
fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> { fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> {
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<N>)> = state.election.candidates.iter() // Present in order in case of tie let mut cands_meeting_quota: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
.map(|c| (c, &state.candidates[c])) .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(); .collect();
// Sort by votes // 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; 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;
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 // Elected with a quota
state.logger.log_smart( state.logger.log_smart(
"{} meets the quota and is elected.", "{} 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 // Recheck as some candidates may have been doomed
let mut cmq: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie let mut cmq: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
.map(|c| (c, &state.candidates[c])) .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(); .collect();
cmq.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes)); cmq.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
cands_meeting_quota = cmq.iter().map(|(c, _)| *c).collect(); cands_meeting_quota = cmq.iter().map(|(c, _)| *c).collect();

View File

@ -287,7 +287,8 @@ fn describe_count<N: Number>(filename: String, election: &Election<N>, opts: &st
#[inline] #[inline]
fn should_show_vre(opts: &stv::STVOptions) -> bool { 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 /// Generate the first column of the HTML results table
@ -360,7 +361,11 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into()); result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
if should_show_vre(opts) { if should_show_vre(opts) {
result.push(&format!(r#"<td class="{}count">{}</td>"#, 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#"<td class="{}count">{}</td>"#, tdclasses2, pp(vre, opts.pp_decimals)).into());
} else {
result.push(&format!(r#"<td class="{}count"></td>"#, tdclasses2).into());
}
} }
return result; return result;

View File

@ -118,7 +118,8 @@ where
&"lbf" => approx_eq(&state.loss_fraction.votes, &votes, cmp_dps, idx, "LBF"), &"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"), &"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"), &"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"), _ => panic!("Unknown sum_rows"),
} }
} }