749 lines
25 KiB
Rust
749 lines
25 KiB
Rust
/* OpenTally: Open-source election vote counting
|
|
* Copyright © 2021–2022 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/>.
|
|
*/
|
|
|
|
// --------------
|
|
// Child packages
|
|
|
|
/// Transfer tables
|
|
mod transfers;
|
|
pub use transfers::{TransferTable, TransferTableCell, TransferTableColumn};
|
|
|
|
/// prettytable-compatible API for HTML table output in WebAssembly
|
|
pub mod prettytable_html;
|
|
|
|
// --------
|
|
// STV code
|
|
|
|
use super::{ExclusionMethod, RoundSubtransfersMode, STVError, STVOptions, 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::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.total_vote();
|
|
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 = state.total_surplus();
|
|
|
|
// 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.as_str()]
|
|
);
|
|
|
|
constraints::update_constraints(state, opts);
|
|
}
|
|
|
|
match opts.surplus {
|
|
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { distribute_surplus(state, opts, elected_candidate); }
|
|
SurplusMethod::IHare | 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, opts: &STVOptions) -> Option<N>
|
|
where
|
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>
|
|
{
|
|
if opts.transferable_only {
|
|
let transferable_units = if opts.surplus.is_weighted() { transferable_votes } else { transferable_ballots };
|
|
|
|
if transferable_votes > surplus {
|
|
return Some(transferable_units.clone());
|
|
} else {
|
|
return None;
|
|
}
|
|
} else {
|
|
if opts.surplus.is_weighted() {
|
|
return Some(total_votes.clone());
|
|
} else {
|
|
return Some(total_ballots.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus]
|
|
pub 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, parcel.source_order, result));
|
|
}
|
|
|
|
// Calculate and print surplus fraction
|
|
|
|
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;
|
|
|
|
if opts.transferable_only && opts.subtract_nontransferable {
|
|
// Override transferable_votes
|
|
transferable_votes = count_card.votes.clone() - exhausted_votes;
|
|
}
|
|
|
|
let mut surplus_denom = calculate_surplus_denom(&surplus, &transferable_ballots, &transferable_votes, &total_ballots, &total_votes, opts);
|
|
let surplus_numer;
|
|
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);
|
|
surplus_numer = surplus_fraction.clone();
|
|
surplus_denom = None;
|
|
} else {
|
|
surplus_numer = Some(surplus.clone());
|
|
}
|
|
|
|
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;
|
|
surplus_numer = None;
|
|
surplus_denom = 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 transfer_table = TransferTable::new_surplus(
|
|
state.election.candidates.iter().filter(|c| state.candidates[c].state == CandidateState::Hopeful || state.candidates[c].state == CandidateState::Guarded).collect(),
|
|
surplus.clone(), surplus_fraction.clone(), surplus_numer.clone(), surplus_denom.clone()
|
|
);
|
|
|
|
for (value_fraction, source_order, result) in parcels_next_prefs {
|
|
for (candidate, entry) in result.candidates.into_iter() {
|
|
// Record transfers
|
|
transfer_table.add_transfers(
|
|
&value_fraction,
|
|
match opts.round_subtransfers {
|
|
RoundSubtransfersMode::ByValueAndSource => Some(source_order),
|
|
RoundSubtransfersMode::ByParcel => None, // Force new column per parcel
|
|
_ => Some(0)
|
|
},
|
|
candidate,
|
|
&entry.num_ballots
|
|
);
|
|
|
|
let mut new_value_fraction;
|
|
if opts.surplus.is_weighted() {
|
|
new_value_fraction = value_fraction.clone();
|
|
new_value_fraction *= surplus_numer.as_ref().unwrap(); // Guaranteed to be Some in WIGM
|
|
if let Some(n) = &surplus_denom {
|
|
new_value_fraction /= n;
|
|
}
|
|
} else {
|
|
if let Some(sf) = &surplus_fraction {
|
|
new_value_fraction = sf.clone();
|
|
} else {
|
|
new_value_fraction = value_fraction.clone();
|
|
}
|
|
}
|
|
|
|
if let Some(dps) = opts.round_values {
|
|
new_value_fraction.floor_mut(dps);
|
|
}
|
|
|
|
// Transfer candidate votes
|
|
let parcel = Parcel {
|
|
votes: entry.votes,
|
|
value_fraction: new_value_fraction,
|
|
source_order: state.num_elected + state.num_excluded,
|
|
};
|
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
|
count_card.parcels.push(parcel);
|
|
}
|
|
|
|
// Record exhausted votes
|
|
transfer_table.add_exhausted(
|
|
&value_fraction,
|
|
match opts.round_subtransfers {
|
|
RoundSubtransfersMode::ByValueAndSource => Some(source_order),
|
|
RoundSubtransfersMode::ByParcel => None, // Force new column per parcel
|
|
_ => Some(0)
|
|
},
|
|
&result.exhausted.num_ballots
|
|
);
|
|
|
|
// Transfer exhausted votes
|
|
let parcel = Parcel {
|
|
votes: result.exhausted.votes,
|
|
value_fraction, // TODO: Reweight exhausted votes
|
|
source_order: state.num_elected + state.num_excluded,
|
|
};
|
|
state.exhausted.parcels.push(parcel);
|
|
}
|
|
|
|
let mut checksum = N::new();
|
|
|
|
// Credit transferred votes
|
|
transfer_table.calculate(opts);
|
|
checksum += transfer_table.apply_to(state, opts);
|
|
state.transfer_table = Some(transfer_table);
|
|
|
|
// 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]
|
|
#[allow(clippy::branches_sharing_code)]
|
|
pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>, complete_type: &'static str)
|
|
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,
|
|
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 = parcels.first().map(|p| p.value_fraction.clone());
|
|
|
|
let mut transfer_table = TransferTable::new_exclusion(
|
|
state.election.candidates.iter().filter(|c| state.candidates[c].state == CandidateState::Hopeful || state.candidates[c].state == CandidateState::Guarded).collect(),
|
|
);
|
|
|
|
for src_parcel in parcels {
|
|
// Count next preferences
|
|
let result = super::next_preferences(state, src_parcel.votes);
|
|
|
|
total_ballots += &result.total_ballots;
|
|
total_votes += &result.total_ballots * &src_parcel.value_fraction;
|
|
|
|
// Transfer candidate votes
|
|
for (candidate, entry) in result.candidates.into_iter() {
|
|
let parcel = Parcel {
|
|
votes: entry.votes,
|
|
value_fraction: src_parcel.value_fraction.clone(),
|
|
source_order: state.num_elected + state.num_excluded,
|
|
};
|
|
|
|
// Record transfers
|
|
transfer_table.add_transfers(
|
|
&parcel.value_fraction,
|
|
match opts.round_subtransfers {
|
|
RoundSubtransfersMode::ByValueAndSource => Some(src_parcel.source_order),
|
|
RoundSubtransfersMode::ByParcel => None, // Force new column per parcel
|
|
_ => Some(0)
|
|
},
|
|
candidate,
|
|
&entry.num_ballots
|
|
);
|
|
|
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
|
count_card.parcels.push(parcel);
|
|
}
|
|
|
|
// Transfer exhausted votes
|
|
let parcel = Parcel {
|
|
votes: result.exhausted.votes,
|
|
value_fraction: src_parcel.value_fraction,
|
|
source_order: state.num_elected + state.num_excluded,
|
|
};
|
|
|
|
// Record transfers
|
|
transfer_table.add_exhausted(
|
|
&parcel.value_fraction,
|
|
match opts.round_subtransfers {
|
|
RoundSubtransfersMode::ByValueAndSource => Some(src_parcel.source_order),
|
|
RoundSubtransfersMode::ByParcel => None, // Force new column per parcel
|
|
_ => Some(0)
|
|
},
|
|
&result.exhausted.num_ballots
|
|
);
|
|
|
|
state.exhausted.parcels.push(parcel);
|
|
}
|
|
|
|
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
|
|
transfer_table.calculate(opts);
|
|
checksum += transfer_table.apply_to(state, opts);
|
|
state.transfer_table = Some(transfer_table);
|
|
|
|
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(format!("{} complete.", complete_type));
|
|
}
|
|
}
|
|
|
|
// Update loss by fraction
|
|
state.loss_fraction.transfer(&-checksum);
|
|
}
|
|
|
|
/// Exclude a candidate and reset the count from first preferences
|
|
pub fn exclude_candidates_and_reset<'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;
|
|
}
|