From 2dc5ed963b8192e81a3ceacd3500b6b2ffbc8689 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 13 Jun 2021 00:15:14 +1000 Subject: [PATCH] Implement forwards and backwards tie-breaking --- src/election.rs | 5 + src/main.rs | 5 + src/stv/mod.rs | 268 ++++++++++++++++++++++++++++++++++++++-------- src/stv/wasm.rs | 24 +++-- src/ties.rs | 78 ++++++++++++-- tests/aec.rs | 1 + tests/ers97.rs | 1 + tests/prsa.rs | 1 + tests/scotland.rs | 1 + 9 files changed, 316 insertions(+), 68 deletions(-) diff --git a/src/election.rs b/src/election.rs index bb2cf19..0713b13 100644 --- a/src/election.rs +++ b/src/election.rs @@ -131,6 +131,9 @@ pub struct CountState<'a, N> { pub exhausted: CountCard<'a, N>, pub loss_fraction: CountCard<'a, N>, + pub forwards_tiebreak: Option>, + pub backwards_tiebreak: Option>, + pub quota: Option, pub vote_required_election: Option, @@ -149,6 +152,8 @@ impl<'a, N: Number> CountState<'a, N> { candidates: HashMap::new(), exhausted: CountCard::new(), loss_fraction: CountCard::new(), + forwards_tiebreak: None, + backwards_tiebreak: None, quota: None, vote_required_election: None, num_elected: 0, diff --git a/src/main.rs b/src/main.rs index 76bf629..3c1ab3e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,6 +104,10 @@ struct STV { // ------------------ // -- STV variants -- + /// Tie-breaking method + #[clap(help_heading=Some("STV VARIANTS"), short='t', long, possible_values=&["forwards", "backwards", "random", "prompt"], default_value="prompt", value_name="methods")] + ties: Vec, + /// Method of surplus distributions #[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")] surplus: String, @@ -187,6 +191,7 @@ where &cmd_opts.quota, &cmd_opts.quota_criterion, &cmd_opts.quota_mode, + &cmd_opts.ties, &cmd_opts.surplus, &cmd_opts.surplus_order, cmd_opts.transferable_only, diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 4a0a270..868f05c 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -31,8 +31,7 @@ use std::cmp::max; use std::collections::HashMap; use std::ops; -#[wasm_bindgen] -pub struct STVOptions { +pub struct STVOptions<'o> { pub round_tvs: Option, pub round_weights: Option, pub round_votes: Option, @@ -42,6 +41,7 @@ pub struct STVOptions { pub quota: QuotaType, pub quota_criterion: QuotaCriterion, pub quota_mode: QuotaMode, + pub ties: Vec>, pub surplus: SurplusMethod, pub surplus_order: SurplusOrder, pub transferable_only: bool, @@ -51,8 +51,7 @@ pub struct STVOptions { pub pp_decimals: usize, } -#[wasm_bindgen] -impl STVOptions { +impl<'o> STVOptions<'o> { pub fn new( round_tvs: Option, round_weights: Option, @@ -63,6 +62,7 @@ impl STVOptions { quota: &str, quota_criterion: &str, quota_mode: &str, + ties: &Vec, surplus: &str, surplus_order: &str, transferable_only: bool, @@ -100,6 +100,13 @@ impl STVOptions { "ers97" => QuotaMode::ERS97, _ => panic!("Invalid --quota-mode"), }, + ties: ties.into_iter().map(|t| match t.as_str() { + "forwards" => TieStrategy::Forwards, + "backwards" => TieStrategy::Backwards, + "random" => todo!(), + "prompt" => TieStrategy::Prompt, + _ => panic!("Invalid --ties"), + }).collect(), surplus: match surplus { "wig" => SurplusMethod::WIG, "uig" => SurplusMethod::UIG, @@ -124,19 +131,18 @@ impl STVOptions { pp_decimals, }; } -} - -impl STVOptions { - // Not exported to WebAssembly + pub fn describe(&self) -> String { let mut flags = Vec::new(); - let n_str = N::describe_opt(); if n_str.len() > 0 { flags.push(N::describe_opt()) }; + let n_str = N::describe_opt(); if !n_str.is_empty() { flags.push(N::describe_opt()) }; if let Some(dps) = self.round_tvs { flags.push(format!("--round-tvs {}", dps)); } if let Some(dps) = self.round_weights { flags.push(format!("--round-weights {}", dps)); } if let Some(dps) = self.round_votes { flags.push(format!("--round-votes {}", dps)); } if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); } if self.sum_surplus_transfers != SumSurplusTransfersMode::SingleStep { flags.push(self.sum_surplus_transfers.describe()); } if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); } + let ties_str = self.ties.iter().map(|t| t.describe()).join(" "); + if ties_str != "prompt" { flags.push(format!("--ties {}", ties_str)); } if self.quota != QuotaType::DroopExact { flags.push(self.quota.describe()); } if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.describe()); } if self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); } @@ -286,12 +292,14 @@ impl ExclusionMethod { #[derive(Debug)] pub enum STVError { RequireInput, + UnresolvedTie, } pub fn count_init(mut state: &mut CountState<'_, N>, opts: &STVOptions) { distribute_first_preferences(&mut state); calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); + init_tiebreaks(&mut state, opts); } pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result @@ -312,6 +320,7 @@ where if continue_exclusion(&mut state, &opts) { calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); + update_tiebreaks(&mut state, opts); return Ok(false); } @@ -319,11 +328,12 @@ where if distribute_surpluses(&mut state, &opts)? { calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); + update_tiebreaks(&mut state, opts); return Ok(false); } // Attempt bulk election - if bulk_elect(&mut state) { + if bulk_elect(&mut state, &opts)? { return Ok(false); } @@ -331,6 +341,7 @@ where if exclude_hopefuls(&mut state, &opts)? { calculate_quota(&mut state, opts); elect_meeting_quota(&mut state, opts); + update_tiebreaks(&mut state, opts); return Ok(false); } @@ -562,9 +573,9 @@ fn elect_meeting_quota(state: &mut CountState, opts: &STVOptions) .filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::Hopeful && meets_quota(vote_req, cc, opts) }) .collect(); - if cands_meeting_quota.len() > 0 { + if !cands_meeting_quota.is_empty() { // Sort by votes - cands_meeting_quota.sort_unstable_by(|a, b| state.candidates.get(a).unwrap().votes.partial_cmp(&state.candidates.get(b).unwrap().votes).unwrap()); + cands_meeting_quota.sort_unstable_by(|a, b| state.candidates.get(a).unwrap().votes.cmp(&state.candidates.get(b).unwrap().votes)); // Declare elected in descending order of votes for candidate in cands_meeting_quota.into_iter().rev() { @@ -634,7 +645,7 @@ where let total_surpluses = has_surplus.iter() .fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota); - if has_surplus.len() > 0 { + if !has_surplus.is_empty() { // Determine if surplues can be deferred if opts.defer_surpluses { if can_defer_surpluses(state, opts, &total_surpluses) { @@ -646,10 +657,10 @@ where match opts.surplus_order { SurplusOrder::BySize => { // Compare b with a to sort high-low - has_surplus.sort_by(|a, b| b.1.votes.partial_cmp(&a.1.votes).unwrap()); + has_surplus.sort_by(|a, b| b.1.votes.cmp(&a.1.votes)); } SurplusOrder::ByOrder => { - has_surplus.sort_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap()); + has_surplus.sort_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected)); } } @@ -660,9 +671,9 @@ where if has_surplus.len() > 1 && has_surplus[0].1.votes == has_surplus[1].1.votes { let max_votes = &has_surplus[0].1.votes; let has_surplus = has_surplus.into_iter().filter_map(|(c, cc)| if &cc.votes == max_votes { Some(c) } else { None }).collect(); - elected_candidate = TieStrategy::Prompt.choose_highest(has_surplus)?; + elected_candidate = choose_highest(state, opts, has_surplus)?; } else { - elected_candidate = has_surplus.first_mut().unwrap().0; + elected_candidate = has_surplus[0].0; } distribute_surplus(state, &opts, elected_candidate); @@ -905,20 +916,35 @@ where state.loss_fraction.transfer(&-checksum); } -fn bulk_elect(state: &mut CountState) -> bool { +fn bulk_elect(state: &mut CountState, opts: &STVOptions) -> Result { if state.election.candidates.len() - state.num_excluded <= state.election.seats { state.kind = None; state.title = "Bulk election".to_string(); // Bulk elect all remaining candidates - let mut hopefuls: Vec<(&&Candidate, &mut CountCard)> = state.candidates.iter_mut() - .filter(|(_, cc)| cc.state == CandidateState::Hopeful) + let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter() + .filter(|c| state.candidates.get(c).unwrap().state == CandidateState::Hopeful) .collect(); - // TODO: Handle ties - hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); - - for (candidate, count_card) in hopefuls.into_iter() { + while !hopefuls.is_empty() { + let max_votes = hopefuls.iter() + .max_by(|a, b| state.candidates.get(**a).unwrap().votes.cmp(&state.candidates.get(**b).unwrap().votes)) + .unwrap(); + let max_votes = &state.candidates.get(max_votes).unwrap().votes; + let max_hopefuls: Vec<&Candidate> = hopefuls.iter() + .filter(|c| &state.candidates.get(**c).unwrap().votes == max_votes) + .map(|c| *c) + .collect(); + + let candidate; + if max_hopefuls.len() > 1 { + // Handle ties + candidate = choose_highest(state, opts, max_hopefuls)?; + } else { + candidate = max_hopefuls[0]; + } + + let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.state = CandidateState::Elected; state.num_elected += 1; count_card.order_elected = state.num_elected as isize; @@ -928,11 +954,13 @@ fn bulk_elect(state: &mut CountState) -> bool { "{} are elected to fill the remaining vacancies.", vec![&candidate.name] ); + + hopefuls.remove(hopefuls.iter().position(|c| *c == candidate).unwrap()); } - return true; + return Ok(true); } - return false; + return Ok(false); } fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &STVOptions) -> Vec<&'a Candidate> { @@ -943,8 +971,8 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST .collect(); // Sort by votes - // TODO: Handle ties - hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); + // NB: Unnecessary to handle ties, as ties will be rejected at "Do not exclude if this could change the order of exclusion" + hopefuls.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes)); let total_surpluses = state.candidates.iter() .filter(|(_, cc)| &cc.votes > state.quota.as_ref().unwrap()) @@ -954,11 +982,6 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST for i in 0..hopefuls.len() { let try_exclude = &hopefuls[0..hopefuls.len()-i]; - // Do not exclude if this splits tied candidates - if i != 0 && try_exclude.last().unwrap().1.votes == hopefuls[hopefuls.len()-i].1.votes { - continue; - } - // Do not exclude if this leaves insufficient candidates if state.num_elected + hopefuls.len() - try_exclude.len() < state.election.seats { continue; @@ -966,7 +989,7 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST // Do not exclude if this could change the order of exclusion let total_votes = try_exclude.into_iter().fold(N::new(), |agg, (_, cc)| agg + &cc.votes); - if i != 0 && total_votes + &total_surpluses > hopefuls[hopefuls.len()-i].1.votes { + if i != 0 && total_votes + &total_surpluses >= hopefuls[hopefuls.len()-i].1.votes { continue; } @@ -991,22 +1014,22 @@ where } // Exclude lowest ranked candidate - if excluded_candidates.len() == 0 { + if excluded_candidates.is_empty() { let mut hopefuls: Vec<(&Candidate, &CountCard)> = state.election.candidates.iter() // Present in order in case of tie .map(|c| (c, state.candidates.get(c).unwrap())) .filter(|(_, cc)| cc.state == CandidateState::Hopeful) .collect(); // Sort by votes - hopefuls.sort_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); + hopefuls.sort_by(|a, b| a.1.votes.cmp(&b.1.votes)); // Handle ties if hopefuls.len() > 1 && hopefuls[0].1.votes == hopefuls[1].1.votes { let min_votes = &hopefuls[0].1.votes; let hopefuls = hopefuls.into_iter().filter_map(|(c, cc)| if &cc.votes == min_votes { Some(c) } else { None }).collect(); - excluded_candidates = vec![TieStrategy::Prompt.choose_lowest(hopefuls)?]; + excluded_candidates = vec![choose_lowest(state, opts, hopefuls)?]; } else { - excluded_candidates = vec![&hopefuls.first().unwrap().0]; + excluded_candidates = vec![&hopefuls[0].0]; } } @@ -1031,14 +1054,13 @@ where { // Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers let mut excluded_with_votes: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() - //.filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && !cc.votes.is_zero()) - .filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| p.len() > 0)) + .filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| !p.is_empty())) .collect(); - if excluded_with_votes.len() > 0 { - excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap()); + if !excluded_with_votes.is_empty() { + excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected)); - let order_excluded = excluded_with_votes.first().unwrap().1.order_elected; + let order_excluded = excluded_with_votes[0].1.order_elected; let excluded_candidates: Vec<&Candidate> = excluded_with_votes.into_iter() .filter(|(_, cc)| cc.order_elected == order_excluded) .map(|(c, _)| *c) @@ -1127,7 +1149,7 @@ where } } - if remaining_votes.len() > 0 { + if !remaining_votes.is_empty() { votes_remain = true; } @@ -1145,9 +1167,9 @@ where panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude"); } - let count_card = state.candidates.get_mut(excluded_candidates.first().unwrap()).unwrap(); + let count_card = state.candidates.get_mut(excluded_candidates[0]).unwrap(); votes = count_card.parcels.remove(0); - votes_remain = count_card.parcels.len() > 0; + votes_remain = !count_card.parcels.is_empty(); // Update votes let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value); @@ -1156,7 +1178,7 @@ where } } - if votes.len() > 0 { + if !votes.is_empty() { let value = &votes[0].value / &votes[0].ballot.orig_value; // Count next preferences @@ -1220,3 +1242,155 @@ fn finished_before_stage(state: &CountState) -> bool { } return false; } + +fn choose_highest<'c, N: Number>(state: &CountState, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { + for strategy in opts.ties.iter() { + match strategy.choose_highest(state, &candidates) { + Ok(c) => { + return Ok(c); + } + Err(e) => { + if let STVError::UnresolvedTie = e { + continue; + } else { + return Err(e); + } + } + } + } + panic!("Unable to resolve tie"); +} + +fn choose_lowest<'c, N: Number>(state: &CountState, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { + for strategy in opts.ties.iter() { + match strategy.choose_lowest(state, &candidates) { + Ok(c) => { + return Ok(c); + } + Err(e) => { + if let STVError::UnresolvedTie = e { + continue; + } else { + return Err(e); + } + } + } + } + return Err(STVError::UnresolvedTie); +} + +fn init_tiebreaks(state: &mut CountState, opts: &STVOptions) { + if !opts.ties.iter().any(|t| t == &TieStrategy::Forwards) && !opts.ties.iter().any(|t| t == &TieStrategy::Backwards) { + return; + } + + // Sort candidates in this stage by votes, grouping by ties + let mut sorted_candidates: Vec<(&&Candidate, &CountCard)> = state.candidates.iter().collect(); + sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes)); + let sorted_candidates: Vec)>> = sorted_candidates.into_iter() + .group_by(|(_, cc)| &cc.votes) + .into_iter() + .map(|(_, candidates)| candidates.collect()) + .collect(); + + // Update forwards tie-breaking order + if opts.ties.iter().any(|t| t == &TieStrategy::Forwards) { + let mut hm: HashMap<&Candidate, usize> = HashMap::new(); + for (i, group) in sorted_candidates.iter().enumerate() { + for (candidate, _) in group.iter() { + hm.insert(candidate, i); + } + } + state.forwards_tiebreak = Some(hm); + } + + // Update backwards tie-breaking order + if opts.ties.iter().any(|t| t == &TieStrategy::Backwards) { + let mut hm: HashMap<&Candidate, usize> = HashMap::new(); + for (i, group) in sorted_candidates.iter().enumerate() { + for (candidate, _) in group.iter() { + hm.insert(candidate, i); + } + } + state.backwards_tiebreak = Some(hm); + } +} + +fn update_tiebreaks(state: &mut CountState, _opts: &STVOptions) { + if let None = state.forwards_tiebreak { + if let None = state.backwards_tiebreak { + return; + } + } + + // Sort candidates in this stage by votes, grouping by ties + let mut sorted_candidates: Vec<(&&Candidate, &CountCard)> = state.candidates.iter().collect(); + sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes)); + let sorted_candidates: Vec> = sorted_candidates.into_iter() + .group_by(|(_, cc)| &cc.votes) + .into_iter() + .map(|(_, candidates)| candidates.map(|(c, _)| *c).collect()) + .collect(); + + // Update forwards tie-breaking order + if let Some(hm) = state.forwards_tiebreak.as_mut() { + // TODO: Check if already completely sorted + let mut sorted_last_round: Vec<(&&Candidate, &usize)> = hm.iter().collect(); + sorted_last_round.sort_unstable_by(|a, b| a.1.cmp(b.1)); + let sorted_last_round: Vec> = sorted_last_round.into_iter() + .group_by(|(_, v)| **v) + .into_iter() + .map(|(_, group)| group.map(|(c, _)| *c).collect()) + .collect(); + + let mut i: usize = 0; + for mut group in sorted_last_round.into_iter() { + if group.len() == 1 { + hm.insert(group[0], i); + i += 1; + continue; + } else { + // Tied in last round - refer to this round + group.sort_unstable_by(|a, b| + sorted_candidates.iter().position(|x| x.contains(a)).unwrap() + .cmp(&sorted_candidates.iter().position(|x| x.contains(b)).unwrap()) + ); + let tied_last_round = group.into_iter() + .group_by(|c| sorted_candidates.iter().position(|x| x.contains(c)).unwrap()); + + for (_, group2) in tied_last_round.into_iter() { + for candidate in group2 { + hm.insert(candidate, i); + } + i += 1; + } + } + } + } + + // Update backwards tie-breaking order + if let Some(hm) = state.backwards_tiebreak.as_mut() { + let hm_orig = hm.clone(); + let mut i: usize = 0; + for group in sorted_candidates.iter() { + if group.len() == 1 { + hm.insert(group[0], i); + i += 1; + continue; + } else { + // Tied in this round - refer to last round + let mut tied_this_round: Vec<&Candidate> = group.into_iter().map(|c| *c).collect(); + tied_this_round.sort_unstable_by(|a, b| hm_orig.get(a).unwrap().cmp(hm_orig.get(b).unwrap())); + let tied_this_round = tied_this_round.into_iter() + .group_by(|c| hm_orig.get(c).unwrap()); + + for (_, group2) in tied_this_round.into_iter() { + for candidate in group2 { + hm.insert(candidate, i); + } + i += 1; + } + } + } + } +} diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 3c2aa80..7ec84eb 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -55,16 +55,17 @@ macro_rules! impl_type { #[wasm_bindgen] #[allow(non_snake_case)] - pub fn [](state: &mut [], opts: &stv::STVOptions) { - stv::count_init(&mut state.0, &opts); + pub fn [](state: &mut [], opts: &STVOptionsWrapper) { + stv::count_init(&mut state.0, &opts.0); } #[wasm_bindgen] #[allow(non_snake_case)] - pub fn [](state: &mut [], opts: &stv::STVOptions) -> Result { - match stv::count_one_stage(&mut state.0, &opts) { + pub fn [](state: &mut [], opts: &STVOptionsWrapper) -> Result { + match stv::count_one_stage(&mut state.0, &opts.0) { Ok(v) => Ok(v), Err(stv::STVError::RequireInput) => Err("RequireInput".into()), + Err(stv::STVError::UnresolvedTie) => Err("UnresolvedTie".into()), } } @@ -72,20 +73,20 @@ macro_rules! impl_type { #[wasm_bindgen] #[allow(non_snake_case)] - pub fn [](election: &[], opts: &stv::STVOptions) -> String { - return init_results_table(&election.0, opts); + pub fn [](election: &[], opts: &STVOptionsWrapper) -> String { + return init_results_table(&election.0, &opts.0); } #[wasm_bindgen] #[allow(non_snake_case)] - pub fn [](filename: String, election: &[], opts: &stv::STVOptions) -> String { - return describe_count(filename, &election.0, opts); + pub fn [](filename: String, election: &[], opts: &STVOptionsWrapper) -> String { + return describe_count(filename, &election.0, &opts.0); } #[wasm_bindgen] #[allow(non_snake_case)] - pub fn [](stage_num: usize, state: &[], opts: &stv::STVOptions) -> Array { - return update_results_table(stage_num, &state.0, opts); + pub fn [](stage_num: usize, state: &[], opts: &STVOptionsWrapper) -> Array { + return update_results_table(stage_num, &state.0, &opts.0); } #[wasm_bindgen] @@ -139,6 +140,9 @@ impl_type!(Fixed); impl_type!(NativeFloat64); impl_type!(Rational); +#[wasm_bindgen] +pub struct STVOptionsWrapper(stv::STVOptions<'static>); + // Reporting fn init_results_table(election: &Election, opts: &stv::STVOptions) -> String { diff --git a/src/ties.rs b/src/ties.rs index b011abe..b9d848c 100644 --- a/src/ties.rs +++ b/src/ties.rs @@ -15,7 +15,8 @@ * along with this program. If not, see . */ -use crate::election::Candidate; +use crate::election::{Candidate, CountState}; +use crate::numbers::Number; use crate::stv::STVError; #[allow(unused_imports)] @@ -24,6 +25,7 @@ use wasm_bindgen::prelude::wasm_bindgen; #[allow(unused_imports)] use std::io::{stdin, stdout, Write}; +#[derive(PartialEq)] pub enum TieStrategy<'s> { Forwards, Backwards, @@ -32,10 +34,42 @@ pub enum TieStrategy<'s> { } impl<'s> TieStrategy<'s> { - pub fn choose_highest<'c>(&self, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { + pub fn describe(&self) -> String { match self { - Self::Forwards => { todo!() } - Self::Backwards => { todo!() } + Self::Forwards => "forwards", + Self::Backwards => "backwards", + Self::Random(_) => todo!(), + Self::Prompt => "prompt", + }.to_string() + } + + pub fn choose_highest<'c, N: Number>(&self, state: &CountState, candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { + match self { + Self::Forwards => { + let mut candidates = candidates.clone(); + candidates.sort_unstable_by(|a, b| + // Compare b to a to sort high-to-low + state.forwards_tiebreak.as_ref().unwrap().get(b).unwrap() + .cmp(&state.forwards_tiebreak.as_ref().unwrap().get(a).unwrap()) + ); + if state.forwards_tiebreak.as_ref().unwrap().get(candidates[0]).unwrap() == state.forwards_tiebreak.as_ref().unwrap().get(candidates[1]).unwrap() { + return Err(STVError::UnresolvedTie); + } else { + return Ok(candidates[0]); + } + } + Self::Backwards => { + let mut candidates = candidates.clone(); + candidates.sort_unstable_by(|a, b| + state.backwards_tiebreak.as_ref().unwrap().get(b).unwrap() + .cmp(&state.backwards_tiebreak.as_ref().unwrap().get(a).unwrap()) + ); + if state.backwards_tiebreak.as_ref().unwrap().get(candidates[0]).unwrap() == state.backwards_tiebreak.as_ref().unwrap().get(candidates[1]).unwrap() { + return Err(STVError::UnresolvedTie); + } else { + return Ok(candidates[0]); + } + } Self::Random(_seed) => { todo!() } Self::Prompt => { return prompt(candidates); @@ -43,22 +77,44 @@ impl<'s> TieStrategy<'s> { } } - pub fn choose_lowest<'c>(&self, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { + pub fn choose_lowest<'c, N: Number>(&self, state: &CountState, candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { match self { - Self::Forwards => { todo!() } - Self::Backwards => { todo!() } + Self::Forwards => { + let mut candidates = candidates.clone(); + candidates.sort_unstable_by(|a, b| + state.forwards_tiebreak.as_ref().unwrap().get(a).unwrap() + .cmp(&state.forwards_tiebreak.as_ref().unwrap().get(b).unwrap()) + ); + if state.forwards_tiebreak.as_ref().unwrap().get(candidates[0]).unwrap() == state.forwards_tiebreak.as_ref().unwrap().get(candidates[1]).unwrap() { + return Err(STVError::UnresolvedTie); + } else { + return Ok(candidates[0]); + } + } + Self::Backwards => { + let mut candidates = candidates.clone(); + candidates.sort_unstable_by(|a, b| + state.backwards_tiebreak.as_ref().unwrap().get(a).unwrap() + .cmp(&state.backwards_tiebreak.as_ref().unwrap().get(b).unwrap()) + ); + if state.backwards_tiebreak.as_ref().unwrap().get(candidates[0]).unwrap() == state.backwards_tiebreak.as_ref().unwrap().get(candidates[1]).unwrap() { + return Err(STVError::UnresolvedTie); + } else { + return Ok(candidates[0]); + } + } Self::Random(_seed) => { - return self.choose_highest(candidates); + return self.choose_highest(state, candidates); } Self::Prompt => { - return self.choose_highest(candidates); + return self.choose_highest(state, candidates); } } } } #[cfg(not(target_arch = "wasm32"))] -fn prompt<'c>(candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { +fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { println!("Multiple tied candidates:"); for (i, candidate) in candidates.iter().enumerate() { println!("{}. {}", i + 1, candidate.name); @@ -93,7 +149,7 @@ extern "C" { } #[cfg(target_arch = "wasm32")] -fn prompt<'c>(candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { +fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { let mut message = String::from("Multiple tied candidates:\n"); for (i, candidate) in candidates.iter().enumerate() { message.push_str(&format!("{}. {}\n", i + 1, candidate.name)); diff --git a/tests/aec.rs b/tests/aec.rs index da1a0ae..f5e4080 100644 --- a/tests/aec.rs +++ b/tests/aec.rs @@ -64,6 +64,7 @@ fn aec_tas19_rational() { quota: stv::QuotaType::Droop, quota_criterion: stv::QuotaCriterion::GreaterOrEqual, quota_mode: stv::QuotaMode::Static, + ties: vec![], surplus: stv::SurplusMethod::UIG, surplus_order: stv::SurplusOrder::ByOrder, transferable_only: false, diff --git a/tests/ers97.rs b/tests/ers97.rs index f665645..6b6f97c 100644 --- a/tests/ers97.rs +++ b/tests/ers97.rs @@ -38,6 +38,7 @@ fn ers97_rational() { quota: stv::QuotaType::DroopExact, quota_criterion: stv::QuotaCriterion::GreaterOrEqual, quota_mode: stv::QuotaMode::ERS97, + ties: vec![], surplus: stv::SurplusMethod::EG, surplus_order: stv::SurplusOrder::BySize, transferable_only: true, diff --git a/tests/prsa.rs b/tests/prsa.rs index 144aa29..7da42ba 100644 --- a/tests/prsa.rs +++ b/tests/prsa.rs @@ -32,6 +32,7 @@ fn prsa1_rational() { quota: stv::QuotaType::Droop, quota_criterion: stv::QuotaCriterion::GreaterOrEqual, quota_mode: stv::QuotaMode::Static, + ties: vec![], surplus: stv::SurplusMethod::EG, surplus_order: stv::SurplusOrder::ByOrder, transferable_only: true, diff --git a/tests/scotland.rs b/tests/scotland.rs index 098114c..3e54b04 100644 --- a/tests/scotland.rs +++ b/tests/scotland.rs @@ -39,6 +39,7 @@ fn scotland_linn07_fixed5() { quota: stv::QuotaType::Droop, quota_criterion: stv::QuotaCriterion::GreaterOrEqual, quota_mode: stv::QuotaMode::Static, + ties: vec![], surplus: stv::SurplusMethod::WIG, surplus_order: stv::SurplusOrder::BySize, transferable_only: false,