Show loss by fraction, and implement --sort-votes and --hide-excluded

This commit is contained in:
RunasSudo 2021-05-29 00:43:58 +10:00
parent e6d57685cb
commit df69ef456f
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 100 additions and 35 deletions

View File

@ -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();
}
}

View File

@ -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);
}
}
}
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(" "));
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<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!("");
}

View File

@ -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 {

View File

@ -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 {