Implement forwards and backwards tie-breaking
This commit is contained in:
parent
5d491687b4
commit
2dc5ed963b
|
@ -131,6 +131,9 @@ pub struct CountState<'a, N> {
|
||||||
pub exhausted: CountCard<'a, N>,
|
pub exhausted: CountCard<'a, N>,
|
||||||
pub loss_fraction: CountCard<'a, N>,
|
pub loss_fraction: CountCard<'a, N>,
|
||||||
|
|
||||||
|
pub forwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
||||||
|
pub backwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
||||||
|
|
||||||
pub quota: Option<N>,
|
pub quota: Option<N>,
|
||||||
pub vote_required_election: Option<N>,
|
pub vote_required_election: Option<N>,
|
||||||
|
|
||||||
|
@ -149,6 +152,8 @@ impl<'a, N: Number> CountState<'a, N> {
|
||||||
candidates: HashMap::new(),
|
candidates: HashMap::new(),
|
||||||
exhausted: CountCard::new(),
|
exhausted: CountCard::new(),
|
||||||
loss_fraction: CountCard::new(),
|
loss_fraction: CountCard::new(),
|
||||||
|
forwards_tiebreak: None,
|
||||||
|
backwards_tiebreak: None,
|
||||||
quota: None,
|
quota: None,
|
||||||
vote_required_election: None,
|
vote_required_election: None,
|
||||||
num_elected: 0,
|
num_elected: 0,
|
||||||
|
|
|
@ -104,6 +104,10 @@ struct STV {
|
||||||
// ------------------
|
// ------------------
|
||||||
// -- STV variants --
|
// -- 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<String>,
|
||||||
|
|
||||||
/// Method of surplus distributions
|
/// 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")]
|
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
|
||||||
surplus: String,
|
surplus: String,
|
||||||
|
@ -187,6 +191,7 @@ where
|
||||||
&cmd_opts.quota,
|
&cmd_opts.quota,
|
||||||
&cmd_opts.quota_criterion,
|
&cmd_opts.quota_criterion,
|
||||||
&cmd_opts.quota_mode,
|
&cmd_opts.quota_mode,
|
||||||
|
&cmd_opts.ties,
|
||||||
&cmd_opts.surplus,
|
&cmd_opts.surplus,
|
||||||
&cmd_opts.surplus_order,
|
&cmd_opts.surplus_order,
|
||||||
cmd_opts.transferable_only,
|
cmd_opts.transferable_only,
|
||||||
|
|
268
src/stv/mod.rs
268
src/stv/mod.rs
|
@ -31,8 +31,7 @@ use std::cmp::max;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
pub struct STVOptions<'o> {
|
||||||
pub struct STVOptions {
|
|
||||||
pub round_tvs: Option<usize>,
|
pub round_tvs: Option<usize>,
|
||||||
pub round_weights: Option<usize>,
|
pub round_weights: Option<usize>,
|
||||||
pub round_votes: Option<usize>,
|
pub round_votes: Option<usize>,
|
||||||
|
@ -42,6 +41,7 @@ pub struct STVOptions {
|
||||||
pub quota: QuotaType,
|
pub quota: QuotaType,
|
||||||
pub quota_criterion: QuotaCriterion,
|
pub quota_criterion: QuotaCriterion,
|
||||||
pub quota_mode: QuotaMode,
|
pub quota_mode: QuotaMode,
|
||||||
|
pub ties: Vec<TieStrategy<'o>>,
|
||||||
pub surplus: SurplusMethod,
|
pub surplus: SurplusMethod,
|
||||||
pub surplus_order: SurplusOrder,
|
pub surplus_order: SurplusOrder,
|
||||||
pub transferable_only: bool,
|
pub transferable_only: bool,
|
||||||
|
@ -51,8 +51,7 @@ pub struct STVOptions {
|
||||||
pub pp_decimals: usize,
|
pub pp_decimals: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
impl<'o> STVOptions<'o> {
|
||||||
impl STVOptions {
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
round_tvs: Option<usize>,
|
round_tvs: Option<usize>,
|
||||||
round_weights: Option<usize>,
|
round_weights: Option<usize>,
|
||||||
|
@ -63,6 +62,7 @@ impl STVOptions {
|
||||||
quota: &str,
|
quota: &str,
|
||||||
quota_criterion: &str,
|
quota_criterion: &str,
|
||||||
quota_mode: &str,
|
quota_mode: &str,
|
||||||
|
ties: &Vec<String>,
|
||||||
surplus: &str,
|
surplus: &str,
|
||||||
surplus_order: &str,
|
surplus_order: &str,
|
||||||
transferable_only: bool,
|
transferable_only: bool,
|
||||||
|
@ -100,6 +100,13 @@ impl STVOptions {
|
||||||
"ers97" => QuotaMode::ERS97,
|
"ers97" => QuotaMode::ERS97,
|
||||||
_ => panic!("Invalid --quota-mode"),
|
_ => 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 {
|
surplus: match surplus {
|
||||||
"wig" => SurplusMethod::WIG,
|
"wig" => SurplusMethod::WIG,
|
||||||
"uig" => SurplusMethod::UIG,
|
"uig" => SurplusMethod::UIG,
|
||||||
|
@ -124,19 +131,18 @@ impl STVOptions {
|
||||||
pp_decimals,
|
pp_decimals,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl STVOptions {
|
|
||||||
// Not exported to WebAssembly
|
|
||||||
pub fn describe<N: Number>(&self) -> String {
|
pub fn describe<N: Number>(&self) -> String {
|
||||||
let mut flags = Vec::new();
|
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_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_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_votes { flags.push(format!("--round-votes {}", dps)); }
|
||||||
if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", 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.sum_surplus_transfers != SumSurplusTransfersMode::SingleStep { flags.push(self.sum_surplus_transfers.describe()); }
|
||||||
if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); }
|
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 != QuotaType::DroopExact { flags.push(self.quota.describe()); }
|
||||||
if self.quota_criterion != QuotaCriterion::Greater { flags.push(self.quota_criterion.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()); }
|
if self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); }
|
||||||
|
@ -286,12 +292,14 @@ impl ExclusionMethod {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum STVError {
|
pub enum STVError {
|
||||||
RequireInput,
|
RequireInput,
|
||||||
|
UnresolvedTie,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) {
|
pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) {
|
||||||
distribute_first_preferences(&mut state);
|
distribute_first_preferences(&mut state);
|
||||||
calculate_quota(&mut state, opts);
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_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<bool, STVError>
|
pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||||
|
@ -312,6 +320,7 @@ where
|
||||||
if continue_exclusion(&mut state, &opts) {
|
if continue_exclusion(&mut state, &opts) {
|
||||||
calculate_quota(&mut state, opts);
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_quota(&mut state, opts);
|
elect_meeting_quota(&mut state, opts);
|
||||||
|
update_tiebreaks(&mut state, opts);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,11 +328,12 @@ where
|
||||||
if distribute_surpluses(&mut state, &opts)? {
|
if distribute_surpluses(&mut state, &opts)? {
|
||||||
calculate_quota(&mut state, opts);
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_quota(&mut state, opts);
|
elect_meeting_quota(&mut state, opts);
|
||||||
|
update_tiebreaks(&mut state, opts);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt bulk election
|
// Attempt bulk election
|
||||||
if bulk_elect(&mut state) {
|
if bulk_elect(&mut state, &opts)? {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,6 +341,7 @@ where
|
||||||
if exclude_hopefuls(&mut state, &opts)? {
|
if exclude_hopefuls(&mut state, &opts)? {
|
||||||
calculate_quota(&mut state, opts);
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_quota(&mut state, opts);
|
elect_meeting_quota(&mut state, opts);
|
||||||
|
update_tiebreaks(&mut state, opts);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,9 +573,9 @@ fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
|
||||||
.filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::Hopeful && meets_quota(vote_req, cc, opts) })
|
.filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::Hopeful && meets_quota(vote_req, cc, opts) })
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if cands_meeting_quota.len() > 0 {
|
if !cands_meeting_quota.is_empty() {
|
||||||
// Sort by votes
|
// 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
|
// Declare elected in descending order of votes
|
||||||
for candidate in cands_meeting_quota.into_iter().rev() {
|
for candidate in cands_meeting_quota.into_iter().rev() {
|
||||||
|
@ -634,7 +645,7 @@ where
|
||||||
let total_surpluses = has_surplus.iter()
|
let total_surpluses = has_surplus.iter()
|
||||||
.fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota);
|
.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
|
// Determine if surplues can be deferred
|
||||||
if opts.defer_surpluses {
|
if opts.defer_surpluses {
|
||||||
if can_defer_surpluses(state, opts, &total_surpluses) {
|
if can_defer_surpluses(state, opts, &total_surpluses) {
|
||||||
|
@ -646,10 +657,10 @@ where
|
||||||
match opts.surplus_order {
|
match opts.surplus_order {
|
||||||
SurplusOrder::BySize => {
|
SurplusOrder::BySize => {
|
||||||
// Compare b with a to sort high-low
|
// 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 => {
|
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 {
|
if has_surplus.len() > 1 && has_surplus[0].1.votes == has_surplus[1].1.votes {
|
||||||
let max_votes = &has_surplus[0].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();
|
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 {
|
} else {
|
||||||
elected_candidate = has_surplus.first_mut().unwrap().0;
|
elected_candidate = has_surplus[0].0;
|
||||||
}
|
}
|
||||||
|
|
||||||
distribute_surplus(state, &opts, elected_candidate);
|
distribute_surplus(state, &opts, elected_candidate);
|
||||||
|
@ -905,20 +916,35 @@ where
|
||||||
state.loss_fraction.transfer(&-checksum);
|
state.loss_fraction.transfer(&-checksum);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
|
fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> {
|
||||||
if state.election.candidates.len() - state.num_excluded <= state.election.seats {
|
if state.election.candidates.len() - state.num_excluded <= state.election.seats {
|
||||||
state.kind = None;
|
state.kind = None;
|
||||||
state.title = "Bulk election".to_string();
|
state.title = "Bulk election".to_string();
|
||||||
|
|
||||||
// Bulk elect all remaining candidates
|
// Bulk elect all remaining candidates
|
||||||
let mut hopefuls: Vec<(&&Candidate, &mut CountCard<N>)> = state.candidates.iter_mut()
|
let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter()
|
||||||
.filter(|(_, cc)| cc.state == CandidateState::Hopeful)
|
.filter(|c| state.candidates.get(c).unwrap().state == CandidateState::Hopeful)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// TODO: Handle ties
|
while !hopefuls.is_empty() {
|
||||||
hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
|
let max_votes = hopefuls.iter()
|
||||||
|
.max_by(|a, b| state.candidates.get(**a).unwrap().votes.cmp(&state.candidates.get(**b).unwrap().votes))
|
||||||
for (candidate, count_card) in hopefuls.into_iter() {
|
.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;
|
count_card.state = CandidateState::Elected;
|
||||||
state.num_elected += 1;
|
state.num_elected += 1;
|
||||||
count_card.order_elected = state.num_elected as isize;
|
count_card.order_elected = state.num_elected as isize;
|
||||||
|
@ -928,11 +954,13 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
|
||||||
"{} are elected to fill the remaining vacancies.",
|
"{} are elected to fill the remaining vacancies.",
|
||||||
vec![&candidate.name]
|
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> {
|
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();
|
.collect();
|
||||||
|
|
||||||
// Sort by votes
|
// Sort by votes
|
||||||
// TODO: Handle ties
|
// 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.partial_cmp(&b.1.votes).unwrap());
|
hopefuls.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
|
||||||
|
|
||||||
let total_surpluses = state.candidates.iter()
|
let total_surpluses = state.candidates.iter()
|
||||||
.filter(|(_, cc)| &cc.votes > state.quota.as_ref().unwrap())
|
.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() {
|
for i in 0..hopefuls.len() {
|
||||||
let try_exclude = &hopefuls[0..hopefuls.len()-i];
|
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
|
// Do not exclude if this leaves insufficient candidates
|
||||||
if state.num_elected + hopefuls.len() - try_exclude.len() < state.election.seats {
|
if state.num_elected + hopefuls.len() - try_exclude.len() < state.election.seats {
|
||||||
continue;
|
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
|
// 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);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -991,22 +1014,22 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclude lowest ranked candidate
|
// Exclude lowest ranked candidate
|
||||||
if excluded_candidates.len() == 0 {
|
if excluded_candidates.is_empty() {
|
||||||
let mut hopefuls: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
|
let mut hopefuls: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
|
||||||
.map(|c| (c, state.candidates.get(c).unwrap()))
|
.map(|c| (c, state.candidates.get(c).unwrap()))
|
||||||
.filter(|(_, cc)| cc.state == CandidateState::Hopeful)
|
.filter(|(_, cc)| cc.state == CandidateState::Hopeful)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Sort by votes
|
// 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
|
// Handle ties
|
||||||
if hopefuls.len() > 1 && hopefuls[0].1.votes == hopefuls[1].1.votes {
|
if hopefuls.len() > 1 && hopefuls[0].1.votes == hopefuls[1].1.votes {
|
||||||
let min_votes = &hopefuls[0].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();
|
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 {
|
} 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
|
// 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<N>)> = state.candidates.iter()
|
let mut excluded_with_votes: Vec<(&&Candidate, &CountCard<N>)> = 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.is_empty()))
|
||||||
.filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| p.len() > 0))
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if excluded_with_votes.len() > 0 {
|
if !excluded_with_votes.is_empty() {
|
||||||
excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap());
|
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()
|
let excluded_candidates: Vec<&Candidate> = excluded_with_votes.into_iter()
|
||||||
.filter(|(_, cc)| cc.order_elected == order_excluded)
|
.filter(|(_, cc)| cc.order_elected == order_excluded)
|
||||||
.map(|(c, _)| *c)
|
.map(|(c, _)| *c)
|
||||||
|
@ -1127,7 +1149,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if remaining_votes.len() > 0 {
|
if !remaining_votes.is_empty() {
|
||||||
votes_remain = true;
|
votes_remain = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1145,9 +1167,9 @@ where
|
||||||
panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude");
|
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 = count_card.parcels.remove(0);
|
||||||
votes_remain = count_card.parcels.len() > 0;
|
votes_remain = !count_card.parcels.is_empty();
|
||||||
|
|
||||||
// Update votes
|
// Update votes
|
||||||
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value);
|
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;
|
let value = &votes[0].value / &votes[0].ballot.orig_value;
|
||||||
|
|
||||||
// Count next preferences
|
// Count next preferences
|
||||||
|
@ -1220,3 +1242,155 @@ fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn choose_highest<'c, N: Number>(state: &CountState<N>, 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<N>, 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<N: Number>(state: &mut CountState<N>, 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<N>)> = state.candidates.iter().collect();
|
||||||
|
sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
|
||||||
|
let sorted_candidates: Vec<Vec<(&&Candidate, &CountCard<N>)>> = 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<N: Number>(state: &mut CountState<N>, _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<N>)> = state.candidates.iter().collect();
|
||||||
|
sorted_candidates.sort_unstable_by(|a, b| a.1.votes.cmp(&b.1.votes));
|
||||||
|
let sorted_candidates: Vec<Vec<&Candidate>> = 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<Vec<&Candidate>> = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -55,16 +55,17 @@ macro_rules! impl_type {
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) {
|
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &STVOptionsWrapper) {
|
||||||
stv::count_init(&mut state.0, &opts);
|
stv::count_init(&mut state.0, &opts.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) -> Result<bool, JsValue> {
|
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &STVOptionsWrapper) -> Result<bool, JsValue> {
|
||||||
match stv::count_one_stage(&mut state.0, &opts) {
|
match stv::count_one_stage(&mut state.0, &opts.0) {
|
||||||
Ok(v) => Ok(v),
|
Ok(v) => Ok(v),
|
||||||
Err(stv::STVError::RequireInput) => Err("RequireInput".into()),
|
Err(stv::STVError::RequireInput) => Err("RequireInput".into()),
|
||||||
|
Err(stv::STVError::UnresolvedTie) => Err("UnresolvedTie".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,20 +73,20 @@ macro_rules! impl_type {
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &stv::STVOptions) -> String {
|
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptionsWrapper) -> String {
|
||||||
return init_results_table(&election.0, opts);
|
return init_results_table(&election.0, &opts.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<describe_count_$type>](filename: String, election: &[<Election$type>], opts: &stv::STVOptions) -> String {
|
pub fn [<describe_count_$type>](filename: String, election: &[<Election$type>], opts: &STVOptionsWrapper) -> String {
|
||||||
return describe_count(filename, &election.0, opts);
|
return describe_count(filename, &election.0, &opts.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &stv::STVOptions) -> Array {
|
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptionsWrapper) -> Array {
|
||||||
return update_results_table(stage_num, &state.0, opts);
|
return update_results_table(stage_num, &state.0, &opts.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
@ -139,6 +140,9 @@ impl_type!(Fixed);
|
||||||
impl_type!(NativeFloat64);
|
impl_type!(NativeFloat64);
|
||||||
impl_type!(Rational);
|
impl_type!(Rational);
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct STVOptionsWrapper(stv::STVOptions<'static>);
|
||||||
|
|
||||||
// Reporting
|
// Reporting
|
||||||
|
|
||||||
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String {
|
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String {
|
||||||
|
|
78
src/ties.rs
78
src/ties.rs
|
@ -15,7 +15,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::election::Candidate;
|
use crate::election::{Candidate, CountState};
|
||||||
|
use crate::numbers::Number;
|
||||||
use crate::stv::STVError;
|
use crate::stv::STVError;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
|
@ -24,6 +25,7 @@ use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use std::io::{stdin, stdout, Write};
|
use std::io::{stdin, stdout, Write};
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
pub enum TieStrategy<'s> {
|
pub enum TieStrategy<'s> {
|
||||||
Forwards,
|
Forwards,
|
||||||
Backwards,
|
Backwards,
|
||||||
|
@ -32,10 +34,42 @@ pub enum TieStrategy<'s> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'s> 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 {
|
match self {
|
||||||
Self::Forwards => { todo!() }
|
Self::Forwards => "forwards",
|
||||||
Self::Backwards => { todo!() }
|
Self::Backwards => "backwards",
|
||||||
|
Self::Random(_) => todo!(),
|
||||||
|
Self::Prompt => "prompt",
|
||||||
|
}.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn choose_highest<'c, N: Number>(&self, state: &CountState<N>, 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::Random(_seed) => { todo!() }
|
||||||
Self::Prompt => {
|
Self::Prompt => {
|
||||||
return prompt(candidates);
|
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<N>, candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
||||||
match self {
|
match self {
|
||||||
Self::Forwards => { todo!() }
|
Self::Forwards => {
|
||||||
Self::Backwards => { todo!() }
|
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) => {
|
Self::Random(_seed) => {
|
||||||
return self.choose_highest(candidates);
|
return self.choose_highest(state, candidates);
|
||||||
}
|
}
|
||||||
Self::Prompt => {
|
Self::Prompt => {
|
||||||
return self.choose_highest(candidates);
|
return self.choose_highest(state, candidates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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:");
|
println!("Multiple tied candidates:");
|
||||||
for (i, candidate) in candidates.iter().enumerate() {
|
for (i, candidate) in candidates.iter().enumerate() {
|
||||||
println!("{}. {}", i + 1, candidate.name);
|
println!("{}. {}", i + 1, candidate.name);
|
||||||
|
@ -93,7 +149,7 @@ extern "C" {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[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");
|
let mut message = String::from("Multiple tied candidates:\n");
|
||||||
for (i, candidate) in candidates.iter().enumerate() {
|
for (i, candidate) in candidates.iter().enumerate() {
|
||||||
message.push_str(&format!("{}. {}\n", i + 1, candidate.name));
|
message.push_str(&format!("{}. {}\n", i + 1, candidate.name));
|
||||||
|
|
|
@ -64,6 +64,7 @@ fn aec_tas19_rational() {
|
||||||
quota: stv::QuotaType::Droop,
|
quota: stv::QuotaType::Droop,
|
||||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||||
quota_mode: stv::QuotaMode::Static,
|
quota_mode: stv::QuotaMode::Static,
|
||||||
|
ties: vec![],
|
||||||
surplus: stv::SurplusMethod::UIG,
|
surplus: stv::SurplusMethod::UIG,
|
||||||
surplus_order: stv::SurplusOrder::ByOrder,
|
surplus_order: stv::SurplusOrder::ByOrder,
|
||||||
transferable_only: false,
|
transferable_only: false,
|
||||||
|
|
|
@ -38,6 +38,7 @@ fn ers97_rational() {
|
||||||
quota: stv::QuotaType::DroopExact,
|
quota: stv::QuotaType::DroopExact,
|
||||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||||
quota_mode: stv::QuotaMode::ERS97,
|
quota_mode: stv::QuotaMode::ERS97,
|
||||||
|
ties: vec![],
|
||||||
surplus: stv::SurplusMethod::EG,
|
surplus: stv::SurplusMethod::EG,
|
||||||
surplus_order: stv::SurplusOrder::BySize,
|
surplus_order: stv::SurplusOrder::BySize,
|
||||||
transferable_only: true,
|
transferable_only: true,
|
||||||
|
|
|
@ -32,6 +32,7 @@ fn prsa1_rational() {
|
||||||
quota: stv::QuotaType::Droop,
|
quota: stv::QuotaType::Droop,
|
||||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||||
quota_mode: stv::QuotaMode::Static,
|
quota_mode: stv::QuotaMode::Static,
|
||||||
|
ties: vec![],
|
||||||
surplus: stv::SurplusMethod::EG,
|
surplus: stv::SurplusMethod::EG,
|
||||||
surplus_order: stv::SurplusOrder::ByOrder,
|
surplus_order: stv::SurplusOrder::ByOrder,
|
||||||
transferable_only: true,
|
transferable_only: true,
|
||||||
|
|
|
@ -39,6 +39,7 @@ fn scotland_linn07_fixed5() {
|
||||||
quota: stv::QuotaType::Droop,
|
quota: stv::QuotaType::Droop,
|
||||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||||
quota_mode: stv::QuotaMode::Static,
|
quota_mode: stv::QuotaMode::Static,
|
||||||
|
ties: vec![],
|
||||||
surplus: stv::SurplusMethod::WIG,
|
surplus: stv::SurplusMethod::WIG,
|
||||||
surplus_order: stv::SurplusOrder::BySize,
|
surplus_order: stv::SurplusOrder::BySize,
|
||||||
transferable_only: false,
|
transferable_only: false,
|
||||||
|
|
Loading…
Reference in New Issue