diff --git a/docs/options.md b/docs/options.md index 7ae227f..cd577ee 100644 --- a/docs/options.md +++ b/docs/options.md @@ -154,6 +154,7 @@ When early bulk election is enabled (default), the count terminates as soon as t * At the beginning of each stage, if the number of not-excluded candidates exactly equals the number of vacancies to fill, all remaining candidates are declared elected in a single stage. This is typical of most STV rules. * If a proposed exclusion would cause the number of not-excluded candidates to exactly equal the number of vacancies, all remaining candidates are declared elected without transfers arising from the proposed exclusion being performed. +* At the end of any stage, if only 1 vacancy remains and one continuing candidate has more votes than all other continuing candidates (plus votes awaiting transfer), that candidate is immediately declared elected. If an early bulk election is performed, further surplus distributions are not performed, and outstanding exclusions, if any, are not completed, even if they could change the order of election. diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 4b62d80..036e2c7 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -655,6 +655,42 @@ fn total_to_quota(mut total: N, seats: usize, opts: &STVOptions) -> N return total; } +/// Update vote required for election according to ERS97 rules +/// +/// This is also used to compute the vote required for election when early bulk election is enabled and 1 vacancy remains. +fn update_vre(state: &mut CountState, opts: &STVOptions) { + let mut log = String::new(); + + // 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 } } + _ => { acc + &cc.votes } + } + }); + 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 + + if &vote_req < state.quota.as_ref().unwrap() { + // VRE is less than the quota + if let Some(v) = &state.vote_required_election { + if &vote_req != v { + log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str()); + state.vote_required_election = Some(vote_req); + state.logger.log_literal(log); + } + } else { + log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str()); + state.vote_required_election = Some(vote_req); + state.logger.log_literal(log); + } + } else { + // VRE is not less than the quota, so use the quota + state.vote_required_election = state.quota.clone(); + } +} + /// Calculate the quota according to [STVOptions::quota] fn calculate_quota(state: &mut CountState, opts: &STVOptions) { // Calculate quota @@ -676,9 +712,9 @@ fn calculate_quota(state: &mut CountState, opts: &STVOptions) { // ERS97/ERS76 rules // ------------------------- - // Reduce quota if allowable + // (ERS97) Reduce quota if allowable - if state.num_elected == 0 { + if opts.quota_mode == QuotaMode::ERS97 && state.num_elected == 0 { let mut log = String::new(); // Calculate the total vote @@ -698,41 +734,23 @@ fn calculate_quota(state: &mut CountState, opts: &STVOptions) { // Calculate vote required for election if state.num_elected < state.election.seats { - let mut log = String::new(); - - // 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 } } - _ => { acc + &cc.votes } - } - }); - 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); - - if &vote_req < state.quota.as_ref().unwrap() { - // VRE is less than the quota - if let Some(v) = &state.vote_required_election { - if &vote_req != v { - log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str()); - state.vote_required_election = Some(vote_req); - state.logger.log_literal(log); - } - } else { - log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str()); - state.vote_required_election = Some(vote_req); - state.logger.log_literal(log); - } - } else { - // VRE is not less than the quota, so use the quota - state.vote_required_election = state.quota.clone(); - } + update_vre(state, opts); } } else { // No ERS97/ERS76 rules - if state.vote_required_election.is_none() || opts.surplus == SurplusMethod::Meek { + if opts.surplus == SurplusMethod::Meek { + // Update quota and so VRE every stage state.vote_required_election = state.quota.clone(); + } else { + if opts.early_bulk_elect && state.num_elected + 1 == state.election.seats { + // 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(); + } + } } } } @@ -777,11 +795,21 @@ 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; - state.logger.log_smart( - "{} meets the quota and is elected.", - "{} meet the quota and are elected.", - vec![&candidate.name] - ); + if meets_quota(state.quota.as_ref().unwrap(), count_card, opts) { + // Elected with a quota + state.logger.log_smart( + "{} meets the quota and is elected.", + "{} meet the quota and are elected.", + vec![&candidate.name] + ); + } else { + // Elected with vote required + state.logger.log_smart( + "{} meets the vote required and is elected.", + "{} meet the vote required and are elected.", + vec![&candidate.name] + ); + } if constraints::update_constraints(state, opts) { // Recheck as some candidates may have been doomed diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 9950003..3c83cdf 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -285,6 +285,11 @@ fn describe_count(filename: String, election: &Election, opts: &st return result; } +#[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; +} + /// Generate the first column of the HTML results table fn init_results_table(election: &Election, opts: &stv::STVOptions) -> String { let mut result = String::from(r#""#); @@ -292,7 +297,7 @@ fn init_results_table(election: &Election, opts: &stv::STVOptions) result.push_str(&format!(r#"{}"#, candidate.name)); } result.push_str(r#"ExhaustedLoss by fractionTotalQuota"#); - if opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76 { + if should_show_vre(opts) { result.push_str(r#"Vote required for election"#); } return result; @@ -354,7 +359,7 @@ fn update_results_table(stage_num: usize, state: &CountState, opts result.push(&format!(r#"{}"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into()); result.push(&format!(r#"{}"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into()); - if opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76 { + if should_show_vre(opts) { result.push(&format!(r#"{}"#, tdclasses2, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into()); }