diff --git a/src/main.rs b/src/main.rs index 6887d9f..b47c8eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,8 +64,12 @@ struct STV { // -- 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, + /// Method of exclusions - #[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["one_round", "by_value"], default_value="one_round", value_name="mode")] + #[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 -- @@ -109,7 +113,20 @@ where // Copy applicable options let stv_opts = stv::STVOptions { round_votes: cmd_opts.round_votes, - exclusion: &cmd_opts.exclusion, + surplus: match cmd_opts.surplus.as_str() { + "wig" => stv::SurplusMethod::WIG, + "uig" => stv::SurplusMethod::UIG, + "eg" => stv::SurplusMethod::EG, + "meek" => stv::SurplusMethod::Meek, + _ => panic!("Invalid --surplus"), + }, + exclusion: match cmd_opts.exclusion.as_str() { + "single_stage" => stv::ExclusionMethod::SingleStage, + "by_value" => stv::ExclusionMethod::ByValue, + "parcels_by_order" => stv::ExclusionMethod::ParcelsByOrder, + _ => panic!("Invalid --exclusion"), + }, + pp_decimals: cmd_opts.pp_decimals, }; // Initialise count state diff --git a/src/stv/mod.rs b/src/stv/mod.rs index cb04448..13874b2 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -23,17 +23,39 @@ pub mod wasm; use crate::numbers::Number; use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; +use wasm_bindgen::prelude::wasm_bindgen; + use std::collections::HashMap; use std::ops; -pub struct STVOptions<'a> { +#[wasm_bindgen] +pub struct STVOptions { pub round_votes: Option, - pub exclusion: &'a str, + pub surplus: SurplusMethod, + pub exclusion: ExclusionMethod, + pub pp_decimals: usize, } -pub fn count_init(mut state: &mut CountState<'_, N>, _opts: &STVOptions) { +#[wasm_bindgen] +#[derive(Clone, Copy)] +pub enum SurplusMethod { + WIG, + UIG, + EG, + Meek, +} + +#[wasm_bindgen] +#[derive(Clone, Copy)] +pub enum ExclusionMethod { + SingleStage, + ByValue, + ParcelsByOrder, +} + +pub fn count_init(mut state: &mut CountState<'_, N>, opts: &STVOptions) { distribute_first_preferences(&mut state); - calculate_quota(&mut state); + calculate_quota(&mut state, opts); elect_meeting_quota(&mut state); } @@ -173,12 +195,12 @@ fn distribute_first_preferences(state: &mut CountState) { state.logger.log_literal("First preferences distributed.".to_string()); } -fn calculate_quota(state: &mut CountState) { +fn calculate_quota(state: &mut CountState, opts: &STVOptions) { let mut log = String::new(); // Calculate the total vote state.quota = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); - log.push_str(format!("{:.2} usable votes, so the quota is ", state.quota).as_str()); + 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); @@ -186,7 +208,7 @@ fn calculate_quota(state: &mut CountState) { // TODO: Different rounding rules state.quota += N::one(); state.quota.floor_mut(0); - log.push_str(format!("{:.2}.", state.quota).as_str()); + log.push_str(format!("{:.dps$}.", state.quota, dps=opts.pp_decimals).as_str()); state.logger.log_literal(log); } @@ -251,21 +273,42 @@ where let count_card = state.candidates.get(elected_candidate).unwrap(); let surplus = &count_card.votes - &state.quota; - // Inclusive Gregory - // TODO: Other methods - let votes = state.candidates.get(elected_candidate).unwrap().parcels.concat(); + let votes; + match opts.surplus { + SurplusMethod::WIG | SurplusMethod::UIG => { + // Inclusive Gregory + votes = state.candidates.get(elected_candidate).unwrap().parcels.concat(); + } + SurplusMethod::EG => { + // Exclusive Gregory + // Should be safe to unwrap() - or else how did we get a quota! + votes = state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap(); + } + SurplusMethod::Meek => { + todo!(); + } + } // Count next preferences let result = next_preferences(state, votes); // Transfer candidate votes - // Unweighted inclusive Gregory - // TODO: Other methods - let transfer_value = surplus.clone() / &result.total_ballots; + 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 {:.2}.", elected_candidate.name, transfer_value)); + state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps$}.", elected_candidate.name, transfer_value, dps=opts.pp_decimals)); let mut checksum = N::new(); @@ -413,39 +456,42 @@ where // Determine votes to transfer in this stage let mut votes; - let votes_remaining; + let votes_remain; - if opts.exclusion == "one_round" { - // Exclude in one round - votes = count_card.parcels.concat(); - votes_remaining = 0; - - } else if opts.exclusion == "by_value" { - // Exclude by value - let all_votes = count_card.parcels.concat(); - - // TODO: Write a multiple min/max function - let min_value = all_votes.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap(); - - votes = Vec::new(); - let mut remaining_votes = Vec::new(); - - // This could be implemented using Vec.drain_filter, but that is experimental currently - for vote in all_votes.into_iter() { - if &vote.value / &vote.ballot.orig_value == min_value { - votes.push(vote); - } else { - remaining_votes.push(vote); - } + match opts.exclusion { + ExclusionMethod::SingleStage => { + // Exclude in one round + votes = count_card.parcels.concat(); + votes_remain = false; + } + ExclusionMethod::ByValue => { + // Exclude by value + let all_votes = count_card.parcels.concat(); + + // TODO: Write a multiple min/max function + let min_value = all_votes.iter().map(|v| &v.value / &v.ballot.orig_value).max().unwrap(); + + votes = Vec::new(); + let mut remaining_votes = Vec::new(); + + // This could be implemented using Vec.drain_filter, but that is experimental currently + for vote in all_votes.into_iter() { + if &vote.value / &vote.ballot.orig_value == min_value { + votes.push(vote); + } else { + remaining_votes.push(vote); + } + } + + votes_remain = remaining_votes.len() > 0; + // Leave remaining votes with candidate (as one parcel) + count_card.parcels = vec![remaining_votes]; + } + ExclusionMethod::ParcelsByOrder => { + // Exclude by parcel by order + votes = count_card.parcels.remove(0); + votes_remain = count_card.parcels.len() > 0; } - - votes_remaining = remaining_votes.len(); - // Leave remaining votes with candidate (as one parcel) - count_card.parcels = vec![remaining_votes]; - - } else { - // TODO: Exclude by parcel - panic!("Invalid --exclusion"); } let mut checksum = N::new(); @@ -456,10 +502,10 @@ where // Count next preferences let result = next_preferences(state, votes); - if opts.exclusion == "one_round" { - state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.2} votes.", result.total_ballots, result.total_votes)); - } else if opts.exclusion == "by_value" { - state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.2} votes, received at value {:.2}.", result.total_ballots, result.total_votes, value)); + if let ExclusionMethod::SingleStage = opts.exclusion { + state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", result.total_ballots, result.total_votes, dps=opts.pp_decimals)); + } else { + state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps$}.", result.total_ballots, result.total_votes, value, dps=opts.pp_decimals)); } // Transfer candidate votes @@ -488,7 +534,7 @@ where state.exhausted.transfer(&exhausted_transfers); checksum += exhausted_transfers; - if votes_remaining > 0 { + if votes_remain { // Subtract from candidate tally let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); checksum -= &result.total_votes; @@ -498,7 +544,7 @@ where } } - if votes_remaining == 0 { + if !votes_remain { // Finalise candidate votes let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); checksum -= &count_card.votes; @@ -508,7 +554,8 @@ where // Update loss by fraction state.loss_fraction.transfer(&-checksum); - if opts.exclusion != "one_round" { + if let ExclusionMethod::SingleStage = opts.exclusion { + } else { state.logger.log_literal("Exclusion complete.".to_string()); } } diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 50a5b7d..2a075dc 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -30,6 +30,7 @@ extern "C" { #[wasm_bindgen(js_namespace = console)] fn log(s: &str); } +/// println! to console macro_rules! cprintln { ($($t:tt)*) => ( #[allow(unused_unsafe)] @@ -55,14 +56,14 @@ macro_rules! impl_type { #[wasm_bindgen] #[allow(non_snake_case)] - pub fn [](state: &mut [], opts: &STVOptions) { - stv::count_init(&mut state.0, &opts.0); + pub fn [](state: &mut [], opts: &stv::STVOptions) { + stv::count_init(&mut state.0, &opts); } #[wasm_bindgen] #[allow(non_snake_case)] - pub fn [](state: &mut [], opts: &STVOptions) -> bool { - return stv::count_one_stage(&mut state.0, &opts.0); + pub fn [](state: &mut [], opts: &stv::STVOptions) -> bool { + return stv::count_one_stage(&mut state.0, &opts); } // Reporting @@ -154,8 +155,9 @@ fn print_stage(stage_num: usize, result: &StageResult) { cprintln!(""); } +/* #[wasm_bindgen] -pub struct STVOptions(stv::STVOptions<'static>); +pub struct STVOptions(stv::STVOptions); #[wasm_bindgen] impl STVOptions { pub fn new(round_votes: Option, exclusion: String) -> Self { @@ -163,9 +165,11 @@ impl STVOptions { return STVOptions(stv::STVOptions { round_votes: round_votes, exclusion: &"one_round", + pp_decimals: 2, }); } else { panic!("Unknown --exclusion"); } } } +*/ diff --git a/tests/aec.rs b/tests/aec.rs index 17d90a9..7473a32 100644 --- a/tests/aec.rs +++ b/tests/aec.rs @@ -55,7 +55,9 @@ fn aec_tas19_rational() { // Initialise options let stv_opts = stv::STVOptions { round_votes: Some(0), - exclusion: "by_value", + surplus: stv::SurplusMethod::UIG, + exclusion: stv::ExclusionMethod::ByValue, + pp_decimals: 2, }; // Initialise count state