Fixes to edge cases in stratify (LR) sample method
This commit is contained in:
parent
2f7abf9f0a
commit
cf75943829
|
@ -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]
|
||||||
};
|
};
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue