From 77cf60c21f91076cf619c438c904f519ad853829 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 29 May 2021 17:51:45 +1000 Subject: [PATCH] Allow configuring --round-votes Fix bug with display of negative Rational's --- src/main.rs | 74 ++++++++----------- src/numbers/mod.rs | 4 +- src/numbers/native.rs | 157 +++++++++++++++++++++------------------- src/numbers/rational.rs | 20 ++++- src/stv/mod.rs | 98 +++++++++++++++++++------ 5 files changed, 207 insertions(+), 146 deletions(-) diff --git a/src/main.rs b/src/main.rs index 07fd613..9c0a264 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,9 +21,9 @@ mod numbers; mod stv; use crate::election::{Candidate, CandidateState, CountCard, CountState, CountStateOrRef, Election, StageResult}; -use crate::numbers::{NativeFloat, Number, Rational}; +use crate::numbers::{NativeFloat64, Number, Rational}; -use clap::Clap; +use clap::{AppSettings, Clap}; use git_version::git_version; use std::fs::File; @@ -47,12 +47,27 @@ enum Command { /// Count a single transferable vote (STV) election #[derive(Clap)] +#[clap(setting=AppSettings::DeriveDisplayOrder, setting=AppSettings::UnifiedHelpMessage)] struct STV { + // -- File input -- + /// Path to the BLT file to be counted filename: String, + + // -- Numbers settings -- + /// Numbers mode - #[clap(short, long, possible_values(&["rational", "native"]), default_value="rational")] + #[clap(short, long, possible_values(&["rational", "float64"]), default_value="rational")] numbers: String, + + // -- Rounding settings -- + + /// Round votes to specified decimal places + #[clap(long)] + round_votes: Option, + + // -- Display settings -- + /// Hide excluded candidates from results report #[clap(long)] hide_excluded: bool, @@ -77,8 +92,8 @@ fn main() { if cmd_opts.numbers == "rational" { let election: Election = Election::from_blt(lines); count_election(election, cmd_opts); - } else if cmd_opts.numbers == "native" { - let election: Election = Election::from_blt(lines); + } else if cmd_opts.numbers == "float64" { + let election: Election = Election::from_blt(lines); count_election(election, cmd_opts); } } @@ -88,57 +103,28 @@ where for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Neg { + // Copy applicable options + let stv_opts = stv::STVOptions { + round_votes: cmd_opts.round_votes, + }; + // Initialise count state let mut state = CountState::new(&election); // Distribute first preferences - stv::distribute_first_preferences(&mut state); - stv::calculate_quota(&mut state); - stv::elect_meeting_quota(&mut state); + stv::count_init(&mut state, &stv_opts); // Display let mut stage_num = 1; make_and_print_result(stage_num, &state, &cmd_opts); loop { - state.logger.entries.clear(); - state.step_all(); - stage_num += 1; - - // Finish count - if stv::finished_before_stage(&state) { + let is_done = stv::count_one_stage(&mut state, &stv_opts); + if is_done { break; } - - // Continue exclusions - if stv::continue_exclusion(&mut state) { - stv::elect_meeting_quota(&mut state); - make_and_print_result(stage_num, &state, &cmd_opts); - continue; - } - - // Distribute surpluses - if stv::distribute_surpluses(&mut state) { - stv::elect_meeting_quota(&mut state); - make_and_print_result(stage_num, &state, &cmd_opts); - continue; - } - - // Attempt bulk election - if stv::bulk_elect(&mut state) { - stv::elect_meeting_quota(&mut state); - make_and_print_result(stage_num, &state, &cmd_opts); - continue; - } - - // Exclude lowest hopeful - if stv::exclude_hopefuls(&mut state) { - stv::elect_meeting_quota(&mut state); - make_and_print_result(stage_num, &state, &cmd_opts); - continue; - } - - todo!(); + stage_num += 1; + make_and_print_result(stage_num, &state, &cmd_opts); } println!("Count complete. The winning candidates are, in order of election:"); diff --git a/src/numbers/mod.rs b/src/numbers/mod.rs index 8d110f0..1b8952d 100644 --- a/src/numbers/mod.rs +++ b/src/numbers/mod.rs @@ -30,7 +30,7 @@ pub trait Number: NumRef + NumAssignRef + ops::Neg + PartialOrd + A fn new() -> Self; fn from(n: usize) -> Self; - fn floor_mut(&mut self); + fn floor_mut(&mut self, dps: usize); fn parse(s: &str) -> Self { if let Ok(value) = Self::from_str_radix(s, 10) { @@ -41,5 +41,5 @@ pub trait Number: NumRef + NumAssignRef + ops::Neg + PartialOrd + A } } -pub use self::native::NativeFloat; +pub use self::native::NativeFloat64; pub use self::rational::Rational; diff --git a/src/numbers/native.rs b/src/numbers/native.rs index a89c947..a8a7bea 100644 --- a/src/numbers/native.rs +++ b/src/numbers/native.rs @@ -25,218 +25,223 @@ use std::num::ParseIntError; use std::fmt; use std::ops; -pub struct NativeFloat(f32); +type ImplType = f64; -impl Number for NativeFloat { +pub struct NativeFloat64(ImplType); + +impl Number for NativeFloat64 { fn new() -> Self { Self(0.0) } - fn from(n: usize) -> Self { Self(n as f32) } + fn from(n: usize) -> Self { Self(n as ImplType) } - fn floor_mut(&mut self) { self.0 = self.0.floor() } + fn floor_mut(&mut self, dps: usize) { + let factor = 10.0_f64.powi(dps as i32); + self.0 = (self.0 * factor).floor() / factor; + } } -impl Num for NativeFloat { +impl Num for NativeFloat64 { type FromStrRadixErr = ParseIntError; fn from_str_radix(str: &str, radix: u32) -> Result { - match i32::from_str_radix(str, radix) { - Ok(value) => Ok(Self(value as f32)), + match i64::from_str_radix(str, radix) { + Ok(value) => Ok(Self(value as ImplType)), Err(err) => Err(err) } } } -impl Assign for NativeFloat { +impl Assign for NativeFloat64 { fn assign(&mut self, src: Self) { self.0 = src.0 } } -impl Assign<&NativeFloat> for NativeFloat { - fn assign(&mut self, src: &NativeFloat) { self.0 = src.0 } +impl Assign<&NativeFloat64> for NativeFloat64 { + fn assign(&mut self, src: &NativeFloat64) { self.0 = src.0 } } -impl Clone for NativeFloat { +impl Clone for NativeFloat64 { fn clone(&self) -> Self { Self(self.0) } } -impl fmt::Display for NativeFloat { +impl fmt::Display for NativeFloat64 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } -impl One for NativeFloat { +impl One for NativeFloat64 { fn one() -> Self { Self(1.0) } } -impl Zero for NativeFloat { +impl Zero for NativeFloat64 { fn zero() -> Self { Self::new() } fn is_zero(&self) -> bool { self.0.is_zero() } } -impl PartialEq for NativeFloat { +impl PartialEq for NativeFloat64 { fn eq(&self, _other: &Self) -> bool { todo!() } } -impl PartialOrd for NativeFloat { +impl PartialOrd for NativeFloat64 { fn partial_cmp(&self, other: &Self) -> Option { self.0.partial_cmp(&other.0) } } -impl ops::Neg for NativeFloat { - type Output = NativeFloat; +impl ops::Neg for NativeFloat64 { + type Output = NativeFloat64; fn neg(self) -> Self::Output { Self(-self.0) } } -impl ops::Add for NativeFloat { - type Output = NativeFloat; +impl ops::Add for NativeFloat64 { + type Output = NativeFloat64; fn add(self, _rhs: Self) -> Self::Output { todo!() } } -impl ops::Sub for NativeFloat { - type Output = NativeFloat; +impl ops::Sub for NativeFloat64 { + type Output = NativeFloat64; fn sub(self, _rhs: Self) -> Self::Output { todo!() } } -impl ops::Mul for NativeFloat { - type Output = NativeFloat; +impl ops::Mul for NativeFloat64 { + type Output = NativeFloat64; fn mul(self, _rhs: Self) -> Self::Output { todo!() } } -impl ops::Div for NativeFloat { - type Output = NativeFloat; +impl ops::Div for NativeFloat64 { + type Output = NativeFloat64; fn div(self, _rhs: Self) -> Self::Output { todo!() } } -impl ops::Rem for NativeFloat { - type Output = NativeFloat; +impl ops::Rem for NativeFloat64 { + type Output = NativeFloat64; fn rem(self, _rhs: Self) -> Self::Output { todo!() } } -impl ops::Add<&NativeFloat> for NativeFloat { - type Output = NativeFloat; - fn add(self, rhs: &NativeFloat) -> Self::Output { Self(self.0 + &rhs.0) } +impl ops::Add<&NativeFloat64> for NativeFloat64 { + type Output = NativeFloat64; + fn add(self, rhs: &NativeFloat64) -> Self::Output { Self(self.0 + &rhs.0) } } -impl ops::Sub<&NativeFloat> for NativeFloat { - type Output = NativeFloat; - fn sub(self, _rhs: &NativeFloat) -> Self::Output { +impl ops::Sub<&NativeFloat64> for NativeFloat64 { + type Output = NativeFloat64; + fn sub(self, _rhs: &NativeFloat64) -> Self::Output { todo!() } } -impl ops::Mul<&NativeFloat> for NativeFloat { - type Output = NativeFloat; - fn mul(self, rhs: &NativeFloat) -> Self::Output { NativeFloat(self.0 * &rhs.0) } +impl ops::Mul<&NativeFloat64> for NativeFloat64 { + type Output = NativeFloat64; + fn mul(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 * &rhs.0) } } -impl ops::Div<&NativeFloat> for NativeFloat { - type Output = NativeFloat; - fn div(self, rhs: &NativeFloat) -> Self::Output { NativeFloat(self.0 / &rhs.0) } +impl ops::Div<&NativeFloat64> for NativeFloat64 { + type Output = NativeFloat64; + fn div(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(self.0 / &rhs.0) } } -impl ops::Rem<&NativeFloat> for NativeFloat { - type Output = NativeFloat; - fn rem(self, _rhs: &NativeFloat) -> Self::Output { +impl ops::Rem<&NativeFloat64> for NativeFloat64 { + type Output = NativeFloat64; + fn rem(self, _rhs: &NativeFloat64) -> Self::Output { todo!() } } -impl ops::AddAssign for NativeFloat { +impl ops::AddAssign for NativeFloat64 { fn add_assign(&mut self, rhs: Self) { self.0 += rhs.0 } } -impl ops::SubAssign for NativeFloat { +impl ops::SubAssign for NativeFloat64 { fn sub_assign(&mut self, rhs: Self) { self.0 -= rhs.0 } } -impl ops::MulAssign for NativeFloat { +impl ops::MulAssign for NativeFloat64 { fn mul_assign(&mut self, _rhs: Self) { todo!() } } -impl ops::DivAssign for NativeFloat { +impl ops::DivAssign for NativeFloat64 { fn div_assign(&mut self, rhs: Self) { self.0 /= &rhs.0; } } -impl ops::RemAssign for NativeFloat { +impl ops::RemAssign for NativeFloat64 { fn rem_assign(&mut self, _rhs: Self) { todo!() } } -impl ops::AddAssign<&NativeFloat> for NativeFloat { - fn add_assign(&mut self, rhs: &NativeFloat) { self.0 += &rhs.0 } +impl ops::AddAssign<&NativeFloat64> for NativeFloat64 { + fn add_assign(&mut self, rhs: &NativeFloat64) { self.0 += &rhs.0 } } -impl ops::SubAssign<&NativeFloat> for NativeFloat { - fn sub_assign(&mut self, rhs: &NativeFloat) { self.0 -= &rhs.0 } +impl ops::SubAssign<&NativeFloat64> for NativeFloat64 { + fn sub_assign(&mut self, rhs: &NativeFloat64) { self.0 -= &rhs.0 } } -impl ops::MulAssign<&NativeFloat> for NativeFloat { - fn mul_assign(&mut self, _rhs: &NativeFloat) { +impl ops::MulAssign<&NativeFloat64> for NativeFloat64 { + fn mul_assign(&mut self, _rhs: &NativeFloat64) { todo!() } } -impl ops::DivAssign<&NativeFloat> for NativeFloat { - fn div_assign(&mut self, _rhs: &NativeFloat) { +impl ops::DivAssign<&NativeFloat64> for NativeFloat64 { + fn div_assign(&mut self, _rhs: &NativeFloat64) { todo!() } } -impl ops::RemAssign<&NativeFloat> for NativeFloat { - fn rem_assign(&mut self, _rhs: &NativeFloat) { +impl ops::RemAssign<&NativeFloat64> for NativeFloat64 { + fn rem_assign(&mut self, _rhs: &NativeFloat64) { todo!() } } -impl ops::Neg for &NativeFloat { - type Output = NativeFloat; - fn neg(self) -> Self::Output { NativeFloat(-&self.0) } +impl ops::Neg for &NativeFloat64 { + type Output = NativeFloat64; + fn neg(self) -> Self::Output { NativeFloat64(-&self.0) } } -impl ops::Add<&NativeFloat> for &NativeFloat { - type Output = NativeFloat; - fn add(self, _rhs: &NativeFloat) -> Self::Output { +impl ops::Add<&NativeFloat64> for &NativeFloat64 { + type Output = NativeFloat64; + fn add(self, _rhs: &NativeFloat64) -> Self::Output { todo!() } } -impl ops::Sub<&NativeFloat> for &NativeFloat { - type Output = NativeFloat; - fn sub(self, rhs: &NativeFloat) -> Self::Output { NativeFloat(&self.0 - &rhs.0) } +impl ops::Sub<&NativeFloat64> for &NativeFloat64 { + type Output = NativeFloat64; + fn sub(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 - &rhs.0) } } -impl ops::Mul<&NativeFloat> for &NativeFloat { - type Output = NativeFloat; - fn mul(self, _rhs: &NativeFloat) -> Self::Output { +impl ops::Mul<&NativeFloat64> for &NativeFloat64 { + type Output = NativeFloat64; + fn mul(self, _rhs: &NativeFloat64) -> Self::Output { todo!() } } -impl ops::Div<&NativeFloat> for &NativeFloat { - type Output = NativeFloat; - fn div(self, _rhs: &NativeFloat) -> Self::Output { +impl ops::Div<&NativeFloat64> for &NativeFloat64 { + type Output = NativeFloat64; + fn div(self, _rhs: &NativeFloat64) -> Self::Output { todo!() } } -impl ops::Rem<&NativeFloat> for &NativeFloat { - type Output = NativeFloat; - fn rem(self, _rhs: &NativeFloat) -> Self::Output { +impl ops::Rem<&NativeFloat64> for &NativeFloat64 { + type Output = NativeFloat64; + fn rem(self, _rhs: &NativeFloat64) -> Self::Output { todo!() } } diff --git a/src/numbers/rational.rs b/src/numbers/rational.rs index 50a1762..f2d2942 100644 --- a/src/numbers/rational.rs +++ b/src/numbers/rational.rs @@ -31,7 +31,16 @@ impl Number for Rational { fn from(n: usize) -> Self { Self(rug::Rational::from(n)) } - fn floor_mut(&mut self) { self.0.floor_mut() } + fn floor_mut(&mut self, dps: usize) { + if dps == 0 { + self.0.floor_mut(); + } else { + let factor = rug::Rational::from(10).pow(dps as u32); + self.0 *= &factor; + self.0.floor_mut(); + self.0 /= factor; + } + } } impl Num for Rational { @@ -65,7 +74,9 @@ impl fmt::Display for Rational { return f.write_str(&result); } else { let base = rug::Rational::from(10).pow(precision as u32); - let mut result = rug::Integer::from((&self.0 * base).round_ref()).to_string(); + let mut result = rug::Integer::from((&self.0 * base).abs().round_ref()).to_string(); + + let should_add_minus = (self.0 < 0) && result != "0"; // Add leading 0s result = format!("{0:0>1$}", result, precision + 1); @@ -73,6 +84,11 @@ impl fmt::Display for Rational { // Add the decimal point result.insert(result.len() - precision, '.'); + // Add the sign + if should_add_minus { + result.insert(0, '-'); + } + return f.write_str(&result); } } else { diff --git a/src/stv/mod.rs b/src/stv/mod.rs index d4e1a01..9acfdc6 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -23,6 +23,56 @@ use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, use std::collections::HashMap; use std::ops::{Neg, Sub}; +pub struct STVOptions { + pub round_votes: Option, +} + +pub fn count_init(mut state: &mut CountState<'_, N>, _opts: &STVOptions) { + distribute_first_preferences(&mut state); + calculate_quota(&mut state); + elect_meeting_quota(&mut state); +} + +pub fn count_one_stage(mut state: &mut CountState<'_, N>, opts: &STVOptions) -> bool +where + for<'r> &'r N: Sub<&'r N, Output=N>, + for<'r> &'r N: Neg +{ + state.logger.entries.clear(); + state.step_all(); + + // Finish count + if finished_before_stage(&state) { + return true; + } + + // Continue exclusions + if continue_exclusion(&mut state, &opts) { + elect_meeting_quota(&mut state); + return false; + } + + // Distribute surpluses + if distribute_surpluses(&mut state, &opts) { + elect_meeting_quota(&mut state); + return false; + } + + // Attempt bulk election + if bulk_elect(&mut state) { + elect_meeting_quota(&mut state); + return false; + } + + // Exclude lowest hopeful + if exclude_hopefuls(&mut state, &opts) { + elect_meeting_quota(&mut state); + return false; + } + + todo!(); +} + struct NextPreferencesResult<'a, N> { candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>, exhausted: NextPreferencesEntry<'a, N>, @@ -88,7 +138,7 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec(state: &mut CountState) { +fn distribute_first_preferences(state: &mut CountState) { let votes = state.election.ballots.iter().map(|b| Vote { ballot: b, value: b.orig_value.clone(), @@ -115,7 +165,7 @@ pub fn distribute_first_preferences(state: &mut CountState) { state.logger.log_literal("First preferences distributed.".to_string()); } -pub fn calculate_quota(state: &mut CountState) { +fn calculate_quota(state: &mut CountState) { let mut log = String::new(); // Calculate the total vote @@ -127,7 +177,7 @@ pub fn calculate_quota(state: &mut CountState) { // TODO: Different rounding rules state.quota += N::one(); - state.quota.floor_mut(); + state.quota.floor_mut(0); log.push_str(format!("{:.2}.", state.quota).as_str()); state.logger.log_literal(log); @@ -138,7 +188,7 @@ fn meets_quota(quota: &N, count_card: &CountCard) -> bool { return count_card.votes >= *quota; } -pub fn elect_meeting_quota(state: &mut CountState) { +fn elect_meeting_quota(state: &mut CountState) { 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)) @@ -162,7 +212,7 @@ pub fn elect_meeting_quota(state: &mut CountState) { } } -pub fn distribute_surpluses(state: &mut CountState) -> bool +fn distribute_surpluses(state: &mut CountState, opts: &STVOptions) -> bool where for<'r> &'r N: Sub<&'r N, Output=N>, for<'r> &'r N: Neg @@ -178,14 +228,14 @@ where // Distribute top candidate's surplus // TODO: Handle ties let elected_candidate = has_surplus.first_mut().unwrap().0; - distribute_surplus(state, elected_candidate); + distribute_surplus(state, &opts, elected_candidate); return true; } return false; } -fn distribute_surplus(state: &mut CountState, elected_candidate: &Candidate) +fn distribute_surplus(state: &mut CountState, opts: &STVOptions, elected_candidate: &Candidate) where for<'r> &'r N: Sub<&'r N, Output=N>, for<'r> &'r N: Neg @@ -225,8 +275,9 @@ where let mut candidate_transfers = entry.num_ballots * &surplus / &result.total_ballots; // Round transfers - // TODO: Make configurable - candidate_transfers.floor_mut(); + if let Some(dps) = opts.round_votes { + candidate_transfers.floor_mut(dps); + } count_card.transfer(&candidate_transfers); checksum += candidate_transfers; } @@ -236,8 +287,9 @@ where state.exhausted.parcels.push(parcel); let mut exhausted_transfers = result.exhausted.num_ballots * &surplus / &result.total_ballots; - // TODO: Make configurable - exhausted_transfers.floor_mut(); + if let Some(dps) = opts.round_votes { + exhausted_transfers.floor_mut(dps); + } state.exhausted.transfer(&exhausted_transfers); checksum += exhausted_transfers; @@ -251,7 +303,7 @@ where state.loss_fraction.transfer(&-checksum); } -pub fn bulk_elect(state: &mut CountState) -> bool { +fn bulk_elect(state: &mut CountState) -> bool { if state.election.candidates.len() - state.num_excluded <= state.election.seats { state.kind = None; state.title = "Bulk election".to_string(); @@ -281,7 +333,7 @@ pub fn bulk_elect(state: &mut CountState) -> bool { return false; } -pub fn exclude_hopefuls(state: &mut CountState) -> bool { +fn exclude_hopefuls(state: &mut CountState, opts: &STVOptions) -> bool { let mut hopefuls: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) .collect(); @@ -301,12 +353,12 @@ pub fn exclude_hopefuls(state: &mut CountState) -> bool { vec![&excluded_candidate.name] ); - exclude_candidate(state, excluded_candidate); + exclude_candidate(state, opts, excluded_candidate); return true; } -pub fn continue_exclusion(state: &mut CountState) -> bool { +fn continue_exclusion(state: &mut CountState, opts: &STVOptions) -> bool { let mut excluded_with_votes: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() .filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && !cc.votes.is_zero()) .collect(); @@ -323,14 +375,14 @@ pub fn continue_exclusion(state: &mut CountState) -> bool { vec![&excluded_candidate.name] ); - exclude_candidate(state, excluded_candidate); + exclude_candidate(state, opts, excluded_candidate); return true; } return false; } -fn exclude_candidate(state: &mut CountState, excluded_candidate: &Candidate) { +fn exclude_candidate(state: &mut CountState, opts: &STVOptions, excluded_candidate: &Candidate) { let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); count_card.state = CandidateState::EXCLUDED; state.num_excluded += 1; @@ -352,9 +404,10 @@ fn exclude_candidate(state: &mut CountState, excluded_candidate: & count_card.parcels.push(parcel); // Round transfers - // TODO: Make configurable let mut candidate_transfers = entry.num_votes; - candidate_transfers.floor_mut(); + if let Some(dps) = opts.round_votes { + candidate_transfers.floor_mut(dps); + } count_card.transfer(&candidate_transfers); checksum += candidate_transfers; } @@ -364,8 +417,9 @@ fn exclude_candidate(state: &mut CountState, excluded_candidate: & state.exhausted.parcels.push(parcel); let mut exhausted_transfers = result.exhausted.num_votes; - // TODO: Make configurable - exhausted_transfers.floor_mut(); + if let Some(dps) = opts.round_votes { + exhausted_transfers.floor_mut(dps); + } state.exhausted.transfer(&exhausted_transfers); checksum += exhausted_transfers; @@ -379,7 +433,7 @@ fn exclude_candidate(state: &mut CountState, excluded_candidate: & state.loss_fraction.transfer(&-checksum); } -pub fn finished_before_stage(state: &CountState) -> bool { +fn finished_before_stage(state: &CountState) -> bool { if state.num_elected >= state.election.seats { return true; }