From 76d69913c7fec5e89d9c61d89f6b62a35c2ae5dd Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Mon, 31 May 2021 23:17:21 +1000 Subject: [PATCH] Implement --transferable-only --- src/main.rs | 12 +++++++ src/stv/mod.rs | 94 ++++++++++++++++++++++++++++++++++++++++---------- tests/aec.rs | 1 + 3 files changed, 88 insertions(+), 19 deletions(-) diff --git a/src/main.rs b/src/main.rs index b47c8eb..fa166cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,41 +45,52 @@ enum Command { #[derive(Clap)] #[clap(setting=AppSettings::DeriveDisplayOrder)] struct STV { + // ---------------- // -- File input -- /// Path to the BLT file to be counted filename: String, + // ---------------------- // -- Numbers settings -- /// Numbers mode #[clap(help_heading=Some("NUMBERS"), short, long, possible_values=&["rational", "float64"], default_value="rational", value_name="mode")] numbers: String, + // ----------------------- // -- Rounding settings -- /// Round votes to specified decimal places #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] round_votes: Option, + // ------------------ // -- 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")] surplus: String, + /// Examine only transferable papers during surplus distributions + #[clap(help_heading=Some("STV VARIANTS"), long)] + transferable_only: bool, + /// Method of exclusions #[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "parcels_by_order"], default_value="single_stage", value_name="method")] exclusion: String, + // ---------------------- // -- Display settings -- /// Hide excluded candidates from results report #[clap(help_heading=Some("DISPLAY"), long)] hide_excluded: bool, + /// Sort candidates by votes in results report #[clap(help_heading=Some("DISPLAY"), long)] sort_votes: bool, + /// Print votes to specified decimal places in results report #[clap(help_heading=Some("DISPLAY"), long, default_value="2", value_name="dps")] pp_decimals: usize, @@ -120,6 +131,7 @@ where "meek" => stv::SurplusMethod::Meek, _ => panic!("Invalid --surplus"), }, + transferable_only: cmd_opts.transferable_only, exclusion: match cmd_opts.exclusion.as_str() { "single_stage" => stv::ExclusionMethod::SingleStage, "by_value" => stv::ExclusionMethod::ByValue, diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 13874b2..148d241 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -32,6 +32,7 @@ use std::ops; pub struct STVOptions { pub round_votes: Option, pub surplus: SurplusMethod, + pub transferable_only: bool, pub exclusion: ExclusionMethod, pub pp_decimals: usize, } @@ -265,6 +266,45 @@ where return false; } +/// Return the denominator of the transfer value +fn calculate_transfer_denom(surplus: &N, result: &NextPreferencesResult, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option +where + for<'r> &'r N: ops::Sub<&'r N, Output=N> +{ + if transferable_only { + let total_units = if weighted { &result.total_votes } else { &result.total_ballots }; + let exhausted_units = if weighted { &result.exhausted.num_votes } else { &result.exhausted.num_ballots }; + let transferable_units = total_units - exhausted_units; + + if transferable_votes > surplus { + return Some(transferable_units); + } else { + return None; + } + } else { + if weighted { + return Some(result.total_votes.clone()); + } else { + return Some(result.total_ballots.clone()); + } + } +} + +fn reweight_vote(num_votes: &N, num_ballots: &N, surplus: &N, weighted: bool, transfer_denom: &Option) -> N { + match transfer_denom { + Some(v) => { + if weighted { + return num_votes.clone() * surplus / v; + } else { + return num_ballots.clone() * surplus / v; + } + } + None => { + return num_votes.clone(); + } + } +} + fn distribute_surplus(state: &mut CountState, opts: &STVOptions, elected_candidate: &Candidate) where for<'r> &'r N: ops::Sub<&'r N, Output=N>, @@ -292,23 +332,29 @@ where // Count next preferences let result = next_preferences(state, votes); - // Transfer candidate votes - let transfer_value; - match opts.surplus { - SurplusMethod::WIG => { - // Weighted inclusive Gregory - transfer_value = surplus.clone() / &result.total_votes; - } - SurplusMethod::UIG | SurplusMethod::EG => { - // Unweighted inclusive Gregory - transfer_value = surplus.clone() / &result.total_ballots; - } - SurplusMethod::Meek => { todo!(); } - } - state.kind = Some("Surplus of"); state.title = String::from(&elected_candidate.name); - state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps$}.", elected_candidate.name, transfer_value, dps=opts.pp_decimals)); + + // Transfer candidate votes + // TODO: Refactor?? + let is_weighted = match opts.surplus { + SurplusMethod::WIG => { true } + SurplusMethod::UIG | SurplusMethod::EG => { false } + SurplusMethod::Meek => { todo!() } + }; + + let transferable_votes = &result.total_votes - &result.exhausted.num_votes; + let transfer_denom = calculate_transfer_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only); + let transfer_value; + match transfer_denom { + Some(ref v) => { + transfer_value = surplus.clone() / v; + state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps$}.", elected_candidate.name, transfer_value, dps=opts.pp_decimals)); + } + None => { + state.logger.log_literal(format!("Surplus of {} distributed at values received.", elected_candidate.name)); + } + } let mut checksum = N::new(); @@ -317,14 +363,13 @@ where // Reweight votes for vote in parcel.iter_mut() { - //vote.value = vote.ballot.orig_value.clone() * &transfer_value; - vote.value = vote.ballot.orig_value.clone() * &surplus / &result.total_ballots; + vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &transfer_denom); } let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); - let mut candidate_transfers = entry.num_ballots * &surplus / &result.total_ballots; + let mut candidate_transfers = reweight_vote(&entry.num_votes, &entry.num_ballots, &surplus, is_weighted, &transfer_denom); // Round transfers if let Some(dps) = opts.round_votes { candidate_transfers.floor_mut(dps); @@ -337,7 +382,18 @@ where let parcel = result.exhausted.votes as Parcel; state.exhausted.parcels.push(parcel); - let mut exhausted_transfers = result.exhausted.num_ballots * &surplus / &result.total_ballots; + let mut exhausted_transfers; + if opts.transferable_only { + if transferable_votes > surplus { + // No ballots exhaust + exhausted_transfers = N::new(); + } else { + exhausted_transfers = &surplus - &transferable_votes; + } + } else { + exhausted_transfers = reweight_vote(&result.exhausted.num_votes, &result.exhausted.num_ballots, &surplus, is_weighted, &transfer_denom); + } + if let Some(dps) = opts.round_votes { exhausted_transfers.floor_mut(dps); } diff --git a/tests/aec.rs b/tests/aec.rs index 7473a32..9972d59 100644 --- a/tests/aec.rs +++ b/tests/aec.rs @@ -56,6 +56,7 @@ fn aec_tas19_rational() { let stv_opts = stv::STVOptions { round_votes: Some(0), surplus: stv::SurplusMethod::UIG, + transferable_only: false, exclusion: stv::ExclusionMethod::ByValue, pp_decimals: 2, };