Show loss by fraction, and implement --sort-votes and --hide-excluded
This commit is contained in:
parent
e6d57685cb
commit
df69ef456f
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
55
src/main.rs
55
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<N: Number>(stage_num: usize, result: &StageResult<N>, cmd_opts: &STV) {
|
||||
println!("{}. {}", stage_num, result.title);
|
||||
println!("{}", result.logs.join(" "));
|
||||
|
||||
// Sort candidates
|
||||
//let mut candidates: Vec<(&&Candidate, &CountCard<N>)> = 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<Item=(&'a Candidate, &'a CountCard<'a, N>)>>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Quota: {:.dps$}", result.state.as_ref().quota, dps=cmd_opts.pp_decimals);
|
||||
fn print_stage<N: Number>(stage_num: usize, result: &StageResult<N>, cmd_opts: &STV) {
|
||||
// Print stage details
|
||||
println!("{}. {}", stage_num, result.title);
|
||||
println!("{}", result.logs.join(" "));
|
||||
|
||||
let state = result.state.as_ref();
|
||||
|
||||
// Print candidates
|
||||
if cmd_opts.sort_votes {
|
||||
// Sort by votes if requested
|
||||
let mut candidates: Vec<(&Candidate, &CountCard<N>)> = 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!("");
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<N: Number>(state: &mut CountState<N>) {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>) -> bool where for<'r> &'r N: Sub<&'r N, Output=N> {
|
||||
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>) -> bool
|
||||
where
|
||||
for<'r> &'r N: Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: Neg<Output=N>
|
||||
{
|
||||
let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||||
.filter(|(_, cc)| cc.votes > state.quota)
|
||||
.collect();
|
||||
|
@ -167,7 +171,11 @@ pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>) -> bool where
|
|||
return false;
|
||||
}
|
||||
|
||||
fn distribute_surplus<N: Number>(state: &mut CountState<N>, elected_candidate: &Candidate) where for<'r> &'r N: Sub<&'r N, Output=N> {
|
||||
fn distribute_surplus<N: Number>(state: &mut CountState<N>, elected_candidate: &Candidate)
|
||||
where
|
||||
for<'r> &'r N: Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: Neg<Output=N>
|
||||
{
|
||||
let count_card = state.candidates.get(elected_candidate).unwrap();
|
||||
let surplus = &count_card.votes - &state.quota;
|
||||
|
||||
|
@ -181,35 +189,47 @@ fn distribute_surplus<N: Number>(state: &mut CountState<N>, 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<N>;
|
||||
|
||||
// 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<N>;
|
||||
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<N: Number>(state: &mut CountState<N>) -> bool {
|
||||
|
@ -278,23 +298,39 @@ fn exclude_candidate<N: Number>(state: &mut CountState<N>, 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<N>;
|
||||
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<N>;
|
||||
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<N: Number>(state: &CountState<N>) -> bool {
|
||||
|
|
Loading…
Reference in New Issue