Fix bug excluding-by-value/source candidates with no votes
Fix bug electing too many candidates if more reach the quota than vacancies remain
Add regression test
This commit is contained in:
RunasSudo 2021-09-09 01:19:31 +10:00
parent e4bfe45f49
commit 260dee1bb5
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 69 additions and 6 deletions

View File

@ -496,7 +496,9 @@ where
} }
ExclusionMethod::ByValue => { ExclusionMethod::ByValue => {
// Exclude by value // Exclude by value
let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter().filter(|c| !state.candidates[*c].finalised).collect(); let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter()
.filter(|c| { let cc = &state.candidates[*c]; !cc.finalised && !cc.parcels.is_empty() })
.collect();
if excluded_with_votes.is_empty() { if excluded_with_votes.is_empty() {
votes_remain = false; votes_remain = false;
@ -554,7 +556,9 @@ where
} }
ExclusionMethod::BySource => { ExclusionMethod::BySource => {
// Exclude by source candidate // Exclude by source candidate
let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter().filter(|c| !state.candidates[*c].finalised).collect(); let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter()
.filter(|c| { let cc = &state.candidates[*c]; !cc.finalised && !cc.parcels.is_empty() })
.collect();
if excluded_with_votes.is_empty() { if excluded_with_votes.is_empty() {
votes_remain = false; votes_remain = false;

View File

@ -939,11 +939,12 @@ fn meets_vre<N: Number>(state: &CountState<N>, count_card: &CountCard<N>, opts:
/// ///
/// Returns `true` if any candidates were elected. /// Returns `true` if any candidates were elected.
fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> { fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> {
let num_vacancies = state.election.seats - state.num_elected; if state.num_elected >= state.election.seats {
if num_vacancies == 0 {
return Ok(false); return Ok(false);
} }
let num_vacancies = state.election.seats - state.num_elected;
let mut hopefuls: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() let mut hopefuls: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter()
.map(|c| (c, &state.candidates[c])) .map(|c| (c, &state.candidates[c]))
.filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) .filter(|(_, cc)| cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded)
@ -1033,7 +1034,7 @@ fn elect_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOption
let elected = !cands_meeting_quota.is_empty(); let elected = !cands_meeting_quota.is_empty();
while !cands_meeting_quota.is_empty() { while !cands_meeting_quota.is_empty() && state.num_elected < state.election.seats {
// Declare elected in descending order of votes // Declare elected in descending order of votes
let max_cands = ties::multiple_max_by(&cands_meeting_quota, |c| &state.candidates[c].votes); let max_cands = ties::multiple_max_by(&cands_meeting_quota, |c| &state.candidates[c].votes);
let candidate = if max_cands.len() > 1 { let candidate = if max_cands.len() > 1 {

17
tests/data/A33.blt Normal file
View File

@ -0,0 +1,17 @@
# Comment: BY STV
# Comment: Number of truncated papers: 0
# Comment: AKA R038 in Wichmann's database
# Source: Nicolaus Tideman via Warren D Smith <https://rangevoting.org/TidemanData.html>
# Contributor: RunasSudo
18 3
1 15 9 5 6 8 0
1 18 7 16 1 12 14 0
1 14 9 17 13 15 5 2 12 16 8 0
1 6 5 3 4 15 12 9 7 1 14 13 16 18 17 8 2 10 11 0
1 12 1 17 9 6 5 2 0
1 15 8 5 3 0
1 8 15 2 3 5 4 1 9 0
1 3 5 1 4 11 2 8 0
1 17 4 9 14 16 6 1 13 8 15 3 18 5 12 11 10 2 7 0
0
"A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "f:a33"

View File

@ -17,12 +17,13 @@
mod utils; mod utils;
use opentally::election::{CountState, Election}; use opentally::election::{CandidateState, CountState, Election};
use opentally::numbers::Rational; use opentally::numbers::Rational;
use opentally::parser::blt; use opentally::parser::blt;
use opentally::stv; use opentally::stv;
use opentally::ties::TieStrategy; use opentally::ties::TieStrategy;
/// Insufficient candidates to fill all vacancies
#[test] #[test]
fn insufficient_candidates1() { fn insufficient_candidates1() {
let stv_opts = stv::STVOptionsBuilder::default() let stv_opts = stv::STVOptionsBuilder::default()
@ -48,6 +49,7 @@ fn insufficient_candidates1() {
} }
} }
/// After bulk exclusion of candidates with no votes, insufficient candidates remain to fill all vacancies
#[test] #[test]
fn insufficient_candidates2() { fn insufficient_candidates2() {
let stv_opts = stv::STVOptionsBuilder::default() let stv_opts = stv::STVOptionsBuilder::default()
@ -72,3 +74,42 @@ fn insufficient_candidates2() {
} }
} }
} }
/// Tideman A33 election: exclusion of candidate with no votes; more candidates reach the quota than vacancies
#[test]
fn tideman_a33_ers97_rational() {
let stv_opts = stv::STVOptionsBuilder::default()
.round_surplus_fractions(Some(2))
.round_values(Some(2))
.round_votes(Some(2))
.round_quota(Some(2))
.quota(stv::QuotaType::DroopExact)
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
.quota_mode(stv::QuotaMode::ERS97)
.ties(vec![TieStrategy::Random(String::new("20210908"))])
.surplus(stv::SurplusMethod::EG)
.transferable_only(true)
.exclusion(stv::ExclusionMethod::ByValue)
.early_bulk_elect(false)
.bulk_exclude(true)
.defer_surpluses(true)
.build().unwrap();
let mut election: Election<Rational> = blt::parse_path("tests/data/A33.blt").expect("Syntax Error");
stv::preprocess_election(&mut election, &stv_opts);
let mut state = CountState::new(&election);
stv::count_init(&mut state, &stv_opts).unwrap();
loop {
let result = stv::count_one_stage::<Rational>(&mut state, &stv_opts);
match result {
Ok(done) => { if done { break; } }
Err(err) => { panic!("{}", err); }
}
}
let num_winners = state.candidates.values().filter(|cc| cc.state == CandidateState::Elected).count();
assert_eq!(num_winners, 3);
}