Fixes to edge cases in stratify (LR) sample method

This commit is contained in:
RunasSudo 2021-09-26 02:27:37 +10:00
parent 2f7abf9f0a
commit cf75943829
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 40 additions and 21 deletions

View File

@ -135,7 +135,7 @@ where
} }
}; };
let elected_candidate = if max_cands.len() > 1 { let elected_candidate = if max_cands.len() > 1 {
super::choose_highest(state, opts, max_cands, "Which candidate's surplus to distribute?")? super::choose_highest(state, opts, &max_cands, "Which candidate's surplus to distribute?")?
} else { } else {
max_cands[0] max_cands[0]
}; };

View File

@ -1061,7 +1061,7 @@ fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOp
while !leading_hopefuls.is_empty() && state.num_elected < state.election.seats { while !leading_hopefuls.is_empty() && state.num_elected < state.election.seats {
let max_cands = ties::multiple_max_by(&leading_hopefuls, |c| &state.candidates[c].votes); let max_cands = ties::multiple_max_by(&leading_hopefuls, |c| &state.candidates[c].votes);
let candidate = if max_cands.len() > 1 { let candidate = if max_cands.len() > 1 {
choose_highest(state, opts, max_cands, "Which candidate to elect?")? choose_highest(state, opts, &max_cands, "Which candidate to elect?")?
} else { } else {
max_cands[0] max_cands[0]
}; };
@ -1109,7 +1109,7 @@ fn elect_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOption
// 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 {
choose_highest(state, opts, max_cands, "Which candidate to elect?")? choose_highest(state, opts, &max_cands, "Which candidate to elect?")?
} else { } else {
max_cands[0] max_cands[0]
}; };
@ -1207,7 +1207,7 @@ where
if num_to_exclude > 0 { if num_to_exclude > 0 {
let total_excluded = to_exclude.into_iter() let total_excluded = to_exclude.into_iter()
.fold(N::new(), |acc, c| acc + &state.candidates[c].votes); .fold(N::new(), |acc, c| acc + &state.candidates[c].votes);
if total_surpluses > &(&hopefuls[num_to_exclude + 1].1.votes - &total_excluded) { if total_surpluses > &(&hopefuls[num_to_exclude].1.votes - &total_excluded) {
return false; return false;
} }
} }
@ -1265,7 +1265,7 @@ fn do_bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions, templa
while !hopefuls.is_empty() { while !hopefuls.is_empty() {
let max_cands = ties::multiple_max_by(&hopefuls, |c| &state.candidates[c].votes); let max_cands = ties::multiple_max_by(&hopefuls, |c| &state.candidates[c].votes);
let candidate = if max_cands.len() > 1 { let candidate = if max_cands.len() > 1 {
choose_highest(state, opts, max_cands, "Which candidate to elect?")? choose_highest(state, opts, &max_cands, "Which candidate to elect?")?
} else { } else {
max_cands[0] max_cands[0]
}; };
@ -1330,7 +1330,7 @@ where
// Exclude only the lowest-ranked doomed candidate // Exclude only the lowest-ranked doomed candidate
let min_cands = ties::multiple_min_by(&doomed, |c| &state.candidates[c].votes); let min_cands = ties::multiple_min_by(&doomed, |c| &state.candidates[c].votes);
excluded_candidates = if min_cands.len() > 1 { excluded_candidates = if min_cands.len() > 1 {
vec![choose_lowest(state, opts, min_cands, "Which candidate to exclude?")?] vec![choose_lowest(state, opts, &min_cands, "Which candidate to exclude?")?]
} else { } else {
vec![min_cands[0]] vec![min_cands[0]]
}; };
@ -1468,7 +1468,7 @@ where
let min_cands = ties::multiple_min_by(&hopefuls, |c| &state.candidates[c].votes); let min_cands = ties::multiple_min_by(&hopefuls, |c| &state.candidates[c].votes);
excluded_candidates = if min_cands.len() > 1 { excluded_candidates = if min_cands.len() > 1 {
vec![choose_lowest(state, opts, min_cands, "Which candidate to exclude?")?] vec![choose_lowest(state, opts, &min_cands, "Which candidate to exclude?")?]
} else { } else {
vec![min_cands[0]] vec![min_cands[0]]
}; };
@ -1586,7 +1586,7 @@ fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
/// Break a tie between the given candidates according to [STVOptions::ties], selecting the highest candidate /// Break a tie between the given candidates according to [STVOptions::ties], selecting the highest candidate
/// ///
/// The given candidates are assumed to be tied in this round. /// The given candidates are assumed to be tied in this round.
fn choose_highest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: Vec<&'c Candidate>, prompt_text: &str) -> Result<&'c Candidate, STVError> { pub fn choose_highest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: &Vec<&'c Candidate>, prompt_text: &str) -> Result<&'c Candidate, STVError> {
for strategy in opts.ties.iter() { for strategy in opts.ties.iter() {
match strategy.choose_highest(state, opts, &candidates, prompt_text) { match strategy.choose_highest(state, opts, &candidates, prompt_text) {
Ok(c) => { Ok(c) => {
@ -1607,7 +1607,7 @@ fn choose_highest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, c
/// Break a tie between the given candidates according to [STVOptions::ties], selecting the lowest candidate /// Break a tie between the given candidates according to [STVOptions::ties], selecting the lowest candidate
/// ///
/// The given candidates are assumed to be tied in this round. /// The given candidates are assumed to be tied in this round.
fn choose_lowest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: Vec<&'c Candidate>, prompt_text: &str) -> Result<&'c Candidate, STVError> { pub fn choose_lowest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: &Vec<&'c Candidate>, prompt_text: &str) -> Result<&'c Candidate, STVError> {
for strategy in opts.ties.iter() { for strategy in opts.ties.iter() {
match strategy.choose_lowest(state, opts, &candidates, prompt_text) { match strategy.choose_lowest(state, opts, &candidates, prompt_text) {
Ok(c) => { Ok(c) => {

View File

@ -188,19 +188,38 @@ where
// Round remainders to remove loss by fraction // Round remainders to remove loss by fraction
let transferred = candidate_transfers_remainders.values().fold(N::new(), |acc, (t, _)| acc + t); let transferred = candidate_transfers_remainders.values().fold(N::new(), |acc, (t, _)| acc + t);
let loss_fraction = &surplus - &transferred; let loss_fraction = &surplus - &transferred;
if !loss_fraction.is_zero() { if !loss_fraction.is_zero() && surplus_fraction.is_some() {
let n_to_round: usize = format!("{:.0}", loss_fraction).parse().expect("Loss by fraction overflows usize"); let n_to_round: usize = format!("{:.0}", loss_fraction).parse().expect("Loss by fraction overflows usize");
let mut cands_by_remainder: Vec<Option<&Candidate>> = candidate_transfers_remainders.keys().cloned().collect(); let mut cands_by_remainder = candidate_transfers_remainders.keys().cloned().collect::<Vec<_>>();
// Sort by whole parts
// Compare b to a to sort high-low // Compare b to a to sort high-low
cands_by_remainder.sort_unstable_by(|a, b| candidate_transfers_remainders[b].1.cmp(&candidate_transfers_remainders[a].1)); cands_by_remainder.sort_unstable_by(|a, b| candidate_transfers_remainders[b].0.cmp(&candidate_transfers_remainders[a].0));
// Then sort by remainders
cands_by_remainder.sort_by(|a, b| candidate_transfers_remainders[b].1.cmp(&candidate_transfers_remainders[a].1));
// Select top remainders // Select top remainders
let top_remainders: Vec<&Option<&Candidate>> = cands_by_remainder.iter().take(n_to_round).collect(); let mut top_remainders = cands_by_remainder.iter().take(n_to_round).collect::<Vec<_>>();
// Check for tied remainders // Check for tied remainders
if candidate_transfers_remainders[top_remainders.last().unwrap()].1 == candidate_transfers_remainders[cands_by_remainder.iter().nth(n_to_round + 1).unwrap()].1 { if candidate_transfers_remainders[top_remainders.last().unwrap()] == candidate_transfers_remainders[cands_by_remainder.iter().nth(n_to_round).unwrap()] {
todo!("Tie for largest remainders"); // Get the top entry
let top_entry = &candidate_transfers_remainders[top_remainders.last().unwrap()];
// Separate out tied entries
top_remainders = top_remainders.into_iter().filter(|c| &candidate_transfers_remainders[c] != top_entry).collect();
let mut tied_top = cands_by_remainder.iter()
.filter_map(|c| if let Some(c2) = c { if &candidate_transfers_remainders[c] == top_entry { Some(*c2) } else { None } } else { None })
.collect::<Vec<_>>();
// Get top entries by tie-breaking method
for _ in 0..(n_to_round-top_remainders.len()) {
let cand = super::choose_highest(state, opts, &tied_top, "Which fraction to round up?")?;
tied_top.remove(tied_top.iter().position(|c| *c == cand).unwrap());
top_remainders.push(cands_by_remainder.iter().find(|c| **c == Some(cand)).unwrap());
}
} }
// Round up top remainders // Round up top remainders

View File

@ -309,7 +309,7 @@ impl STVOptions {
// Reporting // Reporting
/// Generate the lead-in description of the count in HTML /// Generate the lead-in description of the count in HTML
fn describe_count<N: Number>(filename: String, election: &Election<N>, opts: &stv::STVOptions) -> String { pub fn describe_count<N: Number>(filename: String, election: &Election<N>, opts: &stv::STVOptions) -> String {
let mut result = String::from("<p>Count computed by OpenTally (revision "); let mut result = String::from("<p>Count computed by OpenTally (revision ");
result.push_str(crate::VERSION); result.push_str(crate::VERSION);
let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value }); let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value });
@ -326,7 +326,7 @@ fn describe_count<N: Number>(filename: String, election: &Election<N>, opts: &st
} }
/// Generate the first column of the HTML results table /// Generate the first column of the HTML results table
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions, report_style: &str) -> String { pub fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions, report_style: &str) -> String {
let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#); let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
if report_style == "ballots_votes" { if report_style == "ballots_votes" {
@ -361,7 +361,7 @@ fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions,
} }
/// Generate subsequent columns of the HTML results table /// Generate subsequent columns of the HTML results table
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions, report_style: &str) -> Array { pub fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions, report_style: &str) -> Array {
let result = Array::new(); let result = Array::new();
// Insert borders to left of new exclusions in Wright STV // Insert borders to left of new exclusions in Wright STV
@ -621,7 +621,7 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
} }
/// Get the comment for the current stage /// Get the comment for the current stage
fn update_stage_comments<N: Number>(state: &CountState<N>, stage_num: usize) -> String { pub fn update_stage_comments<N: Number>(state: &CountState<N>, stage_num: usize) -> String {
let mut comments = state.logger.render().join(" "); let mut comments = state.logger.render().join(" ");
if let Some(_) = state.transfer_table { if let Some(_) = state.transfer_table {
comments.push_str(&format!(r##" <a href="#" class="detailedTransfersLink" onclick="viewDetailedTransfers({});return false;">[View detailed transfers]</a>"##, stage_num)); comments.push_str(&format!(r##" <a href="#" class="detailedTransfersLink" onclick="viewDetailedTransfers({});return false;">[View detailed transfers]</a>"##, stage_num));
@ -630,7 +630,7 @@ fn update_stage_comments<N: Number>(state: &CountState<N>, stage_num: usize) ->
} }
/// Generate the final column of the HTML results table /// Generate the final column of the HTML results table
fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Array { pub fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Array {
let result = Array::new(); let result = Array::new();
// Header rows // Header rows
@ -673,7 +673,7 @@ fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str)
} }
/// Generate the final lead-out text summarising the result of the election /// Generate the final lead-out text summarising the result of the election
fn final_result_summary<N: Number>(state: &CountState<N>, opts: &stv::STVOptions) -> String { pub fn final_result_summary<N: Number>(state: &CountState<N>, opts: &stv::STVOptions) -> String {
let mut result = String::from("<p>Count complete. The winning candidates are, in order of election:</p><ol>"); let mut result = String::from("<p>Count complete. The winning candidates are, in order of election:</p><ol>");
let mut winners = Vec::new(); let mut winners = Vec::new();