Implement --transferable-only
This commit is contained in:
parent
c114d3a4ee
commit
76d69913c7
12
src/main.rs
12
src/main.rs
|
@ -45,41 +45,52 @@ enum Command {
|
||||||
#[derive(Clap)]
|
#[derive(Clap)]
|
||||||
#[clap(setting=AppSettings::DeriveDisplayOrder)]
|
#[clap(setting=AppSettings::DeriveDisplayOrder)]
|
||||||
struct STV {
|
struct STV {
|
||||||
|
// ----------------
|
||||||
// -- File input --
|
// -- File input --
|
||||||
|
|
||||||
/// Path to the BLT file to be counted
|
/// Path to the BLT file to be counted
|
||||||
filename: String,
|
filename: String,
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
// -- Numbers settings --
|
// -- Numbers settings --
|
||||||
|
|
||||||
/// Numbers mode
|
/// 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,
|
numbers: String,
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
// -- Rounding settings --
|
// -- Rounding settings --
|
||||||
|
|
||||||
/// Round votes to specified decimal places
|
/// Round votes to specified decimal places
|
||||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||||
round_votes: Option<usize>,
|
round_votes: Option<usize>,
|
||||||
|
|
||||||
|
// ------------------
|
||||||
// -- STV variants --
|
// -- STV variants --
|
||||||
|
|
||||||
/// Method of surplus transfers
|
/// Method of surplus transfers
|
||||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
|
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
|
||||||
surplus: String,
|
surplus: String,
|
||||||
|
|
||||||
|
/// Examine only transferable papers during surplus distributions
|
||||||
|
#[clap(help_heading=Some("STV VARIANTS"), long)]
|
||||||
|
transferable_only: bool,
|
||||||
|
|
||||||
/// Method of exclusions
|
/// 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")]
|
#[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,
|
exclusion: String,
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
// -- Display settings --
|
// -- Display settings --
|
||||||
|
|
||||||
/// Hide excluded candidates from results report
|
/// Hide excluded candidates from results report
|
||||||
#[clap(help_heading=Some("DISPLAY"), long)]
|
#[clap(help_heading=Some("DISPLAY"), long)]
|
||||||
hide_excluded: bool,
|
hide_excluded: bool,
|
||||||
|
|
||||||
/// Sort candidates by votes in results report
|
/// Sort candidates by votes in results report
|
||||||
#[clap(help_heading=Some("DISPLAY"), long)]
|
#[clap(help_heading=Some("DISPLAY"), long)]
|
||||||
sort_votes: bool,
|
sort_votes: bool,
|
||||||
|
|
||||||
/// Print votes to specified decimal places in results report
|
/// Print votes to specified decimal places in results report
|
||||||
#[clap(help_heading=Some("DISPLAY"), long, default_value="2", value_name="dps")]
|
#[clap(help_heading=Some("DISPLAY"), long, default_value="2", value_name="dps")]
|
||||||
pp_decimals: usize,
|
pp_decimals: usize,
|
||||||
|
@ -120,6 +131,7 @@ where
|
||||||
"meek" => stv::SurplusMethod::Meek,
|
"meek" => stv::SurplusMethod::Meek,
|
||||||
_ => panic!("Invalid --surplus"),
|
_ => panic!("Invalid --surplus"),
|
||||||
},
|
},
|
||||||
|
transferable_only: cmd_opts.transferable_only,
|
||||||
exclusion: match cmd_opts.exclusion.as_str() {
|
exclusion: match cmd_opts.exclusion.as_str() {
|
||||||
"single_stage" => stv::ExclusionMethod::SingleStage,
|
"single_stage" => stv::ExclusionMethod::SingleStage,
|
||||||
"by_value" => stv::ExclusionMethod::ByValue,
|
"by_value" => stv::ExclusionMethod::ByValue,
|
||||||
|
|
|
@ -32,6 +32,7 @@ use std::ops;
|
||||||
pub struct STVOptions {
|
pub struct STVOptions {
|
||||||
pub round_votes: Option<usize>,
|
pub round_votes: Option<usize>,
|
||||||
pub surplus: SurplusMethod,
|
pub surplus: SurplusMethod,
|
||||||
|
pub transferable_only: bool,
|
||||||
pub exclusion: ExclusionMethod,
|
pub exclusion: ExclusionMethod,
|
||||||
pub pp_decimals: usize,
|
pub pp_decimals: usize,
|
||||||
}
|
}
|
||||||
|
@ -265,6 +266,45 @@ where
|
||||||
return false;
|
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)
|
fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate)
|
||||||
where
|
where
|
||||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
@ -292,23 +332,29 @@ where
|
||||||
// Count next preferences
|
// Count next preferences
|
||||||
let result = next_preferences(state, votes);
|
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.kind = Some("Surplus of");
|
||||||
state.title = String::from(&elected_candidate.name);
|
state.title = String::from(&elected_candidate.name);
|
||||||
|
|
||||||
|
// 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));
|
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();
|
let mut checksum = N::new();
|
||||||
|
|
||||||
|
@ -317,14 +363,13 @@ where
|
||||||
|
|
||||||
// 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 = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &transfer_denom);
|
||||||
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 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
|
// Round transfers
|
||||||
if let Some(dps) = opts.round_votes {
|
if let Some(dps) = opts.round_votes {
|
||||||
candidate_transfers.floor_mut(dps);
|
candidate_transfers.floor_mut(dps);
|
||||||
|
@ -337,7 +382,18 @@ where
|
||||||
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);
|
||||||
|
|
||||||
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 {
|
if let Some(dps) = opts.round_votes {
|
||||||
exhausted_transfers.floor_mut(dps);
|
exhausted_transfers.floor_mut(dps);
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ fn aec_tas19_rational() {
|
||||||
let stv_opts = stv::STVOptions {
|
let stv_opts = stv::STVOptions {
|
||||||
round_votes: Some(0),
|
round_votes: Some(0),
|
||||||
surplus: stv::SurplusMethod::UIG,
|
surplus: stv::SurplusMethod::UIG,
|
||||||
|
transferable_only: false,
|
||||||
exclusion: stv::ExclusionMethod::ByValue,
|
exclusion: stv::ExclusionMethod::ByValue,
|
||||||
pp_decimals: 2,
|
pp_decimals: 2,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue