Implement --transferable-only

This commit is contained in:
RunasSudo 2021-05-31 23:17:21 +10:00
parent c114d3a4ee
commit 76d69913c7
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 88 additions and 19 deletions

View File

@ -45,41 +45,52 @@ enum Command {
#[derive(Clap)]
#[clap(setting=AppSettings::DeriveDisplayOrder)]
struct STV {
// ----------------
// -- File input --
/// Path to the BLT file to be counted
filename: String,
// ----------------------
// -- Numbers settings --
/// Numbers mode
#[clap(help_heading=Some("NUMBERS"), short, long, possible_values=&["rational", "float64"], default_value="rational", value_name="mode")]
numbers: String,
// -----------------------
// -- Rounding settings --
/// Round votes to specified decimal places
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
round_votes: Option<usize>,
// ------------------
// -- STV variants --
/// Method of surplus transfers
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
surplus: String,
/// Examine only transferable papers during surplus distributions
#[clap(help_heading=Some("STV VARIANTS"), long)]
transferable_only: bool,
/// Method of exclusions
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "parcels_by_order"], default_value="single_stage", value_name="method")]
exclusion: String,
// ----------------------
// -- Display settings --
/// Hide excluded candidates from results report
#[clap(help_heading=Some("DISPLAY"), long)]
hide_excluded: bool,
/// Sort candidates by votes in results report
#[clap(help_heading=Some("DISPLAY"), long)]
sort_votes: bool,
/// Print votes to specified decimal places in results report
#[clap(help_heading=Some("DISPLAY"), long, default_value="2", value_name="dps")]
pp_decimals: usize,
@ -120,6 +131,7 @@ where
"meek" => stv::SurplusMethod::Meek,
_ => panic!("Invalid --surplus"),
},
transferable_only: cmd_opts.transferable_only,
exclusion: match cmd_opts.exclusion.as_str() {
"single_stage" => stv::ExclusionMethod::SingleStage,
"by_value" => stv::ExclusionMethod::ByValue,

View File

@ -32,6 +32,7 @@ use std::ops;
pub struct STVOptions {
pub round_votes: Option<usize>,
pub surplus: SurplusMethod,
pub transferable_only: bool,
pub exclusion: ExclusionMethod,
pub pp_decimals: usize,
}
@ -265,6 +266,45 @@ where
return false;
}
/// Return the denominator of the transfer value
fn calculate_transfer_denom<N: Number>(surplus: &N, result: &NextPreferencesResult<N>, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option<N>
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>
{
if transferable_only {
let total_units = if weighted { &result.total_votes } else { &result.total_ballots };
let exhausted_units = if weighted { &result.exhausted.num_votes } else { &result.exhausted.num_ballots };
let transferable_units = total_units - exhausted_units;
if transferable_votes > surplus {
return Some(transferable_units);
} else {
return None;
}
} else {
if weighted {
return Some(result.total_votes.clone());
} else {
return Some(result.total_ballots.clone());
}
}
}
fn reweight_vote<N: Number>(num_votes: &N, num_ballots: &N, surplus: &N, weighted: bool, transfer_denom: &Option<N>) -> N {
match transfer_denom {
Some(v) => {
if weighted {
return num_votes.clone() * surplus / v;
} else {
return num_ballots.clone() * surplus / v;
}
}
None => {
return num_votes.clone();
}
}
}
fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate)
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
@ -292,23 +332,29 @@ where
// Count next preferences
let result = next_preferences(state, votes);
// Transfer candidate votes
let transfer_value;
match opts.surplus {
SurplusMethod::WIG => {
// Weighted inclusive Gregory
transfer_value = surplus.clone() / &result.total_votes;
}
SurplusMethod::UIG | SurplusMethod::EG => {
// Unweighted inclusive Gregory
transfer_value = surplus.clone() / &result.total_ballots;
}
SurplusMethod::Meek => { todo!(); }
}
state.kind = Some("Surplus of");
state.title = String::from(&elected_candidate.name);
state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps$}.", elected_candidate.name, transfer_value, dps=opts.pp_decimals));
// Transfer candidate votes
// TODO: Refactor??
let is_weighted = match opts.surplus {
SurplusMethod::WIG => { true }
SurplusMethod::UIG | SurplusMethod::EG => { false }
SurplusMethod::Meek => { todo!() }
};
let transferable_votes = &result.total_votes - &result.exhausted.num_votes;
let transfer_denom = calculate_transfer_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only);
let transfer_value;
match transfer_denom {
Some(ref v) => {
transfer_value = surplus.clone() / v;
state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps$}.", elected_candidate.name, transfer_value, dps=opts.pp_decimals));
}
None => {
state.logger.log_literal(format!("Surplus of {} distributed at values received.", elected_candidate.name));
}
}
let mut checksum = N::new();
@ -317,14 +363,13 @@ where
// Reweight votes
for vote in parcel.iter_mut() {
//vote.value = vote.ballot.orig_value.clone() * &transfer_value;
vote.value = vote.ballot.orig_value.clone() * &surplus / &result.total_ballots;
vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &transfer_denom);
}
let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.parcels.push(parcel);
let mut candidate_transfers = entry.num_ballots * &surplus / &result.total_ballots;
let mut candidate_transfers = reweight_vote(&entry.num_votes, &entry.num_ballots, &surplus, is_weighted, &transfer_denom);
// Round transfers
if let Some(dps) = opts.round_votes {
candidate_transfers.floor_mut(dps);
@ -337,7 +382,18 @@ where
let parcel = result.exhausted.votes as Parcel<N>;
state.exhausted.parcels.push(parcel);
let mut exhausted_transfers = result.exhausted.num_ballots * &surplus / &result.total_ballots;
let mut exhausted_transfers;
if opts.transferable_only {
if transferable_votes > surplus {
// No ballots exhaust
exhausted_transfers = N::new();
} else {
exhausted_transfers = &surplus - &transferable_votes;
}
} else {
exhausted_transfers = reweight_vote(&result.exhausted.num_votes, &result.exhausted.num_ballots, &surplus, is_weighted, &transfer_denom);
}
if let Some(dps) = opts.round_votes {
exhausted_transfers.floor_mut(dps);
}

View File

@ -56,6 +56,7 @@ fn aec_tas19_rational() {
let stv_opts = stv::STVOptions {
round_votes: Some(0),
surplus: stv::SurplusMethod::UIG,
transferable_only: false,
exclusion: stv::ExclusionMethod::ByValue,
pp_decimals: 2,
};