Fix bug excluding-by-value/source candidates with no votes Fix bug electing too many candidates if more reach the quota than vacancies remain Add regression test
798 lines
27 KiB
Rust
798 lines
27 KiB
Rust
/* 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 super::{ExclusionMethod, NextPreferencesEntry, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder};
|
|
use super::sample;
|
|
|
|
use crate::constraints;
|
|
use crate::election::{Candidate, CandidateState, CountState, Parcel, StageKind, Vote};
|
|
use crate::numbers::Number;
|
|
use crate::ties;
|
|
|
|
use std::cmp::max;
|
|
use std::collections::HashMap;
|
|
use std::ops;
|
|
|
|
/// Distribute first preference votes according to the Gregory method
|
|
pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
|
|
where
|
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>
|
|
{
|
|
let votes = state.election.ballots.iter().map(|b| Vote {
|
|
ballot: b,
|
|
up_to_pref: 0,
|
|
}).collect();
|
|
|
|
let result = super::next_preferences(state, votes);
|
|
|
|
// Transfer candidate votes
|
|
for (candidate, entry) in result.candidates.into_iter() {
|
|
let parcel = Parcel {
|
|
votes: entry.votes,
|
|
value_fraction: N::one(),
|
|
source_order: 0,
|
|
};
|
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
|
count_card.parcels.push(parcel);
|
|
|
|
let mut vote_transfers = entry.num_ballots.clone();
|
|
if let Some(dps) = opts.round_votes {
|
|
vote_transfers.floor_mut(dps);
|
|
}
|
|
count_card.transfer(&vote_transfers);
|
|
|
|
count_card.ballot_transfers += entry.num_ballots;
|
|
}
|
|
|
|
// Transfer exhausted votes
|
|
let parcel = Parcel {
|
|
votes: result.exhausted.votes,
|
|
value_fraction: N::one(),
|
|
source_order: 0,
|
|
};
|
|
state.exhausted.parcels.push(parcel);
|
|
state.exhausted.transfer(&result.exhausted.num_ballots);
|
|
state.exhausted.ballot_transfers += result.exhausted.num_ballots;
|
|
|
|
// Calculate loss by fraction - if minivoters used
|
|
if let Some(orig_total) = &state.election.total_votes {
|
|
let mut total_votes = state.candidates.values().fold(N::new(), |acc, cc| acc + &cc.votes);
|
|
total_votes += &state.exhausted.votes;
|
|
let lbf = orig_total - &total_votes;
|
|
|
|
state.loss_fraction.votes = lbf.clone();
|
|
state.loss_fraction.transfers = lbf;
|
|
}
|
|
|
|
state.title = StageKind::FirstPreferences;
|
|
state.logger.log_literal("First preferences distributed.".to_string());
|
|
}
|
|
|
|
/// Distribute the largest surplus according to the Gregory or random subset method, based on [STVOptions::surplus]
|
|
///
|
|
/// Returns `true` if any surpluses were distributed.
|
|
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
|
where
|
|
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
|
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>
|
|
{
|
|
let quota = state.quota.as_ref().unwrap();
|
|
let has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
|
|
.filter(|c| {
|
|
let cc = &state.candidates[c];
|
|
&cc.votes > quota && !cc.finalised
|
|
})
|
|
.collect();
|
|
|
|
if !has_surplus.is_empty() {
|
|
let total_surpluses = has_surplus.iter()
|
|
.fold(N::new(), |acc, c| acc + &state.candidates[c].votes - quota);
|
|
|
|
// Determine if surplues can be deferred
|
|
if opts.defer_surpluses {
|
|
if super::can_defer_surpluses(state, opts, &total_surpluses) {
|
|
state.logger.log_literal(format!("Distribution of surpluses totalling {:.dps$} votes will be deferred.", total_surpluses, dps=opts.pp_decimals));
|
|
return Ok(false);
|
|
}
|
|
}
|
|
|
|
// Distribute top candidate's surplus
|
|
let max_cands = match opts.surplus_order {
|
|
SurplusOrder::BySize => {
|
|
ties::multiple_max_by(&has_surplus, |c| &state.candidates[c].votes)
|
|
}
|
|
SurplusOrder::ByOrder => {
|
|
ties::multiple_min_by(&has_surplus, |c| state.candidates[c].order_elected)
|
|
}
|
|
};
|
|
let elected_candidate = if max_cands.len() > 1 {
|
|
super::choose_highest(state, opts, max_cands, "Which candidate's surplus to distribute?")?
|
|
} else {
|
|
max_cands[0]
|
|
};
|
|
|
|
// If --no-immediate-elect, declare elected the candidate with the highest surplus
|
|
if !opts.immediate_elect {
|
|
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
|
count_card.state = CandidateState::Elected;
|
|
state.num_elected += 1;
|
|
count_card.order_elected = state.num_elected as isize;
|
|
|
|
state.logger.log_smart(
|
|
"{} meets the quota and is elected.",
|
|
"{} meet the quota and are elected.",
|
|
vec![&elected_candidate.name]
|
|
);
|
|
|
|
constraints::update_constraints(state, opts);
|
|
}
|
|
|
|
match opts.surplus {
|
|
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { distribute_surplus(state, &opts, elected_candidate); }
|
|
SurplusMethod::Cincinnati | SurplusMethod::Hare => { sample::distribute_surplus(state, &opts, elected_candidate)?; }
|
|
_ => unreachable!()
|
|
}
|
|
|
|
return Ok(true);
|
|
}
|
|
|
|
// If --no-immediate-elect, check for candidates with exactly a quota to elect
|
|
// However, if --defer-surpluses, zero surplus is necessarily deferred so skip
|
|
if !opts.immediate_elect && !opts.defer_surpluses {
|
|
if super::elect_hopefuls(state, opts, false)? {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
return Ok(false);
|
|
}
|
|
|
|
/// Return the denominator of the surplus fraction
|
|
///
|
|
/// Returns `None` if the value of transferable votes <= surplus (i.e. all transferable votes are transferred at values received).
|
|
fn calculate_surplus_denom<'n, N: Number>(surplus: &N, transferable_ballots: &'n N, transferable_votes: &'n N, total_ballots: &'n N, total_votes: &'n N, weighted: bool, transferable_only: bool) -> Option<&'n N>
|
|
where
|
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>
|
|
{
|
|
if transferable_only {
|
|
let transferable_units = if weighted { transferable_votes } else { transferable_ballots };
|
|
|
|
if transferable_votes > surplus {
|
|
return Some(transferable_units);
|
|
} else {
|
|
return None;
|
|
}
|
|
} else {
|
|
if weighted {
|
|
return Some(total_votes);
|
|
} else {
|
|
return Some(total_ballots);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Return the reweighted value fraction of a parcel/vote after being transferred
|
|
fn reweight_value_fraction<N: Number>(
|
|
value_fraction: &N,
|
|
surplus: &N,
|
|
weighted: bool,
|
|
surplus_fraction: &Option<N>,
|
|
surplus_denom: &Option<&N>,
|
|
round_tvs: Option<usize>) -> N
|
|
{
|
|
let result;
|
|
|
|
match surplus_denom {
|
|
Some(v) => {
|
|
if let Some(_) = round_tvs {
|
|
// Rounding requested: use the rounded transfer value
|
|
if weighted {
|
|
result = value_fraction.clone() * surplus_fraction.as_ref().unwrap();
|
|
} else {
|
|
result = surplus_fraction.as_ref().unwrap().clone();
|
|
}
|
|
} else {
|
|
// Avoid unnecessary rounding error by first multiplying by the surplus
|
|
if weighted {
|
|
result = value_fraction.clone() * surplus / *v;
|
|
} else {
|
|
result = surplus.clone() / *v;
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
result = value_fraction.clone();
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Compute the number of votes to credit to a continuing candidate during a surplus transfer, based on [STVOptions::sum_surplus_transfers]
|
|
fn sum_surplus_transfers<N: Number>(entry: &NextPreferencesEntry<N>, orig_value_fraction: &N, surplus: &N, is_weighted: bool, surplus_fraction: &Option<N>, surplus_denom: &Option<&N>, _state: &mut CountState<N>, opts: &STVOptions) -> N
|
|
where
|
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
|
{
|
|
match opts.sum_surplus_transfers {
|
|
SumSurplusTransfersMode::ByValue => {
|
|
// Calculate transfer across all votes in this parcel
|
|
let mut result = N::new();
|
|
for vote in entry.votes.iter() {
|
|
result += &vote.ballot.orig_value;
|
|
}
|
|
result *= reweight_value_fraction(orig_value_fraction, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_surplus_fractions);
|
|
return result;
|
|
}
|
|
SumSurplusTransfersMode::PerBallot => {
|
|
// Sum transfer per each individual ballot
|
|
// TODO: This could be moved to distribute_surplus to avoid looping over the votes and calculating transfer values twice
|
|
let mut new_value_fraction = reweight_value_fraction(orig_value_fraction, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_surplus_fractions);
|
|
if let Some(dps) = opts.round_votes {
|
|
new_value_fraction.floor_mut(dps);
|
|
}
|
|
|
|
let mut result = N::new();
|
|
for vote in entry.votes.iter() {
|
|
let mut vote_value = &new_value_fraction * &vote.ballot.orig_value;
|
|
if let Some(dps) = opts.round_votes {
|
|
vote_value.floor_mut(dps);
|
|
}
|
|
result += vote_value;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus]
|
|
fn distribute_surplus<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, elected_candidate: &'a Candidate)
|
|
where
|
|
for<'r> &'r N: ops::Add<&'r N, Output=N>,
|
|
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>
|
|
{
|
|
state.title = StageKind::SurplusOf(&elected_candidate);
|
|
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
|
|
|
|
let count_card = &state.candidates[elected_candidate];
|
|
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
|
|
|
|
// Determine which votes to examine
|
|
|
|
let mut parcels;
|
|
match opts.surplus {
|
|
SurplusMethod::WIG | SurplusMethod::UIG => {
|
|
// Inclusive Gregory
|
|
parcels = Vec::new();
|
|
parcels.append(&mut state.candidates.get_mut(elected_candidate).unwrap().parcels);
|
|
}
|
|
SurplusMethod::EG => {
|
|
// Exclusive Gregory
|
|
// Should be safe to unwrap() - or else how did we get a quota!
|
|
parcels = vec![state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap()];
|
|
}
|
|
_ => unreachable!()
|
|
}
|
|
|
|
// Count votes
|
|
|
|
let mut parcels_next_prefs= Vec::new();
|
|
|
|
let mut transferable_ballots = N::new();
|
|
let mut transferable_votes = N::new();
|
|
|
|
let mut exhausted_ballots = N::new();
|
|
let mut exhausted_votes = N::new();
|
|
|
|
for parcel in parcels {
|
|
// Count next preferences
|
|
let result = super::next_preferences(state, parcel.votes);
|
|
|
|
for (_, entry) in result.candidates.iter() {
|
|
transferable_ballots += &entry.num_ballots;
|
|
transferable_votes += &entry.num_ballots * &parcel.value_fraction;
|
|
}
|
|
|
|
exhausted_ballots += &result.exhausted.num_ballots;
|
|
exhausted_votes += &result.exhausted.num_ballots * &parcel.value_fraction;
|
|
|
|
parcels_next_prefs.push((parcel.value_fraction, result));
|
|
}
|
|
|
|
// Calculate surplus fraction
|
|
|
|
let is_weighted = match opts.surplus {
|
|
SurplusMethod::WIG => { true }
|
|
SurplusMethod::UIG | SurplusMethod::EG => { false }
|
|
_ => unreachable!()
|
|
};
|
|
|
|
let total_ballots = &transferable_ballots + &exhausted_ballots;
|
|
let total_votes = &transferable_votes + &exhausted_votes;
|
|
|
|
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
|
count_card.ballot_transfers = -&total_ballots;
|
|
|
|
let surplus_denom = calculate_surplus_denom(&surplus, &transferable_ballots, &transferable_votes, &total_ballots, &total_votes, is_weighted, opts.transferable_only);
|
|
let mut surplus_fraction;
|
|
match surplus_denom {
|
|
Some(v) => {
|
|
surplus_fraction = Some(surplus.clone() / v);
|
|
|
|
// Round down if requested
|
|
if let Some(dps) = opts.round_surplus_fractions {
|
|
surplus_fraction.as_mut().unwrap().floor_mut(dps);
|
|
}
|
|
|
|
if opts.transferable_only {
|
|
if transferable_ballots == N::one() {
|
|
state.logger.log_literal(format!("Transferring 1 transferable ballot, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
|
} else {
|
|
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", transferable_ballots, transferable_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
|
}
|
|
} else {
|
|
if total_ballots == N::one() {
|
|
state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
|
} else {
|
|
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", total_ballots, total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
surplus_fraction = None;
|
|
|
|
// This can only happen if --transferable-only
|
|
if transferable_ballots == N::one() {
|
|
state.logger.log_literal(format!("Transferring 1 transferable ballot, totalling {:.dps$} transferable votes, at values received.", transferable_votes, dps=opts.pp_decimals));
|
|
} else {
|
|
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, at values received.", transferable_ballots, transferable_votes, dps=opts.pp_decimals));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reweight and transfer parcels
|
|
|
|
let mut candidate_transfers: HashMap<&Candidate, N> = HashMap::new();
|
|
for candidate in state.election.candidates.iter() {
|
|
candidate_transfers.insert(candidate, N::new());
|
|
}
|
|
let mut exhausted_transfers = N::new();
|
|
|
|
for (value_fraction, result) in parcels_next_prefs {
|
|
for (candidate, entry) in result.candidates.into_iter() {
|
|
// Record transfers
|
|
// TODO: Is there a better way of writing this?
|
|
let transfers_orig = candidate_transfers.remove(candidate).unwrap();
|
|
let transfers_add = sum_surplus_transfers(&entry, &value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts);
|
|
candidate_transfers.insert(candidate, transfers_orig + transfers_add);
|
|
|
|
// Transfer candidate votes
|
|
let parcel = Parcel {
|
|
votes: entry.votes,
|
|
value_fraction: reweight_value_fraction(&value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_surplus_fractions),
|
|
source_order: state.num_elected + state.num_excluded,
|
|
};
|
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
|
count_card.ballot_transfers += parcel.num_ballots();
|
|
count_card.parcels.push(parcel);
|
|
}
|
|
|
|
// Record exhausted votes
|
|
if opts.transferable_only {
|
|
if transferable_votes > surplus {
|
|
// No ballots exhaust
|
|
} else {
|
|
exhausted_transfers += &surplus - &transferable_votes;
|
|
}
|
|
} else {
|
|
exhausted_transfers += sum_surplus_transfers(&result.exhausted, &value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts);
|
|
}
|
|
|
|
// Transfer exhausted votes
|
|
let parcel = Parcel {
|
|
votes: result.exhausted.votes,
|
|
value_fraction: value_fraction, // TODO: Reweight exhausted votes
|
|
source_order: state.num_elected + state.num_excluded,
|
|
};
|
|
state.exhausted.ballot_transfers += parcel.num_ballots();
|
|
state.exhausted.parcels.push(parcel);
|
|
}
|
|
|
|
let mut checksum = N::new();
|
|
|
|
// Credit transferred votes
|
|
// ballot_transfers updated above
|
|
for (candidate, mut votes) in candidate_transfers {
|
|
if let Some(dps) = opts.round_votes {
|
|
votes.floor_mut(dps);
|
|
}
|
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
|
count_card.transfer(&votes);
|
|
checksum += votes;
|
|
}
|
|
|
|
// Credit exhausted votes
|
|
if let Some(dps) = opts.round_votes {
|
|
exhausted_transfers.floor_mut(dps);
|
|
}
|
|
state.exhausted.transfer(&exhausted_transfers);
|
|
checksum += exhausted_transfers;
|
|
|
|
// Finalise candidate votes
|
|
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
|
count_card.transfers = -&surplus;
|
|
count_card.votes.assign(state.quota.as_ref().unwrap());
|
|
checksum -= surplus;
|
|
|
|
count_card.finalised = true; // Mark surpluses as done
|
|
|
|
// Update loss by fraction
|
|
state.loss_fraction.transfer(&-checksum);
|
|
}
|
|
|
|
/// Perform one stage of a candidate exclusion according to the Gregory method, based on [STVOptions::exclusion]
|
|
pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
|
where
|
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
|
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 in this stage
|
|
let mut parcels = Vec::new();
|
|
let mut votes_remain;
|
|
let mut checksum = N::new();
|
|
|
|
match opts.exclusion {
|
|
ExclusionMethod::SingleStage => {
|
|
// Exclude in one round
|
|
for excluded_candidate in excluded_candidates.iter() {
|
|
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
|
count_card.ballot_transfers = -count_card.num_ballots();
|
|
count_card.finalised = true;
|
|
|
|
parcels.append(&mut count_card.parcels);
|
|
|
|
// Update votes
|
|
checksum -= &count_card.votes;
|
|
count_card.transfers = -count_card.votes.clone();
|
|
count_card.votes = N::new();
|
|
}
|
|
votes_remain = false;
|
|
}
|
|
ExclusionMethod::ByValue => {
|
|
// Exclude by value
|
|
let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter()
|
|
.filter(|c| { let cc = &state.candidates[*c]; !cc.finalised && !cc.parcels.is_empty() })
|
|
.collect();
|
|
|
|
if excluded_with_votes.is_empty() {
|
|
votes_remain = false;
|
|
} else {
|
|
// If candidates to exclude still having votes, select only those with the greatest value
|
|
let max_value = excluded_with_votes.iter()
|
|
.map(|c| state.candidates[*c].parcels.iter()
|
|
.map(|p| &p.value_fraction)
|
|
.max().unwrap())
|
|
.max().unwrap()
|
|
.clone();
|
|
|
|
votes_remain = false;
|
|
|
|
let mut votes = Vec::new();
|
|
|
|
for excluded_candidate in excluded_with_votes.iter() {
|
|
let count_card = state.candidates.get_mut(*excluded_candidate).unwrap();
|
|
let mut cc_parcels = Vec::new();
|
|
cc_parcels.append(&mut count_card.parcels);
|
|
|
|
// Filter out just those votes with max_value
|
|
let mut remaining_parcels = Vec::new();
|
|
|
|
for mut parcel in cc_parcels {
|
|
if parcel.value_fraction == max_value {
|
|
count_card.ballot_transfers -= parcel.num_ballots();
|
|
|
|
let votes_transferred = parcel.num_votes();
|
|
votes.append(&mut parcel.votes);
|
|
|
|
// Update votes
|
|
checksum -= &votes_transferred;
|
|
count_card.transfer(&-votes_transferred);
|
|
} else {
|
|
remaining_parcels.push(parcel);
|
|
}
|
|
}
|
|
|
|
if !remaining_parcels.is_empty() {
|
|
votes_remain = true;
|
|
}
|
|
|
|
// Leave remaining votes with candidate
|
|
count_card.parcels = remaining_parcels;
|
|
}
|
|
|
|
// Group all votes of one value in single parcel
|
|
parcels.push(Parcel {
|
|
votes: votes,
|
|
value_fraction: max_value,
|
|
source_order: 0, // source_order is unused in this mode
|
|
});
|
|
}
|
|
}
|
|
ExclusionMethod::BySource => {
|
|
// Exclude by source candidate
|
|
let excluded_with_votes: Vec<&&Candidate> = excluded_candidates.iter()
|
|
.filter(|c| { let cc = &state.candidates[*c]; !cc.finalised && !cc.parcels.is_empty() })
|
|
.collect();
|
|
|
|
if excluded_with_votes.is_empty() {
|
|
votes_remain = false;
|
|
} else {
|
|
// If candidates to exclude still having votes, select only those from the earliest elected/excluded source candidate
|
|
let min_order = excluded_with_votes.iter()
|
|
.map(|c| state.candidates[*c].parcels.iter()
|
|
.map(|p| p.source_order)
|
|
.min().unwrap())
|
|
.min().unwrap();
|
|
|
|
votes_remain = false;
|
|
|
|
for excluded_candidate in excluded_with_votes.iter() {
|
|
let count_card = state.candidates.get_mut(*excluded_candidate).unwrap();
|
|
let mut cc_parcels = Vec::new();
|
|
cc_parcels.append(&mut count_card.parcels);
|
|
|
|
// Filter out just those votes with min_order
|
|
let mut remaining_parcels = Vec::new();
|
|
|
|
for parcel in cc_parcels {
|
|
if parcel.source_order == min_order {
|
|
count_card.ballot_transfers -= parcel.num_ballots();
|
|
|
|
let votes_transferred = parcel.num_votes();
|
|
parcels.push(parcel);
|
|
|
|
// Update votes
|
|
checksum -= &votes_transferred;
|
|
count_card.transfer(&-votes_transferred);
|
|
} else {
|
|
remaining_parcels.push(parcel);
|
|
}
|
|
}
|
|
|
|
if !remaining_parcels.is_empty() {
|
|
votes_remain = true;
|
|
}
|
|
|
|
// Leave remaining votes with candidate
|
|
count_card.parcels = remaining_parcels;
|
|
}
|
|
}
|
|
}
|
|
ExclusionMethod::ParcelsByOrder => {
|
|
// Exclude by parcel by order
|
|
if excluded_candidates.len() > 1 && excluded_candidates.iter().any(|c| !state.candidates[c].parcels.is_empty()) {
|
|
// TODO: We can probably support this actually
|
|
panic!("--exclusion parcels_by_order is incompatible with multiple exclusions");
|
|
}
|
|
|
|
let count_card = state.candidates.get_mut(excluded_candidates[0]).unwrap();
|
|
|
|
if count_card.parcels.is_empty() {
|
|
votes_remain = false;
|
|
} else {
|
|
parcels.push(count_card.parcels.remove(0));
|
|
votes_remain = !count_card.parcels.is_empty();
|
|
|
|
count_card.ballot_transfers -= parcels.first().unwrap().num_ballots();
|
|
|
|
// Update votes
|
|
let votes_transferred = parcels.first().unwrap().num_votes();
|
|
checksum -= &votes_transferred;
|
|
count_card.transfer(&-votes_transferred);
|
|
}
|
|
}
|
|
_ => panic!()
|
|
}
|
|
|
|
let mut total_ballots = N::new();
|
|
let mut total_votes = N::new();
|
|
|
|
let value = match parcels.first() { Some(p) => Some(p.value_fraction.clone()), _ => None };
|
|
|
|
let mut candidate_transfers: HashMap<&Candidate, N> = HashMap::new();
|
|
for candidate in state.election.candidates.iter() {
|
|
candidate_transfers.insert(candidate, N::new());
|
|
}
|
|
let mut exhausted_transfers = N::new();
|
|
|
|
for parcel in parcels {
|
|
// Count next preferences
|
|
let result = super::next_preferences(state, parcel.votes);
|
|
|
|
total_ballots += &result.total_ballots;
|
|
total_votes += &result.total_ballots * &parcel.value_fraction;
|
|
|
|
// Transfer candidate votes
|
|
for (candidate, entry) in result.candidates.into_iter() {
|
|
let parcel = Parcel {
|
|
votes: entry.votes,
|
|
value_fraction: parcel.value_fraction.clone(),
|
|
source_order: state.num_elected + state.num_excluded,
|
|
};
|
|
|
|
// Record transfers
|
|
let transfers_orig = candidate_transfers.remove(candidate).unwrap();
|
|
candidate_transfers.insert(candidate, transfers_orig + &entry.num_ballots * &parcel.value_fraction);
|
|
|
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
|
count_card.ballot_transfers += parcel.num_ballots();
|
|
count_card.parcels.push(parcel);
|
|
}
|
|
|
|
// Transfer exhausted votes
|
|
let parcel = Parcel {
|
|
votes: result.exhausted.votes,
|
|
value_fraction: parcel.value_fraction,
|
|
source_order: state.num_elected + state.num_excluded,
|
|
};
|
|
|
|
// Record transfers
|
|
state.exhausted.ballot_transfers += parcel.num_ballots();
|
|
exhausted_transfers += &result.exhausted.num_ballots * &parcel.value_fraction;
|
|
state.exhausted.parcels.push(parcel);
|
|
|
|
// TODO: Detailed transfers logs
|
|
}
|
|
|
|
if let ExclusionMethod::SingleStage = opts.exclusion {
|
|
if total_ballots == N::one() {
|
|
state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes.", total_votes, dps=opts.pp_decimals));
|
|
} else {
|
|
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes.", total_ballots, total_votes, dps=opts.pp_decimals));
|
|
}
|
|
} else {
|
|
if total_ballots.is_zero() {
|
|
state.logger.log_literal(format!("Transferring 0 ballots, totalling {:.dps$} votes.", 0, dps=opts.pp_decimals));
|
|
} else if total_ballots == N::one() {
|
|
state.logger.log_literal(format!("Transferring 1 ballot, totalling {:.dps$} votes, received at value {:.dps2$}.", total_votes, value.unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
|
} else {
|
|
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, received at value {:.dps2$}.", total_ballots, total_votes, value.unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
|
}
|
|
}
|
|
|
|
// Credit transferred votes
|
|
// ballot_transfers updated above
|
|
for (candidate, mut votes) in candidate_transfers {
|
|
if let Some(dps) = opts.round_votes {
|
|
votes.floor_mut(dps);
|
|
}
|
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
|
count_card.transfer(&votes);
|
|
checksum += votes;
|
|
}
|
|
|
|
// Credit exhausted votes
|
|
if let Some(dps) = opts.round_votes {
|
|
exhausted_transfers.floor_mut(dps);
|
|
}
|
|
state.exhausted.transfer(&exhausted_transfers);
|
|
checksum += exhausted_transfers;
|
|
|
|
if !votes_remain {
|
|
// Finalise candidate votes
|
|
for excluded_candidate in excluded_candidates.into_iter() {
|
|
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
|
checksum -= &count_card.votes;
|
|
count_card.transfers -= &count_card.votes;
|
|
count_card.votes = N::new();
|
|
count_card.finalised = true;
|
|
}
|
|
|
|
if opts.exclusion != ExclusionMethod::SingleStage {
|
|
state.logger.log_literal("Exclusion complete.".to_string());
|
|
}
|
|
}
|
|
|
|
// Update loss by fraction
|
|
state.loss_fraction.transfer(&-checksum);
|
|
}
|
|
|
|
/// Perform one stage of a candidate exclusion according to the Wright method
|
|
pub fn wright_exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
|
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>,
|
|
{
|
|
// 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);
|
|
}
|
|
|
|
// Reset count
|
|
for (_, count_card) in state.candidates.iter_mut() {
|
|
if count_card.order_elected > 0 {
|
|
count_card.order_elected = 0;
|
|
}
|
|
count_card.parcels.clear();
|
|
count_card.votes = N::new();
|
|
count_card.transfers = N::new();
|
|
count_card.state = match count_card.state {
|
|
CandidateState::Withdrawn => CandidateState::Withdrawn,
|
|
CandidateState::Excluded => CandidateState::Excluded,
|
|
_ => CandidateState::Hopeful,
|
|
};
|
|
|
|
if count_card.state == CandidateState::Excluded {
|
|
count_card.finalised = true;
|
|
} else {
|
|
count_card.finalised = false;
|
|
}
|
|
}
|
|
|
|
state.exhausted.votes = N::new();
|
|
state.exhausted.transfers = N::new();
|
|
state.loss_fraction.votes = N::new();
|
|
state.loss_fraction.transfers = N::new();
|
|
|
|
state.num_elected = 0;
|
|
|
|
let orig_title = state.title.clone();
|
|
|
|
// Redistribute first preferences
|
|
super::distribute_first_preferences(state, opts);
|
|
|
|
state.title = orig_title;
|
|
|
|
// Trigger recalculation of quota within stv::count_one_stage
|
|
state.quota = None;
|
|
state.vote_required_election = None;
|
|
}
|