diff --git a/src/main.rs b/src/main.rs index 8ecde45..3e7af3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,7 +57,7 @@ struct STV { // -- Numbers settings -- /// Numbers mode - #[clap(help_heading=Some("NUMBERS"), short, long, possible_values(&["rational", "float64"]), default_value="rational", value_name="mode")] + #[clap(help_heading=Some("NUMBERS"), short, long, possible_values=&["rational", "float64"], default_value="rational", value_name="mode")] numbers: String, // -- Rounding settings -- @@ -66,6 +66,12 @@ struct STV { #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] round_votes: Option, + // -- STV variants -- + + /// Method of exclusions + #[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["one_round", "by_value"], default_value="one_round", value_name="mode")] + exclusion: String, + // -- Display settings -- /// Hide excluded candidates from results report @@ -101,11 +107,13 @@ fn main() { fn count_election(election: Election, cmd_opts: STV) where for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Neg { // Copy applicable options let stv_opts = stv::STVOptions { round_votes: cmd_opts.round_votes, + exclusion: &cmd_opts.exclusion, }; // Initialise count state diff --git a/src/numbers/mod.rs b/src/numbers/mod.rs index 1b8952d..8e1281e 100644 --- a/src/numbers/mod.rs +++ b/src/numbers/mod.rs @@ -21,12 +21,12 @@ mod rational; use num_traits::{NumAssignRef, NumRef}; use rug::{self, Assign}; -use std::cmp::{PartialOrd}; +use std::cmp::Ord; use std::fmt; use std::ops; //pub trait Number: NumRef + NumAssignRef + PartialOrd + Assign + Clone + fmt::Display where for<'a> &'a Self: RefNum<&'a Self> { -pub trait Number: NumRef + NumAssignRef + ops::Neg + PartialOrd + Assign + Clone + fmt::Display where for<'a> Self: Assign<&'a Self>{ +pub trait Number: NumRef + NumAssignRef + ops::Neg + Ord + Assign + Clone + fmt::Display where for<'a> Self: Assign<&'a Self>{ fn new() -> Self; fn from(n: usize) -> Self; diff --git a/src/numbers/native.rs b/src/numbers/native.rs index a8a7bea..083f753 100644 --- a/src/numbers/native.rs +++ b/src/numbers/native.rs @@ -20,7 +20,7 @@ use super::Number; use num_traits::{Num, One, Zero}; use rug::Assign; -use std::cmp::{Ordering, PartialEq, PartialOrd}; +use std::cmp::{Ord, Ordering, PartialEq, PartialOrd}; use std::num::ParseIntError; use std::fmt; use std::ops; @@ -76,10 +76,13 @@ impl Zero for NativeFloat64 { fn is_zero(&self) -> bool { self.0.is_zero() } } +impl Eq for NativeFloat64 {} impl PartialEq for NativeFloat64 { - fn eq(&self, _other: &Self) -> bool { - todo!() - } + fn eq(&self, other: &Self) -> bool { self.0 == other.0 } +} + +impl Ord for NativeFloat64 { + fn cmp(&self, other: &Self) -> Ordering { self.0.partial_cmp(&other.0).unwrap() } } impl PartialOrd for NativeFloat64 { @@ -234,9 +237,7 @@ impl ops::Mul<&NativeFloat64> for &NativeFloat64 { impl ops::Div<&NativeFloat64> for &NativeFloat64 { type Output = NativeFloat64; - fn div(self, _rhs: &NativeFloat64) -> Self::Output { - todo!() - } + fn div(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 / &rhs.0) } } impl ops::Rem<&NativeFloat64> for &NativeFloat64 { diff --git a/src/numbers/rational.rs b/src/numbers/rational.rs index f2d2942..db4318e 100644 --- a/src/numbers/rational.rs +++ b/src/numbers/rational.rs @@ -20,7 +20,7 @@ use super::Number; use num_traits::{Num, One, Zero}; use rug::{self, Assign, ops::Pow, rational::ParseRationalError}; -use std::cmp::{Ordering, PartialEq, PartialOrd}; +use std::cmp::{Ord, Ordering, PartialEq, PartialOrd}; use std::fmt; use std::ops; @@ -106,10 +106,13 @@ impl Zero for Rational { fn is_zero(&self) -> bool { self.0 == rug::Rational::new() } } +impl Eq for Rational {} impl PartialEq for Rational { - fn eq(&self, _other: &Self) -> bool { - todo!() - } + fn eq(&self, other: &Self) -> bool { self.0 == other.0 } +} + +impl Ord for Rational { + fn cmp(&self, other: &Self) -> Ordering { self.0.cmp(&other.0) } } impl PartialOrd for Rational { @@ -264,9 +267,7 @@ impl ops::Mul<&Rational> for &Rational { impl ops::Div<&Rational> for &Rational { type Output = Rational; - fn div(self, _rhs: &Rational) -> Self::Output { - todo!() - } + fn div(self, rhs: &Rational) -> Self::Output { Rational(rug::Rational::from(&self.0 / &rhs.0)) } } impl ops::Rem<&Rational> for &Rational { diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 9acfdc6..901aabd 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -21,10 +21,11 @@ use crate::numbers::Number; use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; use std::collections::HashMap; -use std::ops::{Neg, Sub}; +use std::ops; -pub struct STVOptions { +pub struct STVOptions<'a> { pub round_votes: Option, + pub exclusion: &'a str, } pub fn count_init(mut state: &mut CountState<'_, N>, _opts: &STVOptions) { @@ -35,8 +36,9 @@ pub fn count_init(mut state: &mut CountState<'_, N>, _opts: &STVOptio 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 + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Div<&'r N, Output=N>, + for<'r> &'r N: ops::Neg, { state.logger.entries.clear(); state.step_all(); @@ -77,6 +79,7 @@ struct NextPreferencesResult<'a, N> { candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>, exhausted: NextPreferencesEntry<'a, N>, total_ballots: N, + total_votes: N, } struct NextPreferencesEntry<'a, N> { @@ -95,10 +98,12 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec(state: &mut CountState) { fn distribute_surpluses(state: &mut CountState, opts: &STVOptions) -> bool where - for<'r> &'r N: Sub<&'r N, Output=N>, - for<'r> &'r N: Neg + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Neg { let mut has_surplus: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() .filter(|(_, cc)| cc.votes > state.quota) @@ -237,8 +242,8 @@ where 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 + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Neg { let count_card = state.candidates.get(elected_candidate).unwrap(); let surplus = &count_card.votes - &state.quota; @@ -333,7 +338,10 @@ fn bulk_elect(state: &mut CountState) -> bool { return false; } -fn exclude_hopefuls(state: &mut CountState, opts: &STVOptions) -> bool { +fn exclude_hopefuls(state: &mut CountState, opts: &STVOptions) -> bool +where + for<'r> &'r N: ops::Div<&'r N, Output=N>, +{ let mut hopefuls: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) .collect(); @@ -358,9 +366,14 @@ fn exclude_hopefuls(state: &mut CountState, opts: &STVOptions) -> return true; } -fn continue_exclusion(state: &mut CountState, opts: &STVOptions) -> bool { +fn continue_exclusion(state: &mut CountState, opts: &STVOptions) -> bool +where + for<'r> &'r N: ops::Div<&'r N, Output=N>, +{ + // 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)> = state.candidates.iter() - .filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && !cc.votes.is_zero()) + //.filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && !cc.votes.is_zero()) + .filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && cc.parcels.iter().any(|p| p.len() > 0)) .collect(); if excluded_with_votes.len() > 0 { @@ -382,19 +395,67 @@ fn continue_exclusion(state: &mut CountState, opts: &STVOptions) - return false; } -fn exclude_candidate(state: &mut CountState, opts: &STVOptions, excluded_candidate: &Candidate) { +fn exclude_candidate(state: &mut CountState, opts: &STVOptions, excluded_candidate: &Candidate) +where + for<'r> &'r N: ops::Div<&'r N, Output=N>, +{ let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); - count_card.state = CandidateState::EXCLUDED; - state.num_excluded += 1; - count_card.order_elected = -(state.num_excluded as isize); - // Exclude in one round - // TODO: Exclude by parcel - let votes = state.candidates.get(excluded_candidate).unwrap().parcels.concat(); + // Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??! + if count_card.state != CandidateState::EXCLUDED { + count_card.state = CandidateState::EXCLUDED; + state.num_excluded += 1; + count_card.order_elected = -(state.num_excluded as isize); + } + + // Determine votes to transfer in this stage + let mut votes; + let votes_remaining; + + 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); + } + } + + 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 value = &votes[0].value / &votes[0].ballot.orig_value; // 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)); + } + // Transfer candidate votes let mut checksum = N::new(); @@ -423,14 +484,27 @@ fn exclude_candidate(state: &mut CountState, opts: &STVOptions, ex state.exhausted.transfer(&exhausted_transfers); checksum += exhausted_transfers; - // Finalise candidate votes - let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); - checksum -= &count_card.votes; - count_card.transfers = -count_card.votes.clone(); - count_card.votes = N::new(); - - // Update loss by fraction - state.loss_fraction.transfer(&-checksum); + if votes_remaining > 0 { + // Subtract from candidate tally + let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); + checksum -= &result.total_votes; + count_card.transfer(&-result.total_votes); + + // By definition, there is no loss by fraction + } else { + // Finalise candidate votes + let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); + checksum -= &count_card.votes; + count_card.transfers = -count_card.votes.clone(); + count_card.votes = N::new(); + + // Update loss by fraction + state.loss_fraction.transfer(&-checksum); + + if opts.exclusion != "one_round" { + state.logger.log_literal("Exclusion complete.".to_string()); + } + } } fn finished_before_stage(state: &CountState) -> bool {