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 candidates: HashMap<&'a Candidate, CountCard<'a, N>>,
|
||||||
pub exhausted: CountCard<'a, N>,
|
pub exhausted: CountCard<'a, N>,
|
||||||
|
pub loss_fraction: CountCard<'a, N>,
|
||||||
|
|
||||||
pub quota: N,
|
pub quota: N,
|
||||||
|
|
||||||
|
@ -104,6 +105,7 @@ impl<'a, N: Number> CountState<'a, N> {
|
||||||
election: &election,
|
election: &election,
|
||||||
candidates: HashMap::new(),
|
candidates: HashMap::new(),
|
||||||
exhausted: CountCard::new(),
|
exhausted: CountCard::new(),
|
||||||
|
loss_fraction: CountCard::new(),
|
||||||
quota: N::new(),
|
quota: N::new(),
|
||||||
num_elected: 0,
|
num_elected: 0,
|
||||||
num_excluded: 0,
|
num_excluded: 0,
|
||||||
|
@ -121,6 +123,7 @@ impl<'a, N: Number> CountState<'a, N> {
|
||||||
count_card.step();
|
count_card.step();
|
||||||
}
|
}
|
||||||
self.exhausted.step();
|
self.exhausted.step();
|
||||||
|
self.loss_fraction.step();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
53
src/main.rs
53
src/main.rs
|
@ -19,7 +19,7 @@ mod election;
|
||||||
mod numbers;
|
mod numbers;
|
||||||
mod stv;
|
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 crate::numbers::{Number, NumType};
|
||||||
|
|
||||||
use clap::Clap;
|
use clap::Clap;
|
||||||
|
@ -48,33 +48,60 @@ enum Command {
|
||||||
struct STV {
|
struct STV {
|
||||||
/// Path to the BLT file to be counted
|
/// Path to the BLT file to be counted
|
||||||
filename: String,
|
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
|
/// Print votes to specified decimal places in results report
|
||||||
#[clap(long, default_value="2")]
|
#[clap(long, default_value="2")]
|
||||||
pp_decimals: usize,
|
pp_decimals: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_stage<N: Number>(stage_num: usize, result: &StageResult<N>, cmd_opts: &STV) {
|
fn print_candidates<'a, N: 'a + Number, I: Iterator<Item=(&'a Candidate, &'a CountCard<'a, N>)>>(candidates: I, 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() {
|
|
||||||
for (candidate, count_card) in candidates {
|
for (candidate, count_card) in candidates {
|
||||||
if count_card.state == CandidateState::ELECTED {
|
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);
|
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 {
|
} else if count_card.state == CandidateState::EXCLUDED {
|
||||||
|
// 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);
|
println!("- {}: {:.dps$} ({:.dps$}) - Excluded {}", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=cmd_opts.pp_decimals);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
|
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!("");
|
println!("");
|
||||||
}
|
}
|
||||||
|
|
|
@ -190,9 +190,7 @@ impl ops::AddAssign for Rational {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::SubAssign for Rational {
|
impl ops::SubAssign for Rational {
|
||||||
fn sub_assign(&mut self, _rhs: Self) {
|
fn sub_assign(&mut self, rhs: Self) { self.0 -= rhs.0 }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::MulAssign for Rational {
|
impl ops::MulAssign for Rational {
|
||||||
|
@ -214,15 +212,11 @@ impl ops::RemAssign for Rational {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::AddAssign<&Rational> for Rational {
|
impl ops::AddAssign<&Rational> for Rational {
|
||||||
fn add_assign(&mut self, rhs: &Rational) {
|
fn add_assign(&mut self, rhs: &Rational) { self.0 += &rhs.0 }
|
||||||
self.0 += &rhs.0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::SubAssign<&Rational> for Rational {
|
impl ops::SubAssign<&Rational> for Rational {
|
||||||
fn sub_assign(&mut self, _rhs: &Rational) {
|
fn sub_assign(&mut self, rhs: &Rational) { self.0 -= &rhs.0 }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::MulAssign<&Rational> for Rational {
|
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 {
|
impl ops::Add<&Rational> for &Rational {
|
||||||
type Output = Rational;
|
type Output = Rational;
|
||||||
fn add(self, _rhs: &Rational) -> Self::Output {
|
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 crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Sub;
|
use std::ops::{Neg, Sub};
|
||||||
|
|
||||||
struct NextPreferencesResult<'a, N> {
|
struct NextPreferencesResult<'a, N> {
|
||||||
candidates: HashMap<&'a Candidate, NextPreferencesEntry<'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()
|
let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||||||
.filter(|(_, cc)| cc.votes > state.quota)
|
.filter(|(_, cc)| cc.votes > state.quota)
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -167,7 +171,11 @@ pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>) -> bool where
|
||||||
return false;
|
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 count_card = state.candidates.get(elected_candidate).unwrap();
|
||||||
let surplus = &count_card.votes - &state.quota;
|
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
|
// Transfer candidate votes
|
||||||
// Unweighted inclusive Gregory
|
// Unweighted inclusive Gregory
|
||||||
// TODO: Other methods
|
// 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() {
|
for (candidate, entry) in result.candidates.into_iter() {
|
||||||
let mut parcel = entry.votes as Parcel<N>;
|
let mut parcel = entry.votes as Parcel<N>;
|
||||||
|
|
||||||
// Reweight votes
|
// Reweight votes
|
||||||
for vote in parcel.iter_mut() {
|
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();
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||||
count_card.parcels.push(parcel);
|
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
|
// Round transfers
|
||||||
// TODO: Make configurable
|
// TODO: Make configurable
|
||||||
total_transferred.floor_mut();
|
candidate_transfers.floor_mut();
|
||||||
count_card.transfer(&total_transferred);
|
count_card.transfer(&candidate_transfers);
|
||||||
|
checksum += candidate_transfers;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transfer exhausted votes
|
// Transfer exhausted votes
|
||||||
let parcel = result.exhausted.votes as Parcel<N>;
|
let parcel = result.exhausted.votes as Parcel<N>;
|
||||||
state.exhausted.parcels.push(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
|
// Finalise candidate votes
|
||||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||||
count_card.transfers = -surplus;
|
count_card.transfers = -&surplus;
|
||||||
count_card.votes.assign(&state.quota);
|
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 {
|
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);
|
let result = next_preferences(state, votes);
|
||||||
|
|
||||||
// Transfer candidate votes
|
// Transfer candidate votes
|
||||||
|
let mut checksum = N::new();
|
||||||
|
|
||||||
for (candidate, entry) in result.candidates.into_iter() {
|
for (candidate, entry) in result.candidates.into_iter() {
|
||||||
let parcel = entry.votes as Parcel<N>;
|
let parcel = entry.votes as Parcel<N>;
|
||||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||||
count_card.parcels.push(parcel);
|
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
|
// Transfer exhausted votes
|
||||||
let parcel = result.exhausted.votes as Parcel<N>;
|
let parcel = result.exhausted.votes as Parcel<N>;
|
||||||
state.exhausted.parcels.push(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
|
// Finalise candidate votes
|
||||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||||
|
checksum -= &count_card.votes;
|
||||||
count_card.transfers = -count_card.votes.clone();
|
count_card.transfers = -count_card.votes.clone();
|
||||||
count_card.votes = N::new();
|
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 {
|
pub fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
|
||||||
|
|
Loading…
Reference in New Issue