Implement Dáil Éireann STV
This commit is contained in:
parent
a641b97d1f
commit
3a4e53e1f0
@ -17,11 +17,12 @@ The preset dropdown allows you to choose from a hardcoded list of preloaded STV
|
||||
| Australian Capital Territory STV | Rules from the [*Electoral Act 1992* (ACT)](https://www.legislation.act.gov.au/View/a/1992-71/current/PDF/1992-71.PDF), using the exclusive Gregory method. | | ✓ |
|
||||
| Minneapolis STV | Rules from chapter 167 of the [*Minneapolis Code of Ordinances*](https://library.municode.com/mn/minneapolis/codes/code_of_ordinances?nodeId=COOR_TIT8.5EL_CH167MUELRUCO), using the weighted inclusive Gregory method. | [E5] | ✓ |
|
||||
| Cambridge STV | Rules in force in Cambridge, Massachusetts, using random sample transfers. These rules are derived from the [former chapter 54A of the Massachusetts General Laws](https://www.cambridgema.gov/-/media/Files/electioncommission/massachusettsgenerallawschapter54a.pdf), but have by regulation been modified to incorporate the procedures set out in Article IX of the former [1938 Charter of the City of Cincinnati](https://catalog.hathitrust.org/Record/001754258). See also [here](https://web.archive.org/web/20081118104049/http://www.fairvote.org/media/1993countmanual.pdf). | | ✓ |
|
||||
| Dáil Éireann STV | Rules from the [*Electoral Act 1992* (Ireland)](http://www.irishstatutebook.ie/eli/1992/act/23/enacted/en/print), using stratified random sample transfers. | [E4] [E6] | ✓ |
|
||||
| [Wright STV](https://www.aph.gov.au/Parliamentary_Business/Committees/House_of_Representatives_Committees?url=em/elect07/subs/sub051.1.pdf) | Rules proposed by Anthony van der Craats designed for computer counting, involving reset and re-iteration of the count after each candidate exclusion. | | ✓ |
|
||||
| [PRSA 1977](https://www.prsa.org.au/rule1977.htm) | Simple rules designed for hand counting, using the exclusive Gregory method, with counting performed in thousandths of a vote. | | ✓ |
|
||||
| [ERS97](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/) | More complex rules designed for hand counting, using the exclusive Gregory method. | [E6] | ✓ |
|
||||
| • ERS76 | Former rules from the 1976 2nd edition. | [E6] [E7] | ✓ |
|
||||
| • ERS73 | Former rules from the 1973 1st edition. | [E6] [E7] | |
|
||||
| [ERS97](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/) | More complex rules designed for hand counting, using the exclusive Gregory method. | [E7] | ✓ |
|
||||
| • ERS76 | Former rules from the 1976 2nd edition. | [E7] [E8] | ✓ |
|
||||
| • ERS73 | Former rules from the 1973 1st edition. | [E7] [E8] | |
|
||||
| Church of England | Rules from the Church of England [*Single Transferable Vote Rules 2020*](https://www.churchofengland.org/sites/default/files/2020-02/STV%20Rules%202020%20-%20final.pdf), similar to ERS73. | | ✓ |
|
||||
|
||||
Exceptions:
|
||||
@ -29,10 +30,11 @@ Exceptions:
|
||||
* [E1] When generating random numbers, OpenTally uses a [deterministic random number generator based on SHA-256](rng.md), rather than the Wichmann–Hill(-based) algorithm.
|
||||
* [E2] When breaking ties backwards, OpenTally selects the candidate who had more/fewer votes at the last stage when *any* tied candidate had more/fewer votes, rather than the method described in the legislation (when each all had unequal votes). The OpenTally developers regard the method described in the legislation as a defect. For an independent discussion, see <a href="https://dl.acm.org/doi/10.1145/3014812.3014837">Conway et al.</a>
|
||||
* [E3] A tie between 2 candidates for the final vacancy will be broken backwards then at random, rather than the method described in the legislation.
|
||||
* [E4] Bulk exclusion is not performed. See the section on *Bulk exclusion* for further discussion.
|
||||
* [E4] Bulk exclusion is not performed, as the prescribed rules are more conservative than OpenTally's. See also the section on *Bulk exclusion* for further discussion.
|
||||
* [E5] The ‘mathematically eliminated by the sum of all ranked-choice votes comparison’ is not implemented.
|
||||
* [E6] No distinction is made between stages and substages (during exclusion). This affects only the numbering of stages and not the result.
|
||||
* [E7] By default, the quota is always calculated to 2 decimal places. For full ERS76 (ERS73) compliance, set *Round quota to 0 d.p.* when the quota is more than 100 (100 or more).
|
||||
* [E6] The ‘quarter of a quota’ provision is disregarded when determining whether to defer surplus distributions.
|
||||
* [E7] No distinction is made between stages and substages (during exclusion). This affects only the numbering of stages and not the result.
|
||||
* [E8] By default, the quota is always calculated to 2 decimal places. For full ERS76 (ERS73) compliance, set *Round quota to 0 d.p.* when the quota is more than 100 (100 or more).
|
||||
|
||||
For details of validation, see [validation.md](validation.md).
|
||||
|
||||
@ -126,7 +128,8 @@ When *Surplus method* is set to *Meek method*, this option controls how candidat
|
||||
|
||||
When *Surplus method* is set to a random sample method, this option controls which subset of ballot papers is selected for transfer during surplus distributions:
|
||||
|
||||
* *Stratified (then by order)* (default): The candidate's ballot papers are first stratified into subparcels according to next available preference. From each subparcel, the subset transferred comprises the ballot papers most recently received by the candidate.
|
||||
* *Stratify (LR)* (default): The candidate's ballot papers are first stratified into subparcels according to next available preference, and an equal proportion of each subparcel is transferred, with the subset transferred comprising the ballot papers in each subparcel most recently received by the candidate. In the calculation of proportions, the largest remainders are rounded up so there is no loss by fraction. This is the method specified by the [*Electoral Act 1992* (Ireland)](http://www.irishstatutebook.ie/eli/1992/act/23/section/121/enacted/en/html#sec121).
|
||||
* *Stratify (floor)*: The same as *Stratify (LR)*, except in the calculation of proportions, all remainders are disregarded, and the difference is lost by fraction.
|
||||
* *By order*: The subset transferred comprises the ballot papers most recently received by the candidate.
|
||||
* *Every n-th ballot*: The subset is selected using the deterministic method used in [Cambridge, Massachusetts](https://web.archive.org/web/20081118104049/http://www.fairvote.org/media/1993countmanual.pdf) (derived from Article IX of the former 1938 Cincinnati *Code of Ordinances*).
|
||||
|
||||
|
@ -14,6 +14,7 @@ STV-counting software is frequently validated empirically by comparing the resul
|
||||
| Minneapolis STV | [2009 Minneapolis Board of Estimate and Taxation election](https://vote.minneapolismn.gov/results-data/election-results/2009/bet/) | Results sheet (official) | ✓ |
|
||||
| Minneapolis STV | [2013 Minneapolis Parks & Recreation Commissioner At Large election](https://vote.minneapolismn.gov/results-data/election-results/2013/park-board-at-large/) | Results sheet (official) | ✓ |
|
||||
| Cambridge STV | [2003 Cambridge City Council election](https://web.archive.org/web/20070204083508/http://stv.sourceforge.net/) | OpenSTV 1.7, [ChoicePlus Pro 2.1](https://www.votingsolutions.com/cpdetail.htm) (official) | ✓ |
|
||||
| Dáil Éireann STV | [2002 Dublin North election](https://electionsireland.org/counts.cfm?election=2002&cons=96) | Results sheet (official) | ✓ |
|
||||
| Wright STV | [EVE Online CSM 15 election](https://www.eveonline.com/news/view/meet-the-new-council) | [ccp-wright-stv](https://github.com/ccpgames/ccp-wright-stv) (official) | ✓ |
|
||||
| PRSA 1977 | [*Proportional Representation Manual*](https://www.prsa.org.au/publicat.htm#p2) [example 1](https://www.prsa.org.au/utopiatc.pdf) | [Model result](https://www.prsa.org.au/example1.pdf) (official) | ✓ |
|
||||
| PRSA 1977 | 40 elections from [stvdb](https://gitlab.com/RunasSudo/stvdb) | [count.nl (RunasSudo version)](https://gitlab.com/RunasSudo/prsa_count) | ✓ |
|
||||
|
@ -47,6 +47,7 @@
|
||||
<option value="meeknz">Meek STV (New Zealand)</option>
|
||||
<option value="minneapolis">Minneapolis STV</option>
|
||||
<option value="cambridge">Cambridge STV</option>
|
||||
<option value="dail">Dáil Éireann STV</option>
|
||||
</optgroup>
|
||||
<optgroup label="Hand-count">
|
||||
<option value="prsa77">PRSA 1977</option>
|
||||
@ -148,7 +149,8 @@
|
||||
<span class="pill-grey" title="This option has effect only if “Method” is set to a random sample method">Sample</span>
|
||||
Sample method:
|
||||
<select id="selSample">
|
||||
<option value="stratified" selected>Stratified (then by order)</option>
|
||||
<option value="stratify_lr" selected>Stratify (LR)</option>
|
||||
<option value="stratify_floor" selected>Stratify (floor)</option>
|
||||
<option value="by_order">By order</option>
|
||||
<option value="nth_ballot">Every n-th ballot</option>
|
||||
</select>
|
||||
|
@ -678,6 +678,28 @@ function changePreset() {
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'backwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'dail') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
document.getElementById('chkImmediateElect').checked = true;
|
||||
document.getElementById('selSample').value = 'stratify_lr';
|
||||
document.getElementById('chkSamplePerBallot').checked = false;
|
||||
document.getElementById('txtMinThreshold').value = '0';
|
||||
document.getElementById('selNumbers').value = 'rational';
|
||||
document.getElementById('txtPPDP').value = '0';
|
||||
document.getElementById('chkNormaliseBallots').checked = true;
|
||||
document.getElementById('chkRoundQuota').checked = true;
|
||||
document.getElementById('txtRoundQuota').value = '0';
|
||||
document.getElementById('selSumTransfers').value = 'by_value';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'hare';
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'single_stage';
|
||||
document.getElementById('selTies').value = 'forwards,random';
|
||||
} else if (document.getElementById('selPreset').value === 'wright') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
|
@ -134,7 +134,7 @@ pub struct SubcmdOptions {
|
||||
meek_nz_exclusion: bool,
|
||||
|
||||
/// (Cincinnati/Hare) Method of drawing a sample
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["stratified", "by_order", "nth_ballot"], default_value="stratified", value_name="method")]
|
||||
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["stratify_lr", "stratify_floor", "by_order", "nth_ballot"], default_value="stratify_lr", value_name="method")]
|
||||
sample: String,
|
||||
|
||||
/// (Cincinnati/Hare) Sample-based methods: Check for candidate election after each individual ballot paper transfer
|
||||
|
@ -71,8 +71,8 @@ impl Number for Rational {
|
||||
factor.pow_assign(-(dps as i32));
|
||||
factor /= Self::from(2);
|
||||
|
||||
*self = self.clone() - factor;
|
||||
self.ceil_mut(dps);
|
||||
*self = self.clone() + factor;
|
||||
self.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,9 +179,7 @@ impl ops::Add for Rational {
|
||||
|
||||
impl ops::Sub for Rational {
|
||||
type Output = Rational;
|
||||
fn sub(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn sub(self, rhs: Self) -> Self::Output { Self(self.0 - rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Mul for Rational {
|
||||
@ -196,9 +194,7 @@ impl ops::Div for Rational {
|
||||
|
||||
impl ops::Rem for Rational {
|
||||
type Output = Rational;
|
||||
fn rem(self, _rhs: Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn rem(self, rhs: Self) -> Self::Output { Self(self.0 % rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Add<&Rational> for Rational {
|
||||
@ -223,9 +219,7 @@ impl ops::Div<&Rational> for Rational {
|
||||
|
||||
impl ops::Rem<&Rational> for Rational {
|
||||
type Output = Rational;
|
||||
fn rem(self, _rhs: &Rational) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn rem(self, rhs: &Rational) -> Self::Output { Rational(self.0 % &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::AddAssign for Rational {
|
||||
@ -245,9 +239,7 @@ impl ops::DivAssign for Rational {
|
||||
}
|
||||
|
||||
impl ops::RemAssign for Rational {
|
||||
fn rem_assign(&mut self, _rhs: Self) {
|
||||
todo!()
|
||||
}
|
||||
fn rem_assign(&mut self, rhs: Self) { self.0 %= &rhs.0; }
|
||||
}
|
||||
|
||||
impl ops::AddAssign<&Rational> for Rational {
|
||||
@ -263,13 +255,11 @@ impl ops::MulAssign<&Rational> for Rational {
|
||||
}
|
||||
|
||||
impl ops::DivAssign<&Rational> for Rational {
|
||||
fn div_assign(&mut self, rhs: &Rational) { self.0 /= &rhs.0 }
|
||||
fn div_assign(&mut self, rhs: &Rational) { self.0 /= &rhs.0; }
|
||||
}
|
||||
|
||||
impl ops::RemAssign<&Rational> for Rational {
|
||||
fn rem_assign(&mut self, _rhs: &Rational) {
|
||||
todo!()
|
||||
}
|
||||
fn rem_assign(&mut self, rhs: &Rational) { self.0 %= &rhs.0; }
|
||||
}
|
||||
|
||||
impl ops::Neg for &Rational {
|
||||
@ -299,9 +289,7 @@ impl ops::Div<Self> for &Rational {
|
||||
|
||||
impl ops::Rem<Self> for &Rational {
|
||||
type Output = Rational;
|
||||
fn rem(self, _rhs: &Rational) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn rem(self, rhs: &Rational) -> Self::Output { Rational(&self.0 % &rhs.0) }
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -112,7 +112,7 @@ pub struct STVOptions {
|
||||
pub meek_nz_exclusion: bool,
|
||||
|
||||
/// (Cincinnati/Hare) Method of drawing a sample
|
||||
#[builder(default="SampleMethod::Stratified")]
|
||||
#[builder(default="SampleMethod::StratifyLR")]
|
||||
pub sample: SampleMethod,
|
||||
|
||||
/// (Cincinnati/Hare) Sample-based methods: Check for candidate election after each individual ballot paper transfer
|
||||
@ -191,7 +191,7 @@ impl STVOptions {
|
||||
if self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); }
|
||||
}
|
||||
if self.surplus == SurplusMethod::Meek && self.meek_nz_exclusion { flags.push("--meek-nz-exclusion".to_string()); }
|
||||
if (self.surplus == SurplusMethod::Cincinnati || self.surplus == SurplusMethod::Hare) && self.sample != SampleMethod::Stratified { flags.push(self.sample.describe()); }
|
||||
if (self.surplus == SurplusMethod::Cincinnati || self.surplus == SurplusMethod::Hare) && self.sample != SampleMethod::StratifyLR { flags.push(self.sample.describe()); }
|
||||
if (self.surplus == SurplusMethod::Cincinnati || self.surplus == SurplusMethod::Hare) && self.sample_per_ballot { flags.push("--sample-per-ballot".to_string()); }
|
||||
if !self.early_bulk_elect { flags.push("--no-early-bulk-elect".to_string()); }
|
||||
if self.bulk_exclude { flags.push("--bulk-exclude".to_string()); }
|
||||
@ -219,7 +219,9 @@ impl STVOptions {
|
||||
if self.surplus == SurplusMethod::Cincinnati || self.surplus == SurplusMethod::Hare {
|
||||
if self.round_quota != Some(0) { return Err(STVError::InvalidOptions("--surplus cincinnati and --surplus hare require --round-quota 0")); }
|
||||
if !self.normalise_ballots { return Err(STVError::InvalidOptions("--surplus cincinnati and --surplus hare require --normalise-ballots")); }
|
||||
if self.sample == SampleMethod::Stratified && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratified is incompatible with --sample-per-ballot")); }
|
||||
if self.sample == SampleMethod::StratifyLR && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify_lr is incompatible with --sample-per-ballot")); }
|
||||
if self.sample == SampleMethod::StratifyFloor && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify_floor is incompatible with --sample-per-ballot")); }
|
||||
if self.sample == SampleMethod::StratifyLR && self.round_surplus_fractions.is_some() { return Err(STVError::InvalidOptions("--sample stratify_lr is incompatible with --round-surplus-fractions")); }
|
||||
if self.sample_per_ballot && !self.immediate_elect { return Err(STVError::InvalidOptions("--sample-per-ballot is incompatible with --no-immediate-elect")); }
|
||||
}
|
||||
if self.min_threshold != "0" && self.defer_surpluses { return Err(STVError::InvalidOptions("--min-threshold is incompatible with --defer-surpluses")); } // TODO: Permit this
|
||||
@ -506,8 +508,10 @@ impl<S: AsRef<str>> From<S> for ExclusionMethod {
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum SampleMethod {
|
||||
/// Stratify the ballots into parcels according to next available preference and transfer the last ballots from each parcel
|
||||
Stratified,
|
||||
/// Stratify the ballots into parcels according to next available preference and transfer the last ballots from each parcel; round fractions according to largest remainders
|
||||
StratifyLR,
|
||||
/// Stratify the ballots into parcels according to next available preference and transfer the last ballots from each parcel; disregard fractions
|
||||
StratifyFloor,
|
||||
/// Transfer the last ballots
|
||||
ByOrder,
|
||||
/// Transfer every n-th ballot, Cincinnati style
|
||||
@ -518,7 +522,8 @@ impl SampleMethod {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
SampleMethod::Stratified => "--sample stratified",
|
||||
SampleMethod::StratifyLR => "--sample stratify_lr",
|
||||
SampleMethod::StratifyFloor => "--sample stratify_floor",
|
||||
SampleMethod::ByOrder => "--sample by_order",
|
||||
SampleMethod::NthBallot => "--sample nth_ballot",
|
||||
}.to_string()
|
||||
@ -528,7 +533,8 @@ impl SampleMethod {
|
||||
impl<S: AsRef<str>> From<S> for SampleMethod {
|
||||
fn from(s: S) -> Self {
|
||||
match s.as_ref() {
|
||||
"stratified" => SampleMethod::Stratified,
|
||||
"stratify_lr" => SampleMethod::StratifyLR,
|
||||
"stratify_floor" => SampleMethod::StratifyFloor,
|
||||
"by_order" => SampleMethod::ByOrder,
|
||||
"nth_ballot" => SampleMethod::NthBallot,
|
||||
_ => panic!("Invalid --sample-method"),
|
||||
|
@ -48,6 +48,7 @@ where
|
||||
pub fn distribute_surplus<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, elected_candidate: &'a Candidate) -> Result<(), STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
@ -72,16 +73,14 @@ where
|
||||
}
|
||||
|
||||
match opts.sample {
|
||||
SampleMethod::Stratified => {
|
||||
// Stratified by next available preference
|
||||
// FIXME: This is untested
|
||||
|
||||
SampleMethod::StratifyLR | SampleMethod::StratifyFloor => {
|
||||
// Stratified by next available preference (round fractions according to largest remainders)
|
||||
let result = super::next_preferences(state, votes);
|
||||
|
||||
let transferable_ballots = &result.total_ballots - &result.exhausted.num_ballots;
|
||||
let surplus_denom = calculate_surplus_denom(&surplus, &result, &transferable_ballots, opts.transferable_only);
|
||||
let mut surplus_fraction;
|
||||
match surplus_denom {
|
||||
match &surplus_denom {
|
||||
Some(v) => {
|
||||
surplus_fraction = Some(surplus.clone() / v);
|
||||
|
||||
@ -116,22 +115,107 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let mut checksum = N::new();
|
||||
let mut candidate_transfers_remainders: HashMap<Option<&Candidate>, (N, N)> = HashMap::new(); // None -> exhausted pile
|
||||
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
// Credit transferred votes
|
||||
for (candidate, entry) in result.candidates.iter() {
|
||||
// Calculate votes to transfer
|
||||
let mut candidate_transfers;
|
||||
let remainder;
|
||||
match surplus_fraction {
|
||||
Some(ref f) => {
|
||||
candidate_transfers = entry.num_ballots * f;
|
||||
candidate_transfers.floor_mut(0);
|
||||
match opts.sample {
|
||||
SampleMethod::StratifyLR => {
|
||||
// Incompatible with --round-surplus-fractions
|
||||
candidate_transfers = &entry.num_ballots * &surplus / surplus_denom.as_ref().unwrap();
|
||||
candidate_transfers.floor_mut(0);
|
||||
remainder = (&entry.num_ballots * &surplus) % surplus_denom.as_ref().unwrap();
|
||||
}
|
||||
SampleMethod::StratifyFloor => {
|
||||
match opts.round_surplus_fractions {
|
||||
Some(_) => {
|
||||
candidate_transfers = &entry.num_ballots * f;
|
||||
}
|
||||
None => {
|
||||
candidate_transfers = &entry.num_ballots * &surplus / surplus_denom.as_ref().unwrap();
|
||||
}
|
||||
}
|
||||
candidate_transfers.floor_mut(0);
|
||||
remainder = N::new();
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// All ballots transferred
|
||||
candidate_transfers = entry.num_ballots.clone();
|
||||
remainder = N::new();
|
||||
}
|
||||
}
|
||||
let candidate_transfers_usize: usize = format!("{:.0}", candidate_transfers).parse().expect("Transfers overflow usize");
|
||||
|
||||
candidate_transfers_remainders.insert(Some(candidate), (candidate_transfers, remainder));
|
||||
}
|
||||
|
||||
// Calculate exhausted votes to transfer
|
||||
if !opts.transferable_only {
|
||||
let mut exhausted_transfers;
|
||||
let remainder;
|
||||
match opts.sample {
|
||||
SampleMethod::StratifyLR => {
|
||||
// Incompatible with --round-surplus-fractions
|
||||
exhausted_transfers = &result.exhausted.num_ballots * &surplus / surplus_denom.as_ref().unwrap();
|
||||
exhausted_transfers.floor_mut(0);
|
||||
remainder = (&result.exhausted.num_ballots * &surplus) % surplus_denom.as_ref().unwrap();
|
||||
}
|
||||
SampleMethod::StratifyFloor => {
|
||||
match opts.round_surplus_fractions {
|
||||
Some(_) => {
|
||||
exhausted_transfers = &result.exhausted.num_ballots * surplus_fraction.as_ref().unwrap();
|
||||
}
|
||||
None => {
|
||||
exhausted_transfers = &result.exhausted.num_ballots * &surplus / surplus_denom.as_ref().unwrap();
|
||||
}
|
||||
}
|
||||
exhausted_transfers.floor_mut(0);
|
||||
remainder = N::new();
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
|
||||
candidate_transfers_remainders.insert(None, (exhausted_transfers, remainder));
|
||||
}
|
||||
|
||||
if opts.sample == SampleMethod::StratifyLR {
|
||||
// Round remainders to remove loss by fraction
|
||||
let transferred = candidate_transfers_remainders.values().fold(N::new(), |acc, (t, _)| acc + t);
|
||||
let loss_fraction = &surplus - &transferred;
|
||||
if !loss_fraction.is_zero() {
|
||||
let n_to_round: usize = format!("{:.0}", loss_fraction).parse().expect("Loss by fraction overflows usize");
|
||||
|
||||
let mut cands_by_remainder: Vec<Option<&Candidate>> = candidate_transfers_remainders.keys().cloned().collect();
|
||||
// Compare b to a to sort high-low
|
||||
cands_by_remainder.sort_unstable_by(|a, b| candidate_transfers_remainders[b].1.cmp(&candidate_transfers_remainders[a].1));
|
||||
|
||||
// Select top remainders
|
||||
let top_remainders: Vec<&Option<&Candidate>> = cands_by_remainder.iter().take(n_to_round).collect();
|
||||
|
||||
// Check for tied remainders
|
||||
if candidate_transfers_remainders[top_remainders.last().unwrap()].1 == candidate_transfers_remainders[cands_by_remainder.iter().nth(n_to_round + 1).unwrap()].1 {
|
||||
todo!("Tie for largest remainders");
|
||||
}
|
||||
|
||||
// Round up top remainders
|
||||
for candidate in top_remainders {
|
||||
candidate_transfers_remainders.get_mut(candidate).unwrap().0 += N::one();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut checksum = N::new();
|
||||
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
// Credit transferred votes
|
||||
let candidate_transfers = &candidate_transfers_remainders[&Some(candidate)].0;
|
||||
let candidate_transfers_usize: usize = format!("{:.0}", candidate_transfers).parse().expect("Transfer overflows usize");
|
||||
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.transfer(&candidate_transfers);
|
||||
@ -154,16 +238,14 @@ where
|
||||
exhausted_transfers = N::new();
|
||||
} else {
|
||||
exhausted_transfers = &surplus - &transferable_ballots;
|
||||
|
||||
if let Some(dps) = opts.round_votes {
|
||||
exhausted_transfers.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
exhausted_transfers = result.exhausted.num_ballots * surplus_fraction.as_ref().unwrap();
|
||||
exhausted_transfers.floor_mut(0);
|
||||
exhausted_transfers = candidate_transfers_remainders.remove(&None).unwrap().0;
|
||||
}
|
||||
let exhausted_transfers_usize: usize = format!("{:.0}", exhausted_transfers).parse().expect("Transfers overflow usize");
|
||||
let exhausted_transfers_usize: usize = format!("{:.0}", exhausted_transfers).parse().expect("Transfer overflows usize");
|
||||
|
||||
state.exhausted.transfer(&exhausted_transfers);
|
||||
checksum += exhausted_transfers;
|
||||
|
19319
tests/data/DublinNorthSorted.blt
Normal file
19319
tests/data/DublinNorthSorted.blt
Normal file
File diff suppressed because it is too large
Load Diff
15
tests/data/DublinNorthSorted.csv
Normal file
15
tests/data/DublinNorthSorted.csv
Normal file
@ -0,0 +1,15 @@
|
||||
Stage:,1,,2,,3,,4,,5,,6,,7,,8,,9,
|
||||
Comment:,First preferences,,,,,,Exclusion of Ciaran Goulding,,Exclusion of Cathal Boland,,Exclusion of Mick Davis,,Exclusion of Nora Owen,,Surplus of Trevor Sargent,,Exclusion of Michael Kennedy,
|
||||
Clare Daly,5502,H,,,5552,H,5731,H,5797,H,6245,H,6591,H,6773,H,,
|
||||
Mick Davis,1350,H,,,1382,H,1424,H,1440,H,0,EX,0,EX,0,EX,0,EX
|
||||
Jim Glennon,5892,H,,,5945,H,6028,H,6152,H,6294,H,6511,H,6596,H,,
|
||||
Ciaran Goulding,914,H,,,1009,H,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
Michael Kennedy,5253,H,,,5309,H,5368,H,5422,H,5532,H,5732,H,5801,H,0,EX
|
||||
Nora Owen,4012,H,,,4030,H,4132,H,4720,H,4763,H,0,EX,0,EX,0,EX
|
||||
Eamon Quinn,285,H,,,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
Seán Ryan,6359,H,,,6407,H,6535,H,6665,H,6847,H,8578,H,9128,EL,9128,EL
|
||||
Trevor Sargent,7294,H,,,7380,H,7678,H,7818,H,8118,H,9785,EL,8789,EL,8789,EL
|
||||
David Walshe,247,H,,,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
G V Wright,5658,H,,,5707,H,5739,H,5777,H,5868,H,6139,H,6249,H,,
|
||||
Cathal Boland,1177,H,,,1189,H,1216,H,0,EX,0,EX,0,EX,0,EX,0,EX
|
||||
Non-transferable,0,,,,33,,92,,152,,276,,607,,607,,,
|
|
BIN
tests/data/DublinNorthSorted.ods
Normal file
BIN
tests/data/DublinNorthSorted.ods
Normal file
Binary file not shown.
38
tests/tests_impl/dail.rs
Normal file
38
tests/tests_impl/dail.rs
Normal file
@ -0,0 +1,38 @@
|
||||
/* OpenTally: Open-source election vote counting
|
||||
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::utils;
|
||||
|
||||
use opentally::numbers::Rational;
|
||||
use opentally::stv;
|
||||
|
||||
#[test]
|
||||
fn dublin_north_2002_rational() {
|
||||
let stv_opts = stv::STVOptionsBuilder::default()
|
||||
.round_quota(Some(0))
|
||||
.normalise_ballots(true)
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.surplus(stv::SurplusMethod::Hare)
|
||||
.surplus_order(stv::SurplusOrder::ByOrder)
|
||||
.transferable_only(true)
|
||||
.defer_surpluses(true)
|
||||
.build().unwrap();
|
||||
|
||||
assert_eq!(stv_opts.describe::<Rational>(), "--round-quota 0 --normalise-ballots --quota-criterion geq --surplus hare --surplus-order by_order --transferable-only --defer-surpluses");
|
||||
|
||||
utils::read_validate_election::<Rational>("tests/data/DublinNorthSorted.csv", "tests/data/DublinNorthSorted.blt", stv_opts, None, &["exhausted"]);
|
||||
}
|
@ -23,6 +23,7 @@ mod coe;
|
||||
mod constraints;
|
||||
mod convert;
|
||||
mod csm;
|
||||
mod dail;
|
||||
mod equal_ranks;
|
||||
mod ers;
|
||||
mod meek;
|
||||
|
Loading…
Reference in New Issue
Block a user