Refactor implementation of --sum-surplus-transfers -> --round-subtransfers in preparation for NSW Local Gov't STV
This commit is contained in:
parent
d94549dc42
commit
c5d6b8d460
@ -272,12 +272,13 @@ When *Surplus method* is set to *Meek method*:
|
|||||||
* --round-votes controls the rounding of the final number of votes credited to each candidate
|
* --round-votes controls the rounding of the final number of votes credited to each candidate
|
||||||
* Keep values, intermediate products and candidate votes are rounded *up*
|
* Keep values, intermediate products and candidate votes are rounded *up*
|
||||||
|
|
||||||
### (Gregory) Sum surplus transfers (--sum-surplus-transfers)
|
### (Gregory) Round subtransfers (--round-subtransfers)
|
||||||
|
|
||||||
When *Surplus method* is set to a Gregory method, this option allows you to specify how the numbers of votes credited to candidates in a surplus transfer is calculated. In each case, votes are grouped according to the next available preference for a continuing candidate. Subsequently:
|
When *Surplus method* is set to a Gregory method, this option allows you to specify how the numbers of votes credited to candidates in a surplus transfer/exclusion is calculated. In each case, votes are grouped according to the next available preference for a continuing candidate. Subsequently:
|
||||||
|
|
||||||
* *By value*: The votes expressing a next available preference for that candidate are further divided according to value. For each group of votes at a particular value, the total value of all such votes is multiplied by the surplus fraction. The product is credited to that candidate.
|
* *Single step* (default): The total value of all votes expressing a next available preference for that candidate is multiplied by the surplus fraction. The product (rounded if requested) is credited to that candidate.
|
||||||
* *Per ballot*: For each individual vote expressing a next available preference for that candidate, the value of the vote is multiplied by the surplus fraction. The product is credited to that candidate.
|
* *By value*: The votes expressing a next available preference for that candidate are further divided according to value. For each group of votes at a particular value, the total value of all such votes is multiplied by the surplus fraction. The product (rounded if requested) is credited to that candidate.
|
||||||
|
* *Per ballot*: For each individual vote expressing a next available preference for that candidate, the value of the vote is multiplied by the surplus fraction. The product (rounded if requested) is credited to that candidate.
|
||||||
|
|
||||||
This option affects the result only as far as rounding (due to use of fixed-precision/floating-point arithmetic, or an explicit rounding option) is concerned.
|
This option affects the result only as far as rounding (due to use of fixed-precision/floating-point arithmetic, or an explicit rounding option) is concerned.
|
||||||
|
|
||||||
|
@ -295,10 +295,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<label class="col-12">
|
<label class="col-12">
|
||||||
<span class="pill-grey" title="This option has effect only if “Method” is a Gregory method">Gregory</span>
|
<span class="pill-grey" title="This option has effect only if “Method” is a Gregory method">Gregory</span>
|
||||||
Sum surplus transfers:
|
Round subtransfers:
|
||||||
<select id="selSumTransfers">
|
<select id="selSumTransfers">
|
||||||
<!--<option value="single_step" selected>Single step</option>-->
|
<option value="single_step" selected>Single step</option>
|
||||||
<option value="by_value" selected>By value</option>
|
<option value="by_value">By value</option>
|
||||||
<option value="per_ballot">Per ballot</option>
|
<option value="per_ballot">Per ballot</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
@ -32,7 +32,7 @@ function changePreset() {
|
|||||||
document.getElementById('chkRoundVotes').checked = false;
|
document.getElementById('chkRoundVotes').checked = false;
|
||||||
document.getElementById('chkRoundSFs').checked = false;
|
document.getElementById('chkRoundSFs').checked = false;
|
||||||
document.getElementById('chkRoundValues').checked = false;
|
document.getElementById('chkRoundValues').checked = false;
|
||||||
document.getElementById('selSumTransfers').value = 'by_value';
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
document.getElementById('selSurplus').value = 'by_size';
|
document.getElementById('selSurplus').value = 'by_size';
|
||||||
document.getElementById('selMethod').value = 'wig';
|
document.getElementById('selMethod').value = 'wig';
|
||||||
document.getElementById('selPapers').value = 'both';
|
document.getElementById('selPapers').value = 'both';
|
||||||
@ -164,11 +164,11 @@ function changePreset() {
|
|||||||
document.getElementById('txtRoundVotes').value = '0';
|
document.getElementById('txtRoundVotes').value = '0';
|
||||||
document.getElementById('chkRoundSFs').checked = false;
|
document.getElementById('chkRoundSFs').checked = false;
|
||||||
document.getElementById('chkRoundValues').checked = false;
|
document.getElementById('chkRoundValues').checked = false;
|
||||||
document.getElementById('selSumTransfers').value = 'by_value';
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
document.getElementById('selSurplus').value = 'by_order';
|
document.getElementById('selSurplus').value = 'by_order';
|
||||||
document.getElementById('selMethod').value = 'uig';
|
document.getElementById('selMethod').value = 'uig';
|
||||||
document.getElementById('selPapers').value = 'both';
|
document.getElementById('selPapers').value = 'both';
|
||||||
document.getElementById('selExclusion').value = 'by_value';
|
document.getElementById('selExclusion').value = 'single_step';
|
||||||
document.getElementById('selTies').value = 'backwards,random';
|
document.getElementById('selTies').value = 'backwards,random';
|
||||||
} else if (document.getElementById('selPreset').value === 'wa') {
|
} else if (document.getElementById('selPreset').value === 'wa') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
@ -212,11 +212,11 @@ function changePreset() {
|
|||||||
document.getElementById('txtRoundVotes').value = '6';
|
document.getElementById('txtRoundVotes').value = '6';
|
||||||
document.getElementById('chkRoundSFs').checked = false;
|
document.getElementById('chkRoundSFs').checked = false;
|
||||||
document.getElementById('chkRoundValues').checked = false;
|
document.getElementById('chkRoundValues').checked = false;
|
||||||
document.getElementById('selSumTransfers').value = 'by_value';
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
document.getElementById('selSurplus').value = 'by_order';
|
document.getElementById('selSurplus').value = 'by_order';
|
||||||
document.getElementById('selMethod').value = 'eg';
|
document.getElementById('selMethod').value = 'eg';
|
||||||
document.getElementById('selPapers').value = 'transferable';
|
document.getElementById('selPapers').value = 'transferable';
|
||||||
document.getElementById('selExclusion').value = 'by_value';
|
document.getElementById('selExclusion').value = 'single_step';
|
||||||
document.getElementById('selTies').value = 'backwards,random';
|
document.getElementById('selTies').value = 'backwards,random';
|
||||||
} else if (document.getElementById('selPreset').value === 'nswlg') {
|
} else if (document.getElementById('selPreset').value === 'nswlg') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
@ -261,7 +261,7 @@ function changePreset() {
|
|||||||
document.getElementById('chkRoundSFs').checked = true;
|
document.getElementById('chkRoundSFs').checked = true;
|
||||||
document.getElementById('txtRoundSFs').value = '4';
|
document.getElementById('txtRoundSFs').value = '4';
|
||||||
document.getElementById('chkRoundValues').checked = false;
|
document.getElementById('chkRoundValues').checked = false;
|
||||||
document.getElementById('selSumTransfers').value = 'by_value';
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
document.getElementById('selSurplus').value = 'by_size';
|
document.getElementById('selSurplus').value = 'by_size';
|
||||||
document.getElementById('selMethod').value = 'wig';
|
document.getElementById('selMethod').value = 'wig';
|
||||||
document.getElementById('selPapers').value = 'both';
|
document.getElementById('selPapers').value = 'both';
|
||||||
@ -283,7 +283,7 @@ function changePreset() {
|
|||||||
document.getElementById('chkNormaliseBallots').checked = true;
|
document.getElementById('chkNormaliseBallots').checked = true;
|
||||||
document.getElementById('chkRoundQuota').checked = true;
|
document.getElementById('chkRoundQuota').checked = true;
|
||||||
document.getElementById('txtRoundQuota').value = '0';
|
document.getElementById('txtRoundQuota').value = '0';
|
||||||
document.getElementById('selSumTransfers').value = 'by_value';
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
document.getElementById('selMethod').value = 'hare';
|
document.getElementById('selMethod').value = 'hare';
|
||||||
document.getElementById('selPapers').value = 'transferable';
|
document.getElementById('selPapers').value = 'transferable';
|
||||||
document.getElementById('selExclusion').value = 'single_stage';
|
document.getElementById('selExclusion').value = 'single_stage';
|
||||||
@ -304,7 +304,7 @@ function changePreset() {
|
|||||||
document.getElementById('chkNormaliseBallots').checked = true;
|
document.getElementById('chkNormaliseBallots').checked = true;
|
||||||
document.getElementById('chkRoundQuota').checked = true;
|
document.getElementById('chkRoundQuota').checked = true;
|
||||||
document.getElementById('txtRoundQuota').value = '0';
|
document.getElementById('txtRoundQuota').value = '0';
|
||||||
document.getElementById('selSumTransfers').value = 'by_value';
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
document.getElementById('selSurplus').value = 'by_order';
|
document.getElementById('selSurplus').value = 'by_order';
|
||||||
document.getElementById('selMethod').value = 'hare';
|
document.getElementById('selMethod').value = 'hare';
|
||||||
document.getElementById('selPapers').value = 'transferable';
|
document.getElementById('selPapers').value = 'transferable';
|
||||||
@ -328,7 +328,7 @@ function changePreset() {
|
|||||||
document.getElementById('chkRoundVotes').checked = false;
|
document.getElementById('chkRoundVotes').checked = false;
|
||||||
document.getElementById('chkRoundSFs').checked = false;
|
document.getElementById('chkRoundSFs').checked = false;
|
||||||
document.getElementById('chkRoundValues').checked = false;
|
document.getElementById('chkRoundValues').checked = false;
|
||||||
document.getElementById('selSumTransfers').value = 'by_value';
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
document.getElementById('selSurplus').value = 'by_size';
|
document.getElementById('selSurplus').value = 'by_size';
|
||||||
document.getElementById('selMethod').value = 'wig';
|
document.getElementById('selMethod').value = 'wig';
|
||||||
document.getElementById('selPapers').value = 'both';
|
document.getElementById('selPapers').value = 'both';
|
||||||
@ -355,7 +355,7 @@ function changePreset() {
|
|||||||
document.getElementById('txtRoundSFs').value = '3';
|
document.getElementById('txtRoundSFs').value = '3';
|
||||||
document.getElementById('chkRoundValues').checked = true;
|
document.getElementById('chkRoundValues').checked = true;
|
||||||
document.getElementById('txtRoundValues').value = '3';
|
document.getElementById('txtRoundValues').value = '3';
|
||||||
document.getElementById('selSumTransfers').value = 'by_value';
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
document.getElementById('selSurplus').value = 'by_order';
|
document.getElementById('selSurplus').value = 'by_order';
|
||||||
document.getElementById('selMethod').value = 'eg';
|
document.getElementById('selMethod').value = 'eg';
|
||||||
document.getElementById('selPapers').value = 'transferable';
|
document.getElementById('selPapers').value = 'transferable';
|
||||||
@ -382,11 +382,11 @@ function changePreset() {
|
|||||||
document.getElementById('txtRoundSFs').value = '2';
|
document.getElementById('txtRoundSFs').value = '2';
|
||||||
document.getElementById('chkRoundValues').checked = true;
|
document.getElementById('chkRoundValues').checked = true;
|
||||||
document.getElementById('txtRoundValues').value = '2';
|
document.getElementById('txtRoundValues').value = '2';
|
||||||
document.getElementById('selSumTransfers').value = 'by_value';
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
document.getElementById('selSurplus').value = 'by_size';
|
document.getElementById('selSurplus').value = 'by_size';
|
||||||
document.getElementById('selMethod').value = 'eg';
|
document.getElementById('selMethod').value = 'eg';
|
||||||
document.getElementById('selPapers').value = 'transferable';
|
document.getElementById('selPapers').value = 'transferable';
|
||||||
document.getElementById('selExclusion').value = 'by_value';
|
document.getElementById('selExclusion').value = 'single_step';
|
||||||
document.getElementById('selTies').value = 'forwards,random';
|
document.getElementById('selTies').value = 'forwards,random';
|
||||||
} else if (document.getElementById('selPreset').value === 'ers76') {
|
} else if (document.getElementById('selPreset').value === 'ers76') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
@ -409,11 +409,11 @@ function changePreset() {
|
|||||||
document.getElementById('txtRoundSFs').value = '2';
|
document.getElementById('txtRoundSFs').value = '2';
|
||||||
document.getElementById('chkRoundValues').checked = true;
|
document.getElementById('chkRoundValues').checked = true;
|
||||||
document.getElementById('txtRoundValues').value = '2';
|
document.getElementById('txtRoundValues').value = '2';
|
||||||
document.getElementById('selSumTransfers').value = 'by_value';
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
document.getElementById('selSurplus').value = 'by_size';
|
document.getElementById('selSurplus').value = 'by_size';
|
||||||
document.getElementById('selMethod').value = 'eg';
|
document.getElementById('selMethod').value = 'eg';
|
||||||
document.getElementById('selPapers').value = 'transferable';
|
document.getElementById('selPapers').value = 'transferable';
|
||||||
document.getElementById('selExclusion').value = 'by_value';
|
document.getElementById('selExclusion').value = 'single_step';
|
||||||
document.getElementById('selTies').value = 'forwards,random';
|
document.getElementById('selTies').value = 'forwards,random';
|
||||||
} else if (document.getElementById('selPreset').value === 'ers73') {
|
} else if (document.getElementById('selPreset').value === 'ers73') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
@ -436,11 +436,11 @@ function changePreset() {
|
|||||||
document.getElementById('txtRoundSFs').value = '2';
|
document.getElementById('txtRoundSFs').value = '2';
|
||||||
document.getElementById('chkRoundValues').checked = true;
|
document.getElementById('chkRoundValues').checked = true;
|
||||||
document.getElementById('txtRoundValues').value = '2';
|
document.getElementById('txtRoundValues').value = '2';
|
||||||
document.getElementById('selSumTransfers').value = 'by_value';
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
document.getElementById('selSurplus').value = 'by_size';
|
document.getElementById('selSurplus').value = 'by_size';
|
||||||
document.getElementById('selMethod').value = 'eg';
|
document.getElementById('selMethod').value = 'eg';
|
||||||
document.getElementById('selPapers').value = 'transferable';
|
document.getElementById('selPapers').value = 'transferable';
|
||||||
document.getElementById('selExclusion').value = 'by_value';
|
document.getElementById('selExclusion').value = 'single_step';
|
||||||
document.getElementById('selTies').value = 'forwards,random';
|
document.getElementById('selTies').value = 'forwards,random';
|
||||||
} else if (document.getElementById('selPreset').value === 'cofe') {
|
} else if (document.getElementById('selPreset').value === 'cofe') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
@ -467,7 +467,7 @@ function changePreset() {
|
|||||||
document.getElementById('selSurplus').value = 'by_size';
|
document.getElementById('selSurplus').value = 'by_size';
|
||||||
document.getElementById('selMethod').value = 'eg';
|
document.getElementById('selMethod').value = 'eg';
|
||||||
document.getElementById('selPapers').value = 'transferable';
|
document.getElementById('selPapers').value = 'transferable';
|
||||||
document.getElementById('selExclusion').value = 'by_value';
|
document.getElementById('selExclusion').value = 'single_step';
|
||||||
document.getElementById('selTies').value = 'forwards,random';
|
document.getElementById('selTies').value = 'forwards,random';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/* OpenTally: Open-source election vote counting
|
/* OpenTally: Open-source election vote counting
|
||||||
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -79,9 +79,9 @@ pub struct SubcmdOptions {
|
|||||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||||
round_quota: Option<usize>,
|
round_quota: Option<usize>,
|
||||||
|
|
||||||
/// (Gregory STV) How to calculate votes to credit to candidates in surplus transfers
|
/// (Gregory STV) How to round subtransfers during surpluses/exclusions
|
||||||
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["by_value", "per_ballot"], default_value="by_value", value_name="mode")]
|
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["single_step", "per_ballot"], default_value="single_step", value_name="mode")]
|
||||||
sum_surplus_transfers: String,
|
round_subtransfers: String,
|
||||||
|
|
||||||
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
||||||
#[clap(help_heading=Some("ROUNDING"), long, default_value="0.001%", value_name="tolerance")]
|
#[clap(help_heading=Some("ROUNDING"), long, default_value="0.001%", value_name="tolerance")]
|
||||||
@ -284,7 +284,7 @@ where
|
|||||||
cmd_opts.round_values,
|
cmd_opts.round_values,
|
||||||
cmd_opts.round_votes,
|
cmd_opts.round_votes,
|
||||||
cmd_opts.round_quota,
|
cmd_opts.round_quota,
|
||||||
cmd_opts.sum_surplus_transfers.into(),
|
cmd_opts.round_subtransfers.into(),
|
||||||
cmd_opts.meek_surplus_tolerance,
|
cmd_opts.meek_surplus_tolerance,
|
||||||
cmd_opts.normalise_ballots,
|
cmd_opts.normalise_ballots,
|
||||||
cmd_opts.quota.into(),
|
cmd_opts.quota.into(),
|
||||||
|
@ -22,7 +22,7 @@ use super::prettytable_html::{Cell, Row, Table};
|
|||||||
|
|
||||||
use crate::election::{Candidate, CountState};
|
use crate::election::{Candidate, CountState};
|
||||||
use crate::numbers::Number;
|
use crate::numbers::Number;
|
||||||
use crate::stv::{STVOptions, SumSurplusTransfersMode};
|
use crate::stv::{STVOptions, RoundSubtransfersMode};
|
||||||
|
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -128,36 +128,8 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
let is_weighted = self.surplus.is_none() || opts.surplus.is_weighted();
|
let is_weighted = self.surplus.is_none() || opts.surplus.is_weighted();
|
||||||
|
|
||||||
// Iterate through columns
|
// Iterate through columns
|
||||||
|
// Sum votes_in, etc.
|
||||||
for column in self.columns.iter_mut() {
|
for column in self.columns.iter_mut() {
|
||||||
let mut new_value_fraction;
|
|
||||||
if self.surplus.is_some() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
|
|
||||||
if is_weighted {
|
|
||||||
new_value_fraction = column.value_fraction.clone();
|
|
||||||
// If surplus, multiply by surplus fraction
|
|
||||||
if let Some(n) = &self.surpfrac_numer {
|
|
||||||
new_value_fraction *= n;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if let Some(n) = &self.surpfrac_numer {
|
|
||||||
new_value_fraction = n.clone();
|
|
||||||
} else {
|
|
||||||
// Transferred at original value
|
|
||||||
new_value_fraction = column.value_fraction.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(n) = &self.surpfrac_denom {
|
|
||||||
new_value_fraction /= n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Round if required
|
|
||||||
if let Some(dps) = opts.round_values {
|
|
||||||
new_value_fraction.floor_mut(dps);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
new_value_fraction = column.value_fraction.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Candidate votes
|
// Candidate votes
|
||||||
for (candidate, cell) in column.cells.iter_mut() {
|
for (candidate, cell) in column.cells.iter_mut() {
|
||||||
column.total.ballots += &cell.ballots;
|
column.total.ballots += &cell.ballots;
|
||||||
@ -169,19 +141,6 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
column.total.votes_in += &votes_in;
|
column.total.votes_in += &votes_in;
|
||||||
self.total.cells.get_mut(*candidate).unwrap().votes_in += &votes_in;
|
self.total.cells.get_mut(*candidate).unwrap().votes_in += &votes_in;
|
||||||
self.total.total.votes_in += votes_in;
|
self.total.total.votes_in += votes_in;
|
||||||
|
|
||||||
if self.surplus.is_some() || opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
|
|
||||||
let mut votes_out = cell.ballots.clone() * &new_value_fraction;
|
|
||||||
// Round if required
|
|
||||||
if let Some(dps) = opts.round_votes {
|
|
||||||
votes_out.floor_mut(dps);
|
|
||||||
}
|
|
||||||
|
|
||||||
cell.votes_out += &votes_out;
|
|
||||||
column.total.votes_out += &votes_out;
|
|
||||||
self.total.cells.get_mut(*candidate).unwrap().votes_out += &votes_out;
|
|
||||||
self.total.total.votes_out += votes_out;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exhausted votes
|
// Exhausted votes
|
||||||
@ -194,73 +153,167 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
column.total.votes_in += &votes_in;
|
column.total.votes_in += &votes_in;
|
||||||
self.total.exhausted.votes_in += &votes_in;
|
self.total.exhausted.votes_in += &votes_in;
|
||||||
self.total.total.votes_in += votes_in;
|
self.total.total.votes_in += votes_in;
|
||||||
|
}
|
||||||
|
|
||||||
if self.surplus.is_some() || opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
|
match opts.round_subtransfers {
|
||||||
if !opts.transferable_only {
|
RoundSubtransfersMode::SingleStep => {
|
||||||
let mut votes_out = column.exhausted.ballots.clone() * &new_value_fraction;
|
// No need to calculate votes_out for each column
|
||||||
// Round if required
|
|
||||||
if let Some(dps) = opts.round_votes {
|
// Calculate total votes_out per candidate
|
||||||
votes_out.floor_mut(dps);
|
for (_candidate, cell) in self.total.cells.iter_mut() {
|
||||||
|
if is_weighted {
|
||||||
|
// Weighted rules
|
||||||
|
// Multiply votes in by surplus fraction
|
||||||
|
cell.votes_out = multiply_surpfrac(cell.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
|
} else if self.surpfrac.is_none() {
|
||||||
|
// Unweighted rules but transfer at values received
|
||||||
|
cell.votes_out = cell.votes_in.clone();
|
||||||
|
} else {
|
||||||
|
// Unweighted rules
|
||||||
|
// Multiply ballots in by surplus fraction
|
||||||
|
cell.votes_out = multiply_surpfrac(cell.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
}
|
}
|
||||||
|
|
||||||
column.exhausted.votes_out += &votes_out;
|
// Round if required
|
||||||
column.total.votes_out += &votes_out;
|
if let Some(dps) = opts.round_votes {
|
||||||
self.total.exhausted.votes_out += &votes_out;
|
cell.votes_out.floor_mut(dps);
|
||||||
self.total.total.votes_out += votes_out;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Need to calculate total candidate votes_out?
|
// Calculate total exhausted votes
|
||||||
if opts.sum_surplus_transfers == SumSurplusTransfersMode::ByValue {
|
if is_weighted {
|
||||||
for (_candidate, cell) in self.total.cells.iter_mut() {
|
// Weighted rules
|
||||||
let mut votes_out;
|
// Multiply votes in by surplus fraction
|
||||||
|
self.total.exhausted.votes_out = multiply_surpfrac(self.total.exhausted.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
if is_weighted || self.surpfrac.is_none() {
|
} else if self.surpfrac.is_none() {
|
||||||
// NB: If surplus.is_none, then votes transferred at values received
|
// Unweighted rules but transfer at values received
|
||||||
votes_out = cell.votes_in.clone();
|
// This can only happen with --transferable-only, so this will be calculated in apply_to
|
||||||
} else {
|
} else {
|
||||||
votes_out = cell.ballots.clone();
|
// Unweighted rules
|
||||||
|
// Multiply ballots in by surplus fraction
|
||||||
|
self.total.exhausted.votes_out = multiply_surpfrac(self.total.exhausted.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If surplus, multiply by surplus fraction
|
// Round if required
|
||||||
if let Some(n) = &self.surpfrac_numer {
|
if let Some(dps) = opts.round_votes {
|
||||||
votes_out *= n;
|
self.total.exhausted.votes_out.floor_mut(dps);
|
||||||
}
|
}
|
||||||
if let Some(n) = &self.surpfrac_denom {
|
|
||||||
votes_out /= n;
|
|
||||||
}
|
|
||||||
|
|
||||||
cell.votes_out = votes_out; // Rounded later
|
|
||||||
}
|
}
|
||||||
|
RoundSubtransfersMode::ByValue => {
|
||||||
|
// Calculate votes_out for each column
|
||||||
|
for column in self.columns.iter_mut() {
|
||||||
|
// Calculate votes_out per candidate in the column
|
||||||
|
for (_candidate, cell) in column.cells.iter_mut() {
|
||||||
|
if is_weighted {
|
||||||
|
// Weighted rules
|
||||||
|
// Multiply votes in by surplus fraction
|
||||||
|
cell.votes_out = multiply_surpfrac(cell.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
|
} else if self.surpfrac.is_none() {
|
||||||
|
// Unweighted rules but transfer at values received
|
||||||
|
cell.votes_out = cell.votes_in.clone();
|
||||||
|
} else {
|
||||||
|
// Unweighted rules
|
||||||
|
// Multiply ballots in by surplus fraction
|
||||||
|
cell.votes_out = multiply_surpfrac(cell.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
|
}
|
||||||
|
|
||||||
if self.surplus.is_none() || !opts.transferable_only {
|
// Round if required
|
||||||
let mut votes_out;
|
if let Some(dps) = opts.round_votes {
|
||||||
if is_weighted || self.surpfrac.is_none() {
|
cell.votes_out.floor_mut(dps);
|
||||||
votes_out = self.total.exhausted.votes_in.clone();
|
}
|
||||||
} else {
|
}
|
||||||
votes_out = self.total.exhausted.ballots.clone();
|
|
||||||
|
// Calculate exhausted votes in the column
|
||||||
|
if is_weighted {
|
||||||
|
// Weighted rules
|
||||||
|
// Multiply votes in by surplus fraction
|
||||||
|
column.exhausted.votes_out = multiply_surpfrac(column.exhausted.votes_in.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
|
} else if self.surpfrac.is_none() {
|
||||||
|
// Unweighted rules but transfer at values received
|
||||||
|
// This can only happen with --transferable-only, so this will be calculated in apply_to
|
||||||
|
} else {
|
||||||
|
// Unweighted rules
|
||||||
|
// Multiply ballots in by surplus fraction
|
||||||
|
column.exhausted.votes_out = multiply_surpfrac(column.exhausted.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round if required
|
||||||
|
if let Some(dps) = opts.round_votes {
|
||||||
|
column.exhausted.votes_out.floor_mut(dps);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If surplus, multiply by surplus fraction
|
// Sum total votes_out per candidate
|
||||||
if let Some(n) = &self.surpfrac_numer {
|
for (candidate, cell) in self.total.cells.iter_mut() {
|
||||||
votes_out *= n;
|
cell.votes_out = self.columns.iter().fold(N::new(), |acc, col| acc + &col.cells[candidate].votes_out);
|
||||||
}
|
|
||||||
if let Some(n) = &self.surpfrac_denom {
|
|
||||||
votes_out /= n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.total.exhausted.votes_out = votes_out; // Rounded later
|
// Sum total exhausted votes
|
||||||
|
self.total.exhausted.votes_out = self.columns.iter().fold(N::new(), |acc, col| acc + &col.exhausted.votes_out);
|
||||||
}
|
}
|
||||||
}
|
RoundSubtransfersMode::PerBallot => {
|
||||||
|
// Calculate votes_out for each column
|
||||||
|
for column in self.columns.iter_mut() {
|
||||||
|
// Calculate votes_out per candidate in the column
|
||||||
|
for (_candidate, cell) in column.cells.iter_mut() {
|
||||||
|
if is_weighted {
|
||||||
|
// Weighted rules
|
||||||
|
// Multiply ballots in by new value fraction
|
||||||
|
let mut new_value_fraction = multiply_surpfrac(column.value_fraction.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
|
if let Some(dps) = opts.round_values {
|
||||||
|
new_value_fraction.floor_mut(dps);
|
||||||
|
}
|
||||||
|
|
||||||
// Round if required
|
cell.votes_out = cell.ballots.clone() * new_value_fraction;
|
||||||
if let Some(dps) = opts.round_votes {
|
} else if self.surpfrac.is_none() {
|
||||||
for (_candidate, cell) in self.total.cells.iter_mut() {
|
// Unweighted rules but transfer at values received
|
||||||
cell.votes_out.floor_mut(dps);
|
cell.votes_out = cell.votes_in.clone();
|
||||||
|
} else {
|
||||||
|
// Unweighted rules
|
||||||
|
// Multiply ballots in by surplus fraction
|
||||||
|
cell.votes_out = multiply_surpfrac(cell.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round if required
|
||||||
|
if let Some(dps) = opts.round_votes {
|
||||||
|
cell.votes_out.floor_mut(dps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate exhausted votes in the column
|
||||||
|
if is_weighted {
|
||||||
|
// Weighted rules
|
||||||
|
// Multiply ballots in by new value fraction
|
||||||
|
let mut new_value_fraction = multiply_surpfrac(column.value_fraction.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
|
if let Some(dps) = opts.round_values {
|
||||||
|
new_value_fraction.floor_mut(dps);
|
||||||
|
}
|
||||||
|
|
||||||
|
column.exhausted.votes_out = column.exhausted.ballots.clone() * new_value_fraction;
|
||||||
|
} else if self.surpfrac.is_none() {
|
||||||
|
// Unweighted rules but transfer at values received
|
||||||
|
// This can only happen with --transferable-only, so this will be calculated in apply_to
|
||||||
|
} else {
|
||||||
|
// Unweighted rules
|
||||||
|
// Multiply ballots in by surplus fraction
|
||||||
|
column.exhausted.votes_out = multiply_surpfrac(column.exhausted.ballots.clone(), &self.surpfrac_numer, &self.surpfrac_denom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round if required
|
||||||
|
if let Some(dps) = opts.round_votes {
|
||||||
|
column.exhausted.votes_out.floor_mut(dps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum total votes_out per candidate
|
||||||
|
for (candidate, cell) in self.total.cells.iter_mut() {
|
||||||
|
cell.votes_out = self.columns.iter().fold(N::new(), |acc, col| acc + &col.cells[candidate].votes_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum total exhausted votes
|
||||||
|
self.total.exhausted.votes_out = self.columns.iter().fold(N::new(), |acc, col| acc + &col.exhausted.votes_out);
|
||||||
}
|
}
|
||||||
|
_ => todo!()
|
||||||
self.total.exhausted.votes_out.floor_mut(dps);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,10 +363,10 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
set_table_format(&mut table);
|
set_table_format(&mut table);
|
||||||
|
|
||||||
let show_transfers_per_ballot = self.surpfrac.is_some() || opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot;
|
let show_transfers_per_column = opts.round_subtransfers != RoundSubtransfersMode::SingleStep;
|
||||||
|
|
||||||
let num_cols;
|
let num_cols;
|
||||||
if show_transfers_per_ballot {
|
if show_transfers_per_column {
|
||||||
num_cols = self.columns.len() * 3 + 4;
|
num_cols = self.columns.len() * 3 + 4;
|
||||||
} else {
|
} else {
|
||||||
if self.surpfrac.is_none() {
|
if self.surpfrac.is_none() {
|
||||||
@ -331,7 +384,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
for column in self.columns.iter() {
|
for column in self.columns.iter() {
|
||||||
row.push(Cell::new(&format!("Ballots @ {:.dps2$}", column.value_fraction, dps2=max(opts.pp_decimals, 2))).style_spec("cH2"));
|
row.push(Cell::new(&format!("Ballots @ {:.dps2$}", column.value_fraction, dps2=max(opts.pp_decimals, 2))).style_spec("cH2"));
|
||||||
|
|
||||||
if show_transfers_per_ballot {
|
if show_transfers_per_column {
|
||||||
if self.surplus.is_some() {
|
if self.surplus.is_some() {
|
||||||
row.push(Cell::new(&format!("× {:.dps2$}", self.surpfrac.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2))).style_spec("r"));
|
row.push(Cell::new(&format!("× {:.dps2$}", self.surpfrac.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2))).style_spec("r"));
|
||||||
} else {
|
} else {
|
||||||
@ -342,7 +395,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
row.push(Cell::new("Total").style_spec("cH2"));
|
row.push(Cell::new("Total").style_spec("cH2"));
|
||||||
if self.surpfrac.is_some() {
|
if self.surpfrac.is_some() {
|
||||||
row.push(Cell::new(&format!("× {:.dps2$}", self.surpfrac.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2))).style_spec("r"));
|
row.push(Cell::new(&format!("× {:.dps2$}", self.surpfrac.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2))).style_spec("r"));
|
||||||
} else if show_transfers_per_ballot {
|
} else if show_transfers_per_column {
|
||||||
row.push(Cell::new("=").style_spec("c"));
|
row.push(Cell::new("=").style_spec("c"));
|
||||||
}
|
}
|
||||||
table.set_titles(Row::new(row));
|
table.set_titles(Row::new(row));
|
||||||
@ -357,13 +410,13 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
if let Some(cell) = column.cells.get(candidate) {
|
if let Some(cell) = column.cells.get(candidate) {
|
||||||
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
|
||||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||||
if show_transfers_per_ballot {
|
if show_transfers_per_column {
|
||||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
if show_transfers_per_ballot {
|
if show_transfers_per_column {
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -373,13 +426,13 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
if let Some(cell) = self.total.cells.get(candidate) {
|
if let Some(cell) = self.total.cells.get(candidate) {
|
||||||
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
|
||||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||||
if self.surpfrac.is_some() || show_transfers_per_ballot {
|
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
if self.surpfrac.is_some() || show_transfers_per_ballot {
|
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -396,7 +449,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
if !column.exhausted.ballots.is_zero() {
|
if !column.exhausted.ballots.is_zero() {
|
||||||
row.push(Cell::new(&format!("{:.0}", column.exhausted.ballots)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.0}", column.exhausted.ballots)).style_spec("r"));
|
||||||
row.push(Cell::new(&format!("{:.dps$}", column.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.dps$}", column.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||||
if show_transfers_per_ballot {
|
if show_transfers_per_column {
|
||||||
if column.exhausted.votes_out.is_zero() {
|
if column.exhausted.votes_out.is_zero() {
|
||||||
row.push(Cell::new("-").style_spec("c"));
|
row.push(Cell::new("-").style_spec("c"));
|
||||||
} else {
|
} else {
|
||||||
@ -406,7 +459,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
} else {
|
} else {
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
if show_transfers_per_ballot {
|
if show_transfers_per_column {
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -416,7 +469,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
if !self.total.exhausted.ballots.is_zero() {
|
if !self.total.exhausted.ballots.is_zero() {
|
||||||
row.push(Cell::new(&format!("{:.0}", self.total.exhausted.ballots)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.0}", self.total.exhausted.ballots)).style_spec("r"));
|
||||||
row.push(Cell::new(&format!("{:.dps$}", self.total.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.dps$}", self.total.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||||
if self.surpfrac.is_some() || show_transfers_per_ballot {
|
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||||
if self.total.exhausted.votes_out.is_zero() {
|
if self.total.exhausted.votes_out.is_zero() {
|
||||||
row.push(Cell::new("-").style_spec("c"));
|
row.push(Cell::new("-").style_spec("c"));
|
||||||
} else {
|
} else {
|
||||||
@ -426,7 +479,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
} else {
|
} else {
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
if self.surpfrac.is_some() || show_transfers_per_ballot {
|
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||||
row.push(Cell::new(""));
|
row.push(Cell::new(""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -442,7 +495,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
for column in self.columns.iter() {
|
for column in self.columns.iter() {
|
||||||
row.push(Cell::new(&format!("{:.0}", column.total.ballots)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.0}", column.total.ballots)).style_spec("r"));
|
||||||
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||||
if show_transfers_per_ballot {
|
if show_transfers_per_column {
|
||||||
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -466,7 +519,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
|
|
||||||
row.push(Cell::new(&format!("{:.0}", gt_ballots)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.0}", gt_ballots)).style_spec("r"));
|
||||||
row.push(Cell::new(&format!("{:.dps$}", gt_votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.dps$}", gt_votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||||
if self.surpfrac.is_some() || show_transfers_per_ballot {
|
if self.surpfrac.is_some() || show_transfers_per_column {
|
||||||
row.push(Cell::new(&format!("{:.dps$}", gt_votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
row.push(Cell::new(&format!("{:.dps$}", gt_votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -493,6 +546,17 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Multiply the specified number by the surplus fraction (if applicable)
|
||||||
|
fn multiply_surpfrac<N: Number>(mut number: N, surpfrac_numer: &Option<N>, surpfrac_denom: &Option<N>) -> N {
|
||||||
|
if let Some(n) = surpfrac_numer {
|
||||||
|
number *= n;
|
||||||
|
}
|
||||||
|
if let Some(n) = surpfrac_denom {
|
||||||
|
number /= n;
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
/// Column in a [TransferTable]
|
/// Column in a [TransferTable]
|
||||||
pub struct TransferTableColumn<'e, N: Number> {
|
pub struct TransferTableColumn<'e, N: Number> {
|
||||||
/// Value fraction of ballots counted in this column
|
/// Value fraction of ballots counted in this column
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/* OpenTally: Open-source election vote counting
|
/* OpenTally: Open-source election vote counting
|
||||||
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -64,9 +64,9 @@ pub struct STVOptions {
|
|||||||
#[builder(default="None")]
|
#[builder(default="None")]
|
||||||
pub round_quota: Option<usize>,
|
pub round_quota: Option<usize>,
|
||||||
|
|
||||||
/// How to calculate votes to credit to candidates in surplus transfers
|
/// How to round votes in transfer table
|
||||||
#[builder(default="SumSurplusTransfersMode::ByValue")]
|
#[builder(default="RoundSubtransfersMode::SingleStep")]
|
||||||
pub sum_surplus_transfers: SumSurplusTransfersMode,
|
pub round_subtransfers: RoundSubtransfersMode,
|
||||||
|
|
||||||
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
||||||
#[builder(default=r#"String::from("0.001%")"#)]
|
#[builder(default=r#"String::from("0.001%")"#)]
|
||||||
@ -176,7 +176,7 @@ impl STVOptions {
|
|||||||
if let Some(dps) = self.round_votes { flags.push(format!("--round-votes {}", dps)); }
|
if let Some(dps) = self.round_votes { flags.push(format!("--round-votes {}", dps)); }
|
||||||
}
|
}
|
||||||
if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); }
|
if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); }
|
||||||
if self.surplus != SurplusMethod::Meek && self.sum_surplus_transfers != SumSurplusTransfersMode::ByValue { flags.push(self.sum_surplus_transfers.describe()); }
|
if self.surplus != SurplusMethod::Meek && self.round_subtransfers != RoundSubtransfersMode::SingleStep { flags.push(self.round_subtransfers.describe()); }
|
||||||
if self.surplus == SurplusMethod::Meek && self.meek_surplus_tolerance != "0.001%" { flags.push(format!("--meek-surplus-tolerance {}", self.meek_surplus_tolerance)); }
|
if self.surplus == SurplusMethod::Meek && self.meek_surplus_tolerance != "0.001%" { flags.push(format!("--meek-surplus-tolerance {}", self.meek_surplus_tolerance)); }
|
||||||
if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); }
|
if self.normalise_ballots { flags.push("--normalise-ballots".to_string()); }
|
||||||
if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); }
|
if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); }
|
||||||
@ -229,32 +229,40 @@ impl STVOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enum of options for [STVOptions::sum_surplus_transfers]
|
/// Enum of options for [STVOptions::round_subtransfers]
|
||||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum SumSurplusTransfersMode {
|
pub enum RoundSubtransfersMode {
|
||||||
/// Sum and round a candidate's surplus transfers separately for ballot papers received at each particular value
|
/// Do not round subtransfers (only round final number of votes credited)
|
||||||
|
SingleStep,
|
||||||
|
/// Round in subtransfers according to the value when received
|
||||||
ByValue,
|
ByValue,
|
||||||
/// Sum and round a candidate's surplus transfers individually for each ballot paper
|
/// Round in subtransfers according to the candidate from who each vote was received, and the value when received
|
||||||
|
ByValueAndSource,
|
||||||
|
/// Sum and round transfers individually for each ballot paper
|
||||||
PerBallot,
|
PerBallot,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SumSurplusTransfersMode {
|
impl RoundSubtransfersMode {
|
||||||
/// Convert to CLI argument representation
|
/// Convert to CLI argument representation
|
||||||
fn describe(self) -> String {
|
fn describe(self) -> String {
|
||||||
match self {
|
match self {
|
||||||
SumSurplusTransfersMode::ByValue => "--sum-surplus-transfers by_value",
|
RoundSubtransfersMode::SingleStep => "--sum-surplus-transfers single_step",
|
||||||
SumSurplusTransfersMode::PerBallot => "--sum-surplus-transfers per_ballot",
|
RoundSubtransfersMode::ByValue => "--sum-surplus-transfers by_value",
|
||||||
|
RoundSubtransfersMode::ByValueAndSource => "--sum-surplus-transfers by_value_and_source",
|
||||||
|
RoundSubtransfersMode::PerBallot => "--sum-surplus-transfers per_ballot",
|
||||||
}.to_string()
|
}.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: AsRef<str>> From<S> for SumSurplusTransfersMode {
|
impl<S: AsRef<str>> From<S> for RoundSubtransfersMode {
|
||||||
fn from(s: S) -> Self {
|
fn from(s: S) -> Self {
|
||||||
match s.as_ref() {
|
match s.as_ref() {
|
||||||
"by_value" => SumSurplusTransfersMode::ByValue,
|
"single_step" => RoundSubtransfersMode::SingleStep,
|
||||||
"per_ballot" => SumSurplusTransfersMode::PerBallot,
|
"by_value" => RoundSubtransfersMode::ByValue,
|
||||||
|
"by_value_and_source" => RoundSubtransfersMode::ByValueAndSource,
|
||||||
|
"per_ballot" => RoundSubtransfersMode::PerBallot,
|
||||||
_ => panic!("Invalid --sum-transfers"),
|
_ => panic!("Invalid --sum-transfers"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,7 +239,7 @@ impl STVOptions {
|
|||||||
round_values: Option<usize>,
|
round_values: Option<usize>,
|
||||||
round_votes: Option<usize>,
|
round_votes: Option<usize>,
|
||||||
round_quota: Option<usize>,
|
round_quota: Option<usize>,
|
||||||
sum_surplus_transfers: &str,
|
round_subtransfers: &str,
|
||||||
meek_surplus_tolerance: String,
|
meek_surplus_tolerance: String,
|
||||||
normalise_ballots: bool,
|
normalise_ballots: bool,
|
||||||
quota: &str,
|
quota: &str,
|
||||||
@ -268,7 +268,7 @@ impl STVOptions {
|
|||||||
round_values,
|
round_values,
|
||||||
round_votes,
|
round_votes,
|
||||||
round_quota,
|
round_quota,
|
||||||
sum_surplus_transfers.into(),
|
round_subtransfers.into(),
|
||||||
meek_surplus_tolerance,
|
meek_surplus_tolerance,
|
||||||
normalise_ballots,
|
normalise_ballots,
|
||||||
quota.into(),
|
quota.into(),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/* OpenTally: Open-source election vote counting
|
/* OpenTally: Open-source election vote counting
|
||||||
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -27,7 +27,7 @@ fn ers97_coe_rational() {
|
|||||||
.round_values(Some(2))
|
.round_values(Some(2))
|
||||||
.round_votes(Some(2))
|
.round_votes(Some(2))
|
||||||
.round_quota(Some(2))
|
.round_quota(Some(2))
|
||||||
.sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot)
|
.round_subtransfers(stv::RoundSubtransfersMode::PerBallot)
|
||||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||||
.surplus(stv::SurplusMethod::EG)
|
.surplus(stv::SurplusMethod::EG)
|
||||||
.transferable_only(true)
|
.transferable_only(true)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/* OpenTally: Open-source election vote counting
|
/* OpenTally: Open-source election vote counting
|
||||||
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
* Copyright © 2021–2022 Lee Yingtong Li (RunasSudo)
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -32,7 +32,7 @@ fn scotland_linn07_fixed5() {
|
|||||||
//.round_values(Some(5))
|
//.round_values(Some(5))
|
||||||
//.round_votes(Some(5))
|
//.round_votes(Some(5))
|
||||||
.round_quota(Some(0))
|
.round_quota(Some(0))
|
||||||
.sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot)
|
.round_subtransfers(stv::RoundSubtransfersMode::PerBallot)
|
||||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||||
.early_bulk_elect(false)
|
.early_bulk_elect(false)
|
||||||
.pp_decimals(5)
|
.pp_decimals(5)
|
||||||
@ -52,7 +52,7 @@ fn scotland_linn07_gfixed5() {
|
|||||||
.round_values(Some(5)) // Must specify rounding as guarded decimals represented to 10 dps internally
|
.round_values(Some(5)) // Must specify rounding as guarded decimals represented to 10 dps internally
|
||||||
.round_votes(Some(5))
|
.round_votes(Some(5))
|
||||||
.round_quota(Some(0))
|
.round_quota(Some(0))
|
||||||
.sum_surplus_transfers(stv::SumSurplusTransfersMode::PerBallot)
|
.round_subtransfers(stv::RoundSubtransfersMode::PerBallot)
|
||||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||||
.early_bulk_elect(false)
|
.early_bulk_elect(false)
|
||||||
.pp_decimals(5)
|
.pp_decimals(5)
|
||||||
|
Loading…
Reference in New Issue
Block a user