From e88d2674d68c5f116357583a3e67f783464161ca Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 2 Jun 2021 18:07:05 +1000 Subject: [PATCH] Implement --quota, --quota-criterion --- src/main.rs | 15 +++++- src/numbers/mod.rs | 1 + src/numbers/native.rs | 5 ++ src/numbers/rational_num.rs | 11 +++++ src/numbers/rational_rug.rs | 11 +++++ src/stv/mod.rs | 94 +++++++++++++++++++++++++++++-------- tests/aec.rs | 2 + tests/prsa.rs | 2 + 8 files changed, 121 insertions(+), 20 deletions(-) diff --git a/src/main.rs b/src/main.rs index ffaf5b7..54c1587 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,11 +77,22 @@ struct STV { #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] round_quota: Option, + // ----------- + // -- Quota -- + + /// Quota type + #[clap(help_heading=Some("QUOTA"), short, long, possible_values=&["droop", "hare", "droop_exact", "hare_exact"], default_value="droop_exact")] + quota: String, + + /// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota + #[clap(help_heading=Some("QUOTA"), short='c', long, possible_values=&["geq", "gt"], default_value="gt", value_name="criterion")] + quota_criterion: String, + // ------------------ // -- STV variants -- /// Method of surplus transfers - #[clap(help_heading=Some("STV VARIANTS"), 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, #[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["by_size", "by_order"], default_value="by_size", value_name="order")] @@ -142,6 +153,8 @@ where cmd_opts.round_weights, cmd_opts.round_votes, cmd_opts.round_quota, + &cmd_opts.quota, + &cmd_opts.quota_criterion, &cmd_opts.surplus, &cmd_opts.surplus_order, cmd_opts.transferable_only, diff --git a/src/numbers/mod.rs b/src/numbers/mod.rs index ad0066d..b2238b0 100644 --- a/src/numbers/mod.rs +++ b/src/numbers/mod.rs @@ -47,6 +47,7 @@ where fn pow_assign(&mut self, exponent: i32); fn floor_mut(&mut self, dps: usize); + fn ceil_mut(&mut self, dps: usize); fn parse(s: &str) -> Self { if let Ok(value) = Self::from_str_radix(s, 10) { diff --git a/src/numbers/native.rs b/src/numbers/native.rs index c136850..fe5d05a 100644 --- a/src/numbers/native.rs +++ b/src/numbers/native.rs @@ -39,6 +39,11 @@ impl Number for NativeFloat64 { let factor = 10.0_f64.powi(dps as i32); self.0 = (self.0 * factor).floor() / factor; } + + fn ceil_mut(&mut self, dps: usize) { + let factor = 10.0_f64.powi(dps as i32); + self.0 = (self.0 * factor).ceil() / factor; + } } impl Num for NativeFloat64 { diff --git a/src/numbers/rational_num.rs b/src/numbers/rational_num.rs index d161368..5013c00 100644 --- a/src/numbers/rational_num.rs +++ b/src/numbers/rational_num.rs @@ -44,6 +44,17 @@ impl Number for Rational { self.0 /= factor; } } + + fn ceil_mut(&mut self, dps: usize) { + if dps == 0 { + self.0 = self.0.ceil(); + } else { + let factor = BigRational::from_integer(BigInt::from(10)).pow(dps as i32); + self.0 *= &factor; + self.0 = self.0.ceil(); + self.0 /= factor; + } + } } impl Num for Rational { diff --git a/src/numbers/rational_rug.rs b/src/numbers/rational_rug.rs index 0411b3b..6097a99 100644 --- a/src/numbers/rational_rug.rs +++ b/src/numbers/rational_rug.rs @@ -44,6 +44,17 @@ impl Number for Rational { self.0 /= factor; } } + + fn ceil_mut(&mut self, dps: usize) { + if dps == 0 { + self.0.ceil_mut(); + } else { + let factor = rug::Rational::from(10).pow(dps as u32); + self.0 *= &factor; + self.0.ceil_mut(); + self.0 /= factor; + } + } } impl Num for Rational { diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 863ec21..3559e14 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -34,6 +34,8 @@ pub struct STVOptions { pub round_weights: Option, pub round_votes: Option, pub round_quota: Option, + pub quota: QuotaType, + pub quota_criterion: QuotaCriterion, pub surplus: SurplusMethod, pub surplus_order: SurplusOrder, pub transferable_only: bool, @@ -48,6 +50,8 @@ impl STVOptions { round_weights: Option, round_votes: Option, round_quota: Option, + quota: &str, + quota_criterion: &str, surplus: &str, surplus_order: &str, transferable_only: bool, @@ -59,6 +63,18 @@ impl STVOptions { round_weights, round_votes, round_quota, + quota: match quota { + "droop" => QuotaType::Droop, + "hare" => QuotaType::Hare, + "droop_exact" => QuotaType::DroopExact, + "hare_exact" => QuotaType::HareExact, + _ => panic!("Invalid --quota"), + }, + quota_criterion: match quota_criterion { + "geq" => QuotaCriterion::GreaterOrEqual, + "gt" => QuotaCriterion::Greater, + _ => panic!("Invalid --quota-criterion"), + }, surplus: match surplus { "wig" => SurplusMethod::WIG, "uig" => SurplusMethod::UIG, @@ -83,6 +99,22 @@ impl STVOptions { } } +#[wasm_bindgen] +#[derive(Clone, Copy)] +pub enum QuotaType { + Droop, + Hare, + DroopExact, + HareExact, +} + +#[wasm_bindgen] +#[derive(Clone, Copy)] +pub enum QuotaCriterion { + GreaterOrEqual, + Greater, +} + #[wasm_bindgen] #[derive(Clone, Copy)] pub enum SurplusMethod { @@ -110,7 +142,7 @@ pub enum ExclusionMethod { 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); + elect_meeting_quota(&mut state, opts); } pub fn count_one_stage(mut state: &mut CountState<'_, N>, opts: &STVOptions) -> bool @@ -129,25 +161,25 @@ where // Continue exclusions if continue_exclusion(&mut state, &opts) { - elect_meeting_quota(&mut state); + elect_meeting_quota(&mut state, opts); return false; } // Distribute surpluses if distribute_surpluses(&mut state, &opts) { - elect_meeting_quota(&mut state); + elect_meeting_quota(&mut state, opts); return false; } // Attempt bulk election if bulk_elect(&mut state) { - elect_meeting_quota(&mut state); + elect_meeting_quota(&mut state, opts); return false; } // Exclude lowest hopeful if exclude_hopefuls(&mut state, &opts) { - elect_meeting_quota(&mut state); + elect_meeting_quota(&mut state, opts); return false; } @@ -256,17 +288,35 @@ fn calculate_quota(state: &mut CountState, opts: &STVOptions) { state.quota = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); log.push_str(format!("{:.dps$} usable votes, so the quota is ", state.quota, dps=opts.pp_decimals).as_str()); - // TODO: Different quotas - state.quota /= N::from(state.election.seats + 1); + match opts.quota { + QuotaType::Droop | QuotaType::DroopExact => { + state.quota /= N::from(state.election.seats + 1); + } + QuotaType::Hare | QuotaType::HareExact => { + state.quota /= N::from(state.election.seats); + } + } - // Increment to next available increment if let Some(dps) = opts.round_quota { - let mut factor = N::from(10); - factor.pow_assign(dps as i32); - state.quota *= &factor; - state.quota.floor_mut(0); - state.quota += N::one(); - state.quota /= factor; + match opts.quota { + QuotaType::Droop | QuotaType::Hare => { + // Increment to next available increment + let mut factor = N::from(10); + factor.pow_assign(dps as i32); + state.quota *= &factor; + state.quota.floor_mut(0); + state.quota += N::one(); + state.quota /= factor; + } + QuotaType::DroopExact | QuotaType::HareExact => { + // Round up to next available increment if necessary + let mut factor = N::from(10); + factor.pow_assign(dps as i32); + state.quota *= &factor; + state.quota.ceil_mut(0); + state.quota /= factor; + } + } } log.push_str(format!("{:.dps$}.", state.quota, dps=opts.pp_decimals).as_str()); @@ -274,15 +324,21 @@ fn calculate_quota(state: &mut CountState, opts: &STVOptions) { state.logger.log_literal(log); } -fn meets_quota(quota: &N, count_card: &CountCard) -> bool { - // TODO: Different quota rules - return count_card.votes >= *quota; +fn meets_quota(quota: &N, count_card: &CountCard, opts: &STVOptions) -> bool { + match opts.quota_criterion { + QuotaCriterion::GreaterOrEqual => { + return count_card.votes >= *quota; + } + QuotaCriterion::Greater => { + return count_card.votes > *quota; + } + } } -fn elect_meeting_quota(state: &mut CountState) { +fn elect_meeting_quota(state: &mut CountState, opts: &STVOptions) { let quota = &state.quota; // Have to do this or else the borrow checker gets confused let mut cands_meeting_quota: Vec<(&&Candidate, &mut CountCard)> = state.candidates.iter_mut() - .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL && meets_quota(quota, cc)) + .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL && meets_quota(quota, cc, opts)) .collect(); if cands_meeting_quota.len() > 0 { diff --git a/tests/aec.rs b/tests/aec.rs index ec5792c..9e5a903 100644 --- a/tests/aec.rs +++ b/tests/aec.rs @@ -59,6 +59,8 @@ fn aec_tas19_rational() { round_weights: None, round_votes: Some(0), round_quota: Some(0), + quota: stv::QuotaType::Droop, + quota_criterion: stv::QuotaCriterion::GreaterOrEqual, surplus: stv::SurplusMethod::UIG, surplus_order: stv::SurplusOrder::ByOrder, transferable_only: false, diff --git a/tests/prsa.rs b/tests/prsa.rs index 0cc78a2..9d30fca 100644 --- a/tests/prsa.rs +++ b/tests/prsa.rs @@ -27,6 +27,8 @@ fn prsa1_rational() { round_weights: Some(3), round_votes: Some(3), round_quota: Some(3), + quota: stv::QuotaType::Droop, + quota_criterion: stv::QuotaCriterion::GreaterOrEqual, surplus: stv::SurplusMethod::EG, surplus_order: stv::SurplusOrder::ByOrder, transferable_only: true,