Implement --exclusion by_value
This commit is contained in:
parent
f4fdf64072
commit
4f661722e4
10
src/main.rs
10
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<usize>,
|
||||
|
||||
// -- 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<N: Number>(election: Election<N>, 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<Output=N>
|
||||
{
|
||||
// Copy applicable options
|
||||
let stv_opts = stv::STVOptions {
|
||||
round_votes: cmd_opts.round_votes,
|
||||
exclusion: &cmd_opts.exclusion,
|
||||
};
|
||||
|
||||
// Initialise count state
|
||||
|
@ -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<Output=Self> + PartialOrd + Assign + Clone + fmt::Display where for<'a> Self: Assign<&'a Self>{
|
||||
pub trait Number: NumRef + NumAssignRef + ops::Neg<Output=Self> + Ord + Assign + Clone + fmt::Display where for<'a> Self: Assign<&'a Self>{
|
||||
fn new() -> Self;
|
||||
fn from(n: usize) -> Self;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
126
src/stv/mod.rs
126
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<usize>,
|
||||
pub exclusion: &'a str,
|
||||
}
|
||||
|
||||
pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, _opts: &STVOptions) {
|
||||
@ -35,8 +36,9 @@ pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, _opts: &STVOptio
|
||||
|
||||
pub fn count_one_stage<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) -> bool
|
||||
where
|
||||
for<'r> &'r N: Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: Neg<Output=N>
|
||||
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<Output=N>,
|
||||
{
|
||||
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<Vote<'a
|
||||
num_votes: N::new(),
|
||||
},
|
||||
total_ballots: N::new(),
|
||||
total_votes: N::new(),
|
||||
};
|
||||
|
||||
for mut vote in votes.into_iter() {
|
||||
result.total_ballots += &vote.ballot.orig_value;
|
||||
result.total_votes += &vote.value;
|
||||
|
||||
let mut next_candidate = None;
|
||||
|
||||
@ -214,8 +219,8 @@ fn elect_meeting_quota<N: Number>(state: &mut CountState<N>) {
|
||||
|
||||
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool
|
||||
where
|
||||
for<'r> &'r N: Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: Neg<Output=N>
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||||
.filter(|(_, cc)| cc.votes > state.quota)
|
||||
@ -237,8 +242,8 @@ where
|
||||
|
||||
fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate)
|
||||
where
|
||||
for<'r> &'r N: Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: Neg<Output=N>
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
let count_card = state.candidates.get(elected_candidate).unwrap();
|
||||
let surplus = &count_card.votes - &state.quota;
|
||||
@ -333,7 +338,10 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn exclude_hopefuls<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
|
||||
fn exclude_hopefuls<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool
|
||||
where
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||||
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL)
|
||||
.collect();
|
||||
@ -358,9 +366,14 @@ fn exclude_hopefuls<N: Number>(state: &mut CountState<N>, opts: &STVOptions) ->
|
||||
return true;
|
||||
}
|
||||
|
||||
fn continue_exclusion<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
|
||||
fn continue_exclusion<N: Number>(state: &mut CountState<N>, 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<N>)> = 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<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -
|
||||
return false;
|
||||
}
|
||||
|
||||
fn exclude_candidate<N: Number>(state: &mut CountState<N>, opts: &STVOptions, excluded_candidate: &Candidate) {
|
||||
fn exclude_candidate<N: Number>(state: &mut CountState<N>, 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<N: Number>(state: &mut CountState<N>, 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<N: Number>(state: &CountState<N>) -> bool {
|
||||
|
Loading…
Reference in New Issue
Block a user