From df69ef456fb8e6447d7731db6d3d06f05e142449 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 29 May 2021 00:43:58 +1000 Subject: [PATCH] Show loss by fraction, and implement --sort-votes and --hide-excluded --- src/election.rs | 3 +++ src/main.rs | 55 +++++++++++++++++++++++++++++++++------------ src/numbers.rs | 17 +++++++------- src/stv/mod.rs | 60 +++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 100 insertions(+), 35 deletions(-) diff --git a/src/election.rs b/src/election.rs index 160139d..59c1d08 100644 --- a/src/election.rs +++ b/src/election.rs @@ -91,6 +91,7 @@ pub struct CountState<'a, N> { pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>, pub exhausted: CountCard<'a, N>, + pub loss_fraction: CountCard<'a, N>, pub quota: N, @@ -104,6 +105,7 @@ impl<'a, N: Number> CountState<'a, N> { election: &election, candidates: HashMap::new(), exhausted: CountCard::new(), + loss_fraction: CountCard::new(), quota: N::new(), num_elected: 0, num_excluded: 0, @@ -121,6 +123,7 @@ impl<'a, N: Number> CountState<'a, N> { count_card.step(); } self.exhausted.step(); + self.loss_fraction.step(); } } diff --git a/src/main.rs b/src/main.rs index 0feedc5..ee1668e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ mod election; mod numbers; mod stv; -use crate::election::{CandidateState, CountState, CountStateOrRef, Election, StageResult}; +use crate::election::{Candidate, CandidateState, CountCard, CountState, CountStateOrRef, Election, StageResult}; use crate::numbers::{Number, NumType}; use clap::Clap; @@ -48,33 +48,60 @@ enum Command { struct STV { /// Path to the BLT file to be counted filename: String, + /// Hide excluded candidates from results report + #[clap(long)] + hide_excluded: bool, + /// Sort candidates by votes in results report + #[clap(long)] + sort_votes: bool, /// Print votes to specified decimal places in results report #[clap(long, default_value="2")] pp_decimals: usize, } -fn print_stage(stage_num: usize, result: &StageResult, cmd_opts: &STV) { - println!("{}. {}", stage_num, result.title); - println!("{}", result.logs.join(" ")); - - // Sort candidates - //let mut candidates: Vec<(&&Candidate, &CountCard)> = result.state.candidates.iter().collect(); - //candidates.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); - let candidates = result.state.as_ref().election.candidates.iter().map(|c| (c, result.state.as_ref().candidates.get(c).unwrap())); - - // Print candidates - //for (candidate, count_card) in candidates.into_iter().rev() { +fn print_candidates<'a, N: 'a + Number, I: Iterator)>>(candidates: I, cmd_opts: &STV) { for (candidate, count_card) in candidates { if count_card.state == CandidateState::ELECTED { println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=cmd_opts.pp_decimals); } else if count_card.state == CandidateState::EXCLUDED { - println!("- {}: {:.dps$} ({:.dps$}) - Excluded {}", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=cmd_opts.pp_decimals); + // If --hide-excluded, hide unless nonzero votes or nonzero transfers + if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() { + println!("- {}: {:.dps$} ({:.dps$}) - Excluded {}", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=cmd_opts.pp_decimals); + } } else { println!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals); } } +} + +fn print_stage(stage_num: usize, result: &StageResult, cmd_opts: &STV) { + // Print stage details + println!("{}. {}", stage_num, result.title); + println!("{}", result.logs.join(" ")); - println!("Quota: {:.dps$}", result.state.as_ref().quota, dps=cmd_opts.pp_decimals); + let state = result.state.as_ref(); + + // Print candidates + if cmd_opts.sort_votes { + // Sort by votes if requested + let mut candidates: Vec<(&Candidate, &CountCard)> = state.candidates.iter() + .map(|(c, cc)| (*c, cc)).collect(); + // First sort by order of election (as a tie-breaker, if votes are equal) + candidates.sort_unstable_by(|a, b| b.1.order_elected.partial_cmp(&a.1.order_elected).unwrap()); + // Then sort by votes + candidates.sort_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); + print_candidates(candidates.into_iter().rev(), cmd_opts); + } else { + let candidates = state.election.candidates.iter() + .map(|c| (c, state.candidates.get(c).unwrap())); + print_candidates(candidates, cmd_opts); + } + + // Print summary rows + println!("Exhausted: {:.dps$} ({:.dps$})", state.exhausted.votes, state.exhausted.transfers, dps=cmd_opts.pp_decimals); + println!("Loss by fraction: {:.dps$} ({:.dps$})", state.loss_fraction.votes, state.loss_fraction.transfers, dps=cmd_opts.pp_decimals); + + println!("Quota: {:.dps$}", state.quota, dps=cmd_opts.pp_decimals); println!(""); } diff --git a/src/numbers.rs b/src/numbers.rs index 6f52621..0675c28 100644 --- a/src/numbers.rs +++ b/src/numbers.rs @@ -190,9 +190,7 @@ impl ops::AddAssign for Rational { } impl ops::SubAssign for Rational { - fn sub_assign(&mut self, _rhs: Self) { - todo!() - } + fn sub_assign(&mut self, rhs: Self) { self.0 -= rhs.0 } } impl ops::MulAssign for Rational { @@ -214,15 +212,11 @@ impl ops::RemAssign for Rational { } impl ops::AddAssign<&Rational> for Rational { - fn add_assign(&mut self, rhs: &Rational) { - self.0 += &rhs.0; - } + fn add_assign(&mut self, rhs: &Rational) { self.0 += &rhs.0 } } impl ops::SubAssign<&Rational> for Rational { - fn sub_assign(&mut self, _rhs: &Rational) { - todo!() - } + fn sub_assign(&mut self, rhs: &Rational) { self.0 -= &rhs.0 } } impl ops::MulAssign<&Rational> for Rational { @@ -243,6 +237,11 @@ impl ops::RemAssign<&Rational> for Rational { } } +impl ops::Neg for &Rational { + type Output = Rational; + fn neg(self) -> Self::Output { Rational(rug::Rational::from(-&self.0)) } +} + impl ops::Add<&Rational> for &Rational { type Output = Rational; fn add(self, _rhs: &Rational) -> Self::Output { diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 877215a..cf53a6a 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -21,7 +21,7 @@ use crate::numbers::Number; use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; use std::collections::HashMap; -use std::ops::Sub; +use std::ops::{Neg, Sub}; struct NextPreferencesResult<'a, N> { candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>, @@ -148,7 +148,11 @@ pub fn elect_meeting_quota(state: &mut CountState) { } } -pub fn distribute_surpluses(state: &mut CountState) -> bool where for<'r> &'r N: Sub<&'r N, Output=N> { +pub fn distribute_surpluses(state: &mut CountState) -> bool +where + for<'r> &'r N: Sub<&'r N, Output=N>, + for<'r> &'r N: Neg +{ let mut has_surplus: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() .filter(|(_, cc)| cc.votes > state.quota) .collect(); @@ -167,7 +171,11 @@ pub fn distribute_surpluses(state: &mut CountState) -> bool where return false; } -fn distribute_surplus(state: &mut CountState, elected_candidate: &Candidate) where for<'r> &'r N: Sub<&'r N, Output=N> { +fn distribute_surplus(state: &mut CountState, elected_candidate: &Candidate) +where + for<'r> &'r N: Sub<&'r N, Output=N>, + for<'r> &'r N: Neg +{ let count_card = state.candidates.get(elected_candidate).unwrap(); let surplus = &count_card.votes - &state.quota; @@ -181,35 +189,47 @@ fn distribute_surplus(state: &mut CountState, elected_candidate: & // Transfer candidate votes // Unweighted inclusive Gregory // TODO: Other methods - let transfer_value = surplus.clone() / &result.total_ballots; + //let transfer_value = surplus.clone() / &result.total_ballots; + let mut checksum = N::new(); for (candidate, entry) in result.candidates.into_iter() { let mut parcel = entry.votes as Parcel; // Reweight votes for vote in parcel.iter_mut() { - vote.value = vote.ballot.orig_value.clone() * &transfer_value; + //vote.value = vote.ballot.orig_value.clone() * &transfer_value; + vote.value = vote.ballot.orig_value.clone() * &surplus / &result.total_ballots; } let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); - let mut total_transferred = entry.num_ballots * &surplus / &result.total_ballots; + let mut candidate_transfers = entry.num_ballots * &surplus / &result.total_ballots; // Round transfers // TODO: Make configurable - total_transferred.floor_mut(); - count_card.transfer(&total_transferred); + candidate_transfers.floor_mut(); + count_card.transfer(&candidate_transfers); + checksum += candidate_transfers; } // Transfer exhausted votes let parcel = result.exhausted.votes as Parcel; state.exhausted.parcels.push(parcel); - state.exhausted.transfer(&result.exhausted.num_votes); + + let mut exhausted_transfers = result.exhausted.num_ballots * &surplus / &result.total_ballots; + // TODO: Make configurable + exhausted_transfers.floor_mut(); + state.exhausted.transfer(&exhausted_transfers); + checksum += exhausted_transfers; // Finalise candidate votes let count_card = state.candidates.get_mut(elected_candidate).unwrap(); - count_card.transfers = -surplus; + count_card.transfers = -&surplus; count_card.votes.assign(&state.quota); + checksum -= surplus; + + // Update loss by fraction + state.loss_fraction.transfer(&-checksum); } pub fn bulk_elect(state: &mut CountState) -> bool { @@ -278,23 +298,39 @@ fn exclude_candidate(state: &mut CountState, excluded_candidate: & let result = next_preferences(state, votes); // Transfer candidate votes + let mut checksum = N::new(); + for (candidate, entry) in result.candidates.into_iter() { let parcel = entry.votes as Parcel; let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); - count_card.transfer(&entry.num_votes); + // Round transfers + // TODO: Make configurable + let mut candidate_transfers = entry.num_votes; + candidate_transfers.floor_mut(); + count_card.transfer(&candidate_transfers); + checksum += candidate_transfers; } // Transfer exhausted votes let parcel = result.exhausted.votes as Parcel; state.exhausted.parcels.push(parcel); - state.exhausted.transfer(&result.exhausted.num_votes); + + let mut exhausted_transfers = result.exhausted.num_votes; + // TODO: Make configurable + exhausted_transfers.floor_mut(); + 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); } pub fn finished_before_stage(state: &CountState) -> bool {