Implement Cambridge STV - Cincinnati/Hare methods of surpluses

This commit is contained in:
RunasSudo 2021-08-03 18:38:45 +10:00
parent 6da51837a5
commit f182ca02bd
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
13 changed files with 443 additions and 42 deletions

View File

@ -79,15 +79,22 @@ Some STV counting rules provide, for example, that no surplus shall be transf
This dropdown allows you to select how ballots are transferred during surplus transfers. The recommended methods are:
* *Weighted inclusive Gregory* (default): During surplus transfers, all applicable ballot papers of the transferring candidate are examined. Transfers are weighted according to the weights of the ballot papers.
* *Weighted inclusive Gregory* (default): During surplus transfers, all applicable ballot papers of the elected candidate are examined. Transfers are weighted according to the values of the ballot papers.
* *Meek method*: Transfers are computed as described at <http://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf>.
Other methods are supported, but not recommended:
Other Gregory methods are supported, but not recommended:
* *Unweighted inclusive Gregory*: During surplus transfers, all applicable ballot papers of the transferring candidate are examined. Transfers are not weighted, and each ballot paper has equal value in the calculation.
* *Exclusive Gregory (last bundle)*: During surplus transfers, only the ballot papers received in the last transfer are examined. Transfers are not weighted.
* *Unweighted inclusive Gregory*: During surplus transfers, all applicable ballot papers of the elected candidate are examined. Transfers are not weighted, and each ballot paper has equal value in the calculation.
* *Exclusive Gregory (last bundle)*: During surplus transfers, only the ballot papers received in the last transfer (all of one value) are examined.
Other surplus transfer methods, such as non-fractional transfers (e.g. random sample) are not supported at this time.
Random subset methods are also supported, but also not recommended:
* *Cincinnati (inclusive subset)*: During surplus transfers, a subset of the elected candidate's ballot papers, equal in size to the surplus, is examined.
* *Hare (exclusive subset)*: During surplus transfers, a subset of the ballot papers received in the last transfer, equal in size to the surplus, is examined.
The use of a random subset method requires *Normalise ballots* to be enabled, and requires the *Quota criterion* to be set to *>=*. When a random subset method is used, surplus transfers and exclusions are performed one ballot paper at a time, and candidates are declared elected immediately on reaching the quota. Consequential surpluses therefore do not arise, and surpluses only occur during the count of first preferences.
In both random subset methods, 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*). This depends on the order of ballot papers in the BLT file, and is independent of the *Random seed* option.
### Papers to examine in surplus transfer (--transferable-only)
@ -223,13 +230,13 @@ When *Surplus method* is set to *Meek method*:
### (Gregory) Sum surplus transfers (--sum-surplus-transfers)
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 is calculated. In each case, votes are grouped according to the next available preference for a continuing candidate. Subsequently:
* *Single step*: The total value of all votes expressing a next available preference for that candidate 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 is credited to that candidate. This is distinct to *Single step* only for weighted inclusive Gregory.
* *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.
This option affects the result only insofar as rounding (due to use of fixed-precision arithmetic, or due to 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.
### (Meek) Surplus tolerance (--meek-surplus-tolerance)

View File

@ -45,6 +45,7 @@
<option value="act">Australian Capital Territory STV</option>
<option value="wa">Western Australia STV</option>
<option value="meeknz">Meek STV (New Zealand)</option>
<option value="cambridge">Cambridge STV</option>
</optgroup>
<optgroup label="Hand-count">
<option value="prsa77">PRSA 1977</option>
@ -110,6 +111,8 @@
<option value="uig">Unweighted inclusive Gregory</option>
<option value="eg">Exclusive Gregory (last bundle)</option>
<option value="meek">Meek method</option>
<option value="cincinnati">Cincinnati (inclusive subset)</option>
<option value="hare">Hare (exclusive subset)</option>
</select>
</label>
<label>

View File

@ -547,6 +547,23 @@ function changePreset() {
document.getElementById('selPapers').value = 'transferable';
document.getElementById('selExclusion').value = 'by_value';
document.getElementById('selTies').value = 'backwards,random';
} else if (document.getElementById('selPreset').value === 'cambridge') {
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; // TODO: Cambridge-style bulk exclusion
document.getElementById('chkDeferSurpluses').checked = false;
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 = 'single_step';
document.getElementById('selTransfers').value = 'cincinnati';
document.getElementById('selPapers').value = 'transferable';
document.getElementById('selExclusion').value = 'single_stage';
document.getElementById('selTies').value = 'backwards,random';
} else if (document.getElementById('selPreset').value === 'wright') {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';

View File

@ -119,7 +119,7 @@ struct STV {
random_seed: Option<String>,
/// Method of surplus distributions
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek", "cincinnati", "hare"], default_value="wig", value_name="method")]
surplus: String,
/// (Gregory STV) Order to distribute surpluses

View File

@ -80,6 +80,19 @@ impl Number for Fixed {
}
}
fn round_mut(&mut self, dps: usize) {
// Only do something if truncating
if dps < get_dps() {
// TODO: Streamline
let mut factor = Self::from(10);
factor.pow_assign(-(dps as i32));
factor /= Self::from(2);
*self = self.clone() - factor;
self.ceil_mut(dps);
}
}
fn parse(s: &str) -> Self {
// Parse decimal
if s.contains('.') {

View File

@ -87,6 +87,19 @@ impl Number for GuardedFixed {
}
}
fn round_mut(&mut self, dps: usize) {
// Only do something if truncating
if dps < get_dps() * 2 {
// TODO: Streamline
let mut factor = Self::from(10);
factor.pow_assign(-(dps as i32));
factor /= Self::from(2);
*self = self.clone() - factor;
self.ceil_mut(dps);
}
}
fn parse(s: &str) -> Self {
// Parse decimal
if s.contains('.') {

View File

@ -47,7 +47,7 @@ pub trait Assign<Src=Self> {
pub trait Number:
NumRef + NumAssignRef + ops::Neg<Output=Self> + Ord + Assign + From<usize> + Clone + fmt::Debug + fmt::Display
where
for<'a> Self: Assign<&'a Self>
for<'a> Self: Assign<&'a Self>,
{
/// Return a new [Number]
fn new() -> Self;
@ -63,6 +63,8 @@ where
fn floor_mut(&mut self, dps: usize);
/// Round `self` up if necessary to `dps` decimal places
fn ceil_mut(&mut self, dps: usize);
/// Round `self` half-up to the nearest `dps` decimal places
fn round_mut(&mut self, dps: usize);
/// Parse the given string into a [Number]
fn parse(s: &str) -> Self {

View File

@ -47,6 +47,11 @@ impl Number for NativeFloat64 {
let factor = 10.0_f64.powi(dps as i32);
self.0 = (self.0 * factor).ceil() / factor;
}
fn round_mut(&mut self, dps: usize) {
let factor = 10.0_f64.powi(dps as i32);
self.0 = (self.0 * factor).round() / factor;
}
}
impl Num for NativeFloat64 {

View File

@ -62,6 +62,20 @@ impl Number for Rational {
}
}
fn round_mut(&mut self, dps: usize) {
if dps == 0 {
self.0 = self.0.round();
} else {
// TODO: Streamline
let mut factor = Self::from(10);
factor.pow_assign(-(dps as i32));
factor /= Self::from(2);
*self = self.clone() - factor;
self.ceil_mut(dps);
}
}
fn parse(s: &str) -> Self {
// Parse decimal
if s.contains('.') {

View File

@ -61,6 +61,20 @@ impl Number for Rational {
}
}
fn round_mut(&mut self, dps: usize) {
if dps == 0 {
self.0.round_mut();
} else {
// TODO: Streamline
let mut factor = Self::from(10);
factor.pow_assign(-(dps as i32));
factor /= Self::from(2);
*self = self.clone() - factor;
self.ceil_mut(dps);
}
}
fn parse(s: &str) -> Self {
// Parse decimal
if s.contains('.') {

View File

@ -16,6 +16,7 @@
*/
use super::{ExclusionMethod, NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder};
use super::subset;
use crate::constraints;
use crate::election::{Candidate, CandidateState, CountState, Parcel, Vote};
@ -61,7 +62,7 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
state.logger.log_literal("First preferences distributed.".to_string());
}
/// Distribute the largest surplus according to the Gregory method, based on [STVOptions::surplus]
/// Distribute the largest surplus according to the Gregory or random subset method, based on [STVOptions::surplus]
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
@ -103,7 +104,11 @@ where
max_cands[0]
};
distribute_surplus(state, &opts, elected_candidate);
match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { distribute_surplus(state, &opts, elected_candidate); }
SurplusMethod::Cincinnati | SurplusMethod::Hare => { subset::distribute_surplus(state, &opts, elected_candidate)?; }
_ => unreachable!()
}
return Ok(true);
}
@ -229,6 +234,8 @@ where
for<'r> &'r N: ops::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N>
{
state.kind = Some("Surplus of");
state.title = String::from(&elected_candidate.name);
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
let count_card = &state.candidates[elected_candidate];
@ -245,21 +252,18 @@ where
// Should be safe to unwrap() - or else how did we get a quota!
votes = state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap().votes;
}
_ => { panic!("Invalid --surplus for Gregory method"); }
_ => unreachable!()
}
// Count next preferences
let result = super::next_preferences(state, votes);
state.kind = Some("Surplus of");
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!() }
_ => unreachable!()
};
let transferable_votes = &result.total_votes - &result.exhausted.num_votes;
@ -524,9 +528,9 @@ where
}
if !votes.is_empty() {
// Count next preferences
let value = &votes[0].value / &votes[0].ballot.orig_value;
// Count next preferences
let result = super::next_preferences(state, votes);
if let ExclusionMethod::SingleStage = opts.exclusion {

View File

@ -17,10 +17,12 @@
#![allow(mutable_borrow_reservation_conflict)]
/// Gregory method of surplus distributions
/// Gregory methods of surplus distributions
pub mod gregory;
/// Meek method of surplus distributions, etc.
pub mod meek;
/// Random subset methods of surplus distributions
pub mod subset;
/// WebAssembly wrappers
//#[cfg(target_arch = "wasm32")]
@ -166,6 +168,8 @@ impl STVOptions {
"uig" => SurplusMethod::UIG,
"eg" => SurplusMethod::EG,
"meek" => SurplusMethod::Meek,
"cincinnati" => SurplusMethod::Cincinnati,
"hare" => SurplusMethod::Hare,
_ => panic!("Invalid --surplus"),
},
surplus_order: match surplus_order {
@ -206,9 +210,11 @@ impl STVOptions {
pub fn describe<N: Number>(&self) -> String {
let mut flags = Vec::new();
let n_str = N::describe_opt(); if !n_str.is_empty() { flags.push(N::describe_opt()) };
if let Some(dps) = self.round_surplus_fractions { flags.push(format!("--round-tvs {}", dps)); }
if let Some(dps) = self.round_values { flags.push(format!("--round-weights {}", dps)); }
if let Some(dps) = self.round_votes { flags.push(format!("--round-votes {}", dps)); }
if self.surplus != SurplusMethod::Cincinnati && self.surplus != SurplusMethod::Hare {
if let Some(dps) = self.round_surplus_fractions { flags.push(format!("--round-tvs {}", dps)); }
if let Some(dps) = self.round_values { flags.push(format!("--round-weights {}", 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 self.surplus != SurplusMethod::Meek && self.sum_surplus_transfers != SumSurplusTransfersMode::SingleStep { flags.push(self.sum_surplus_transfers.describe()); }
if self.surplus == SurplusMethod::Meek && self.meek_surplus_tolerance != "0.001%" { flags.push(format!("--meek-surplus-tolerance {}", self.meek_surplus_tolerance)); }
@ -244,6 +250,11 @@ impl STVOptions {
if self.transferable_only { return Err(STVError::InvalidOptions("--surplus meek is incompatible with --transferable-only")); }
if self.exclusion != ExclusionMethod::SingleStage { return Err(STVError::InvalidOptions("--surplus meek requires --exclusion single_stage")); }
}
if self.surplus == SurplusMethod::Cincinnati || self.surplus == SurplusMethod::Hare {
if self.exclusion != ExclusionMethod::SingleStage { return Err(STVError::InvalidOptions("--surplus cincinnati and --surplus hare require --exclusion single_stage")); }
if self.quota_criterion != QuotaCriterion::GreaterOrEqual { return Err(STVError::InvalidOptions("--surplus cincinnati and --surplus hare require --quota-criterion geq")); }
if !self.normalise_ballots { return Err(STVError::InvalidOptions("--surplus cincinnati and --surplus hare require --normalise-ballots")); }
}
return Ok(());
}
}
@ -357,6 +368,10 @@ pub enum SurplusMethod {
EG,
/// Meek method
Meek,
/// Cincinnati method (inclusive random subset)
Cincinnati,
/// Hare method (exclusive random subset)
Hare,
}
impl SurplusMethod {
@ -367,6 +382,8 @@ impl SurplusMethod {
SurplusMethod::UIG => "--surplus uig",
SurplusMethod::EG => "--surplus eg",
SurplusMethod::Meek => "--surplus meek",
SurplusMethod::Cincinnati => "--surplus cincinnati",
SurplusMethod::Hare => "--surplus hare",
}.to_string()
}
}
@ -544,14 +561,11 @@ where
}
// Exclude lowest hopeful
if exclude_hopefuls(state, &opts)? {
calculate_quota(state, opts);
elect_hopefuls(state, opts)?;
update_tiebreaks(state, opts);
return Ok(false);
}
panic!("Count incomplete but unable to proceed");
exclude_hopefuls(state, &opts)?; // Cannot fail
calculate_quota(state, opts);
elect_hopefuls(state, opts)?;
update_tiebreaks(state, opts);
return Ok(false);
}
/// See [next_preferences]
@ -632,7 +646,7 @@ where
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
{
match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG | SurplusMethod::Cincinnati | SurplusMethod::Hare => {
gregory::distribute_first_preferences(state);
}
SurplusMethod::Meek => {
@ -981,7 +995,7 @@ where
for<'r> &'r N: ops::Neg<Output=N>,
{
match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG | SurplusMethod::Cincinnati | SurplusMethod::Hare => {
return gregory::distribute_surpluses(state, opts);
}
SurplusMethod::Meek => {
@ -1007,7 +1021,7 @@ fn can_bulk_elect<N: Number>(state: &CountState<N>, num_to_exclude: usize) -> bo
}
/// Declare all continuing candidates to be elected
fn do_bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions, template1: &'static str, template2: &'static str) -> Result<bool, STVError> {
fn do_bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions, template1: &'static str, template2: &'static str) -> Result<(), STVError> {
let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter()
.filter(|c| {
let cc = &state.candidates[c];
@ -1048,7 +1062,7 @@ fn do_bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions, templa
}
}
return Ok(true);
return Ok(());
}
/// Declare all continuing candidates elected, if the number equals the number of remaining vacancies
@ -1057,7 +1071,8 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result
state.kind = None;
state.title = "Bulk election".to_string();
return do_bulk_elect(state, opts, "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.");
do_bulk_elect(state, opts, "{} is elected to fill the remaining vacancy.", "{} are elected to fill the remaining vacancies.")?;
return Ok(true);
}
return Ok(false);
}
@ -1108,11 +1123,13 @@ where
count_card.order_elected = -(order_excluded as isize);
}
return do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.");
do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.")?;
return Ok(true);
}
}
return exclude_candidates(state, opts, excluded_candidates);
exclude_candidates(state, opts, excluded_candidates)?;
return Ok(true);
}
return Ok(false);
@ -1167,7 +1184,7 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST
}
/// Exclude the lowest-ranked hopeful candidate(s)
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<(), STVError>
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
@ -1216,11 +1233,13 @@ where
count_card.order_elected = -(order_excluded as isize);
}
return do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.");
do_bulk_elect(state, opts, "As a result of the proposed exclusion, {} is elected to fill the remaining vacancy.", "As a result of the proposed exclusion, {} are elected to fill the remaining vacancies.")?;
return Ok(());
}
}
return exclude_candidates(state, opts, excluded_candidates);
exclude_candidates(state, opts, excluded_candidates)?;
return Ok(());
}
/// Continue the exclusion of a candidate who is being excluded
@ -1253,14 +1272,15 @@ where
names
);
return exclude_candidates(state, opts, excluded_candidates);
exclude_candidates(state, opts, excluded_candidates)?;
return Ok(true);
}
return Ok(false);
}
/// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion]
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) -> Result<bool, STVError>
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'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>,
@ -1275,6 +1295,9 @@ where
SurplusMethod::Meek => {
meek::exclude_candidates(state, opts, excluded_candidates);
}
SurplusMethod::Cincinnati | SurplusMethod::Hare => {
subset::exclude_candidates(state, opts, excluded_candidates)?;
}
}
}
ExclusionMethod::ByValue | ExclusionMethod::BySource | ExclusionMethod::ParcelsByOrder => {
@ -1286,7 +1309,7 @@ where
}
}
return Ok(true);
return Ok(());
}
/// Determine if the count is complete because the number of elected candidates equals the number of vacancies

286
src/stv/subset.rs Normal file
View File

@ -0,0 +1,286 @@
/* 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::constraints;
use crate::election::{Candidate, CandidateState, CountState, Parcel, Vote};
use crate::numbers::Number;
use crate::stv::{STVOptions, SurplusMethod};
use std::collections::HashMap;
use std::ops;
use super::STVError;
/// Distribute the surplus of a given candidate according to the random subset method, based on [STVOptions::surplus]
pub fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate) -> Result<(), STVError>
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N>
{
state.kind = Some("Surplus of");
state.title = String::from(&elected_candidate.name);
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
let votes;
match opts.surplus {
SurplusMethod::Cincinnati => {
// Inclusive
votes = count_card.concat_parcels();
}
SurplusMethod::Hare => {
// Exclusive
// Should be safe to unwrap() - or else how did we get a quota!
votes = count_card.parcels.pop().unwrap().votes;
}
_ => unreachable!()
}
// Calculate skip value
let total_ballots = votes.len();
let mut skip_fraction = N::from(total_ballots) / &surplus;
skip_fraction.round_mut(0);
state.logger.log_literal(format!("Examining {:.0} ballots, with skip value {:.0}.", total_ballots, skip_fraction));
// Number the votes
let mut numbered_votes: HashMap<usize, Vote<N>> = HashMap::new();
for (i, vote) in votes.into_iter().enumerate() {
numbered_votes.insert(i, vote);
}
// Transfer candidate votes
let skip_value: usize = format!("{:.0}", skip_fraction).parse().expect("Skip value overflows usize");
let mut iteration = 0;
let mut index = skip_value - 1; // Subtract 1 as votes are 0-indexed
while &state.candidates[elected_candidate].votes > state.quota.as_ref().unwrap() {
let mut vote = numbered_votes.remove(&index).unwrap();
// Transfer to next preference
let mut next_candidate = None;
for (i, preference) in vote.ballot.preferences.iter().enumerate().skip(vote.up_to_pref) {
let candidate = &state.election.candidates[*preference];
let count_card = &state.candidates[candidate];
if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state {
next_candidate = Some(candidate);
vote.up_to_pref = i + 1;
break;
}
}
// Have to structure like this to satisfy Rust's borrow checker
if let Some(candidate) = next_candidate {
// Available preference
state.candidates.get_mut(elected_candidate).unwrap().transfer(&-vote.value.clone());
let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.transfer(&vote.value);
match count_card.parcels.last_mut() {
Some(parcel) => {
if parcel.source_order == state.num_elected + state.num_excluded {
parcel.votes.push(vote);
} else {
let parcel = Parcel {
votes: vec![vote],
source_order: state.num_elected + state.num_excluded,
};
count_card.parcels.push(parcel);
}
}
None => {
let parcel = Parcel {
votes: vec![vote],
source_order: state.num_elected + state.num_excluded,
};
count_card.parcels.push(parcel);
}
}
super::elect_hopefuls(state, opts)?;
} else {
// Exhausted
if opts.transferable_only {
// Another ballot paper required
} else {
state.candidates.get_mut(elected_candidate).unwrap().transfer(&-vote.value.clone());
state.exhausted.transfer(&vote.value);
match state.exhausted.parcels.last_mut() {
Some(parcel) => {
if parcel.source_order == state.num_elected + state.num_excluded {
parcel.votes.push(vote);
} else {
let parcel = Parcel {
votes: vec![vote],
source_order: state.num_elected + state.num_excluded,
};
state.exhausted.parcels.push(parcel);
}
}
None => {
let parcel = Parcel {
votes: vec![vote],
source_order: state.num_elected + state.num_excluded,
};
state.exhausted.parcels.push(parcel);
}
}
}
}
index += skip_value;
if index >= total_ballots {
iteration += 1;
index = iteration + skip_value - 1;
if iteration >= skip_value {
// We have run out of ballot papers
// Remaining ballot papers exhaust
let surplus = &state.candidates[elected_candidate].votes - state.quota.as_ref().unwrap();
state.exhausted.transfer(&surplus);
state.candidates.get_mut(elected_candidate).unwrap().transfer(&-surplus);
break;
}
}
}
return Ok(());
}
/// Perform one stage of a candidate exclusion according to the random subset method
pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) -> Result<(), STVError>
where
for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
// Used to give bulk excluded candidate the same order_elected
let order_excluded = state.num_excluded + 1;
for excluded_candidate in excluded_candidates.iter() {
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
if count_card.state != CandidateState::Excluded {
count_card.state = CandidateState::Excluded;
state.num_excluded += 1;
count_card.order_elected = -(order_excluded as isize);
constraints::update_constraints(state, opts);
}
}
// Determine votes to transfer
let mut votes = Vec::new();
for excluded_candidate in excluded_candidates.iter() {
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
votes.append(&mut count_card.concat_parcels());
count_card.parcels.clear();
}
let total_ballots = votes.len();
if !votes.is_empty() {
if total_ballots == 1 {
state.logger.log_literal("Transferring 1 ballot.".to_string());
} else {
state.logger.log_literal(format!("Transferring {:.0} ballots.", total_ballots));
}
// Transfer vote by vote
for mut vote in votes {
// Subtract votes from excluded candidate
let count_card = state.candidates.get_mut(&state.election.candidates[vote.ballot.preferences[vote.up_to_pref - 1]]).unwrap();
count_card.transfer(&-vote.value.clone());
// Transfer to next preference
let mut next_candidate = None;
for (i, preference) in vote.ballot.preferences.iter().enumerate().skip(vote.up_to_pref) {
let candidate = &state.election.candidates[*preference];
let count_card = &state.candidates[candidate];
if let CandidateState::Hopeful | CandidateState::Guarded = count_card.state {
next_candidate = Some(candidate);
vote.up_to_pref = i + 1;
break;
}
}
// Have to structure like this to satisfy Rust's borrow checker
if let Some(candidate) = next_candidate {
// Available preference
let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.transfer(&vote.value);
match count_card.parcels.last_mut() {
Some(parcel) => {
if parcel.source_order == state.num_elected + state.num_excluded {
parcel.votes.push(vote);
} else {
let parcel = Parcel {
votes: vec![vote],
source_order: state.num_elected + state.num_excluded,
};
count_card.parcels.push(parcel);
}
}
None => {
let parcel = Parcel {
votes: vec![vote],
source_order: state.num_elected + state.num_excluded,
};
count_card.parcels.push(parcel);
}
}
super::elect_hopefuls(state, opts)?;
} else {
// Exhausted
state.exhausted.transfer(&vote.value);
match state.exhausted.parcels.last_mut() {
Some(parcel) => {
if parcel.source_order == state.num_elected + state.num_excluded {
parcel.votes.push(vote);
} else {
let parcel = Parcel {
votes: vec![vote],
source_order: state.num_elected + state.num_excluded,
};
state.exhausted.parcels.push(parcel);
}
}
None => {
let parcel = Parcel {
votes: vec![vote],
source_order: state.num_elected + state.num_excluded,
};
state.exhausted.parcels.push(parcel);
}
}
}
}
}
return Ok(());
}