Refactor and add documentation
This commit is contained in:
parent
f9d47533ee
commit
3bbef933bb
|
@ -21,6 +21,7 @@ use crate::sharandom::SHARandom;
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// An election to be counted
|
||||
pub struct Election<N> {
|
||||
pub name: String,
|
||||
pub seats: usize,
|
||||
|
@ -30,6 +31,7 @@ pub struct Election<N> {
|
|||
}
|
||||
|
||||
impl<N: Number> Election<N> {
|
||||
/// Parse the given BLT file and return an [Election]
|
||||
pub fn from_blt<I: Iterator<Item=String>>(mut lines: I) -> Self {
|
||||
// Read first line
|
||||
let line = lines.next().expect("Unexpected EOF");
|
||||
|
@ -101,6 +103,7 @@ impl<N: Number> Election<N> {
|
|||
return election;
|
||||
}
|
||||
|
||||
/// Convert ballots with weight >1 to multiple ballots of weight 1
|
||||
pub fn normalise_ballots(&mut self) {
|
||||
let mut normalised_ballots = Vec::new();
|
||||
for ballot in self.ballots.iter() {
|
||||
|
@ -119,11 +122,13 @@ impl<N: Number> Election<N> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A candidate in an [Election]
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
pub struct Candidate {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// The current state of counting an [Election]
|
||||
#[derive(Clone)]
|
||||
pub struct CountState<'a, N> {
|
||||
pub election: &'a Election<N>,
|
||||
|
@ -148,6 +153,7 @@ pub struct CountState<'a, N> {
|
|||
}
|
||||
|
||||
impl<'a, N: Number> CountState<'a, N> {
|
||||
/// Construct a new blank [CountState] for the given [Election]
|
||||
pub fn new(election: &'a Election<N>) -> Self {
|
||||
let mut state = CountState {
|
||||
election: &election,
|
||||
|
@ -177,6 +183,7 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||
return state;
|
||||
}
|
||||
|
||||
/// [Step](CountCard::step) every [CountCard] to prepare for the next stage
|
||||
pub fn step_all(&mut self) {
|
||||
for (_, count_card) in self.candidates.iter_mut() {
|
||||
count_card.step();
|
||||
|
@ -186,6 +193,7 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents either a reference to a [CountState] or a clone
|
||||
#[allow(dead_code)]
|
||||
pub enum CountStateOrRef<'a, N> {
|
||||
State(CountState<'a, N>), // NYI: May be used e.g. for tie-breaking or rollback-based constraints
|
||||
|
@ -193,10 +201,12 @@ pub enum CountStateOrRef<'a, N> {
|
|||
}
|
||||
|
||||
impl<'a, N> CountStateOrRef<'a, N> {
|
||||
/// Construct a [CountStateOrRef] as a reference to a [CountState]
|
||||
pub fn from(state: &'a CountState<N>) -> Self {
|
||||
return Self::Ref(state);
|
||||
}
|
||||
|
||||
/// Return a reference to the underlying [CountState]
|
||||
pub fn as_ref(&self) -> &CountState<N> {
|
||||
match self {
|
||||
CountStateOrRef::State(state) => &state,
|
||||
|
@ -205,6 +215,7 @@ impl<'a, N> CountStateOrRef<'a, N> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Result of a stage of counting
|
||||
pub struct StageResult<'a, N> {
|
||||
pub kind: Option<&'a str>,
|
||||
pub title: &'a String,
|
||||
|
@ -212,6 +223,7 @@ pub struct StageResult<'a, N> {
|
|||
pub state: CountStateOrRef<'a, N>,
|
||||
}
|
||||
|
||||
/// Current state of a [Candidate] during an election count
|
||||
#[derive(Clone)]
|
||||
pub struct CountCard<'a, N> {
|
||||
pub state: CandidateState,
|
||||
|
@ -225,6 +237,7 @@ pub struct CountCard<'a, N> {
|
|||
}
|
||||
|
||||
impl<'a, N: Number> CountCard<'a, N> {
|
||||
/// Returns a new blank [CountCard]
|
||||
pub fn new() -> Self {
|
||||
return CountCard {
|
||||
state: CandidateState::Hopeful,
|
||||
|
@ -236,23 +249,23 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||
};
|
||||
}
|
||||
|
||||
//pub fn votes(&'a self) -> N {
|
||||
// return self.orig_votes.clone() + &self.transfers;
|
||||
//}
|
||||
|
||||
/// Transfer the given number of votes to this [CountCard], incrementing [transfers](CountCard::transfers) and [votes](CountCard::votes)
|
||||
pub fn transfer(&mut self, transfer: &'_ N) {
|
||||
self.transfers += transfer;
|
||||
self.votes += transfer;
|
||||
}
|
||||
|
||||
/// Set [orig_votes](CountCard::orig_votes) to [votes](CountCard::votes), and set [transfers](CountCard::transfers) to 0
|
||||
pub fn step(&mut self) {
|
||||
self.orig_votes = self.votes.clone();
|
||||
self.transfers = N::new();
|
||||
}
|
||||
}
|
||||
|
||||
/// Parcel of [Vote]s during a count
|
||||
pub type Parcel<'a, N> = Vec<Vote<'a, N>>;
|
||||
|
||||
/// Represents a [Ballot] with an associated value
|
||||
#[derive(Clone)]
|
||||
pub struct Vote<'a, N> {
|
||||
pub ballot: &'a Ballot<N>,
|
||||
|
@ -260,11 +273,13 @@ pub struct Vote<'a, N> {
|
|||
pub up_to_pref: usize,
|
||||
}
|
||||
|
||||
/// A record of a voter's preferences
|
||||
pub struct Ballot<N> {
|
||||
pub orig_value: N,
|
||||
pub preferences: Vec<usize>,
|
||||
}
|
||||
|
||||
/// State of a [Candidate] during a count
|
||||
#[allow(dead_code)]
|
||||
#[derive(PartialEq)]
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -15,17 +15,25 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// Data types for representing abstract elections
|
||||
pub mod election;
|
||||
/// Smart logging framework
|
||||
pub mod logger;
|
||||
/// Implementations of different numeric representations
|
||||
pub mod numbers;
|
||||
/// Deterministic random number generation using SHA256
|
||||
pub mod sharandom;
|
||||
/// STV counting logic
|
||||
pub mod stv;
|
||||
/// Tie-breaking methods
|
||||
pub mod ties;
|
||||
|
||||
use git_version::git_version;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
/// The git revision of this OpenTally build
|
||||
pub const VERSION: &str = git_version!(args=["--always", "--dirty=-dev"], fallback="unknown");
|
||||
|
||||
/// Get [VERSION] as a String (for WebAssembly)
|
||||
#[wasm_bindgen]
|
||||
pub fn version() -> String { VERSION.to_string() }
|
||||
|
|
|
@ -15,12 +15,16 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// Smart logger used in election counts
|
||||
#[derive(Clone)]
|
||||
pub struct Logger<'a> {
|
||||
pub entries: Vec<LogEntry<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Logger<'a> {
|
||||
/// Append a new entry to the log
|
||||
///
|
||||
/// If consecutive smart log entries have the same templates, they will be merged
|
||||
pub fn log(&mut self, entry: LogEntry<'a>) {
|
||||
if let LogEntry::Smart(mut smart) = entry {
|
||||
if self.entries.len() > 0 {
|
||||
|
@ -41,10 +45,14 @@ impl<'a> Logger<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Append a new literal log entry
|
||||
pub fn log_literal(&mut self, literal: String) {
|
||||
self.log(LogEntry::Literal(literal));
|
||||
}
|
||||
|
||||
/// Append a new smart log entry
|
||||
///
|
||||
/// If consecutive smart log entries have the same templates, they will be merged
|
||||
pub fn log_smart(&mut self, template1: &'a str, template2: &'a str, data: Vec<&'a str>) {
|
||||
self.log(LogEntry::Smart(SmartLogEntry {
|
||||
template1: template1,
|
||||
|
@ -53,6 +61,7 @@ impl<'a> Logger<'a> {
|
|||
}));
|
||||
}
|
||||
|
||||
/// Render the log to a [String]
|
||||
pub fn render(&self) -> Vec<String> {
|
||||
return self.entries.iter().map(|e| match e {
|
||||
LogEntry::Smart(smart) => smart.render(),
|
||||
|
@ -61,12 +70,14 @@ impl<'a> Logger<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents either a literal or smart log entry
|
||||
#[derive(Clone)]
|
||||
pub enum LogEntry<'a> {
|
||||
Smart(SmartLogEntry<'a>),
|
||||
Literal(String)
|
||||
}
|
||||
|
||||
/// Smart log entry
|
||||
#[derive(Clone)]
|
||||
pub struct SmartLogEntry<'a> {
|
||||
template1: &'a str,
|
||||
|
@ -75,6 +86,7 @@ pub struct SmartLogEntry<'a> {
|
|||
}
|
||||
|
||||
impl<'a> SmartLogEntry<'a> {
|
||||
/// Render the [SmartLogEntry] to a [String]
|
||||
pub fn render(&self) -> String {
|
||||
if self.data.len() == 0 {
|
||||
panic!("Attempted to format smart log entry with no data");
|
||||
|
@ -86,6 +98,7 @@ impl<'a> SmartLogEntry<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Join the given strings, with commas and terminal "and"
|
||||
pub fn smart_join(data: &Vec<&str>) -> String {
|
||||
return format!("{} and {}", data[0..data.len()-1].join(", "), data.last().unwrap());
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::{Assign, From, Number};
|
||||
use super::{Assign, Number};
|
||||
|
||||
use ibig::{IBig, ops::Abs};
|
||||
use num_traits::{Num, One, Zero};
|
||||
|
@ -37,6 +37,7 @@ fn get_factor() -> &'static IBig {
|
|||
unsafe { FACTOR.as_ref().unwrap() }
|
||||
}
|
||||
|
||||
/// Fixed-point number
|
||||
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Fixed(IBig);
|
||||
|
||||
|
|
|
@ -15,12 +15,16 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/// Fixed-point arithmetic (using `ibig`)
|
||||
mod fixed;
|
||||
/// Native 64-bit floating point arithmetic
|
||||
mod native;
|
||||
|
||||
/// Exact rational arithmetic (using `rug`/GMP)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod rational_rug;
|
||||
|
||||
/// Exact rational arithmetic (using `num-bigint`)
|
||||
//#[cfg(target_arch = "wasm32")]
|
||||
mod rational_num;
|
||||
|
||||
|
@ -30,29 +34,35 @@ use std::cmp::Ord;
|
|||
use std::fmt;
|
||||
use std::ops;
|
||||
|
||||
/// Assign value, avoiding additional allocations
|
||||
pub trait Assign<Src=Self> {
|
||||
/// Set the value of `self` to the value of `src`, avoiding additional allocations
|
||||
fn assign(&mut self, src: Src);
|
||||
}
|
||||
|
||||
pub trait From<T> {
|
||||
fn from(n: T) -> Self;
|
||||
}
|
||||
|
||||
/// Trait for OpenTally numeric representations
|
||||
//pub trait Number: NumRef + NumAssignRef + PartialOrd + Assign + Clone + fmt::Display where for<'a> &'a Self: RefNum<&'a Self> {
|
||||
pub trait Number:
|
||||
NumRef + NumAssignRef + ops::Neg<Output=Self> + Ord + Assign + From<usize> + From<f64> + Clone + fmt::Display
|
||||
where
|
||||
for<'a> Self: Assign<&'a Self>
|
||||
{
|
||||
/// Return a new [Number]
|
||||
fn new() -> Self;
|
||||
|
||||
/// Convert to CLI argument representation
|
||||
fn describe() -> String;
|
||||
/// Convert to CLI argument representation, returning an empty string if the default
|
||||
fn describe_opt() -> String { Self::describe() }
|
||||
|
||||
/// Exponentiate `self` to the `exponent` power
|
||||
fn pow_assign(&mut self, exponent: i32);
|
||||
/// Round `self` down if necessary to `dps` decimal places
|
||||
fn floor_mut(&mut self, dps: usize);
|
||||
/// Round `self` up if necessary to `dps` decimal places
|
||||
fn ceil_mut(&mut self, dps: usize);
|
||||
|
||||
/// Parse the given string into a [Number]
|
||||
fn parse(s: &str) -> Self {
|
||||
if let Ok(value) = Self::from_str_radix(s, 10) {
|
||||
return value;
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::{Assign, From, Number};
|
||||
use super::{Assign, Number};
|
||||
|
||||
use derive_more::Display;
|
||||
use num_traits::{Num, One, Zero};
|
||||
|
@ -26,6 +26,7 @@ use std::ops;
|
|||
|
||||
type ImplType = f64;
|
||||
|
||||
/// Native 64-bit floating-point number
|
||||
#[derive(Clone, Display, PartialEq, PartialOrd)]
|
||||
pub struct NativeFloat64(ImplType);
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::{Assign, From, Number};
|
||||
use super::{Assign, Number};
|
||||
|
||||
use num_traits::{Num, One, Signed, Zero};
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::{Assign, From, Number};
|
||||
use super::{Assign, Number};
|
||||
|
||||
use num_traits::{Num, One, Zero};
|
||||
use rug::{self, ops::Pow, ops::PowAssign, rational::ParseRationalError};
|
||||
|
@ -25,6 +25,7 @@ use std::cmp::{Ord, Ordering, PartialEq, PartialOrd};
|
|||
use std::fmt;
|
||||
use std::ops;
|
||||
|
||||
/// Rational number
|
||||
#[derive(Clone, PartialEq, PartialOrd)]
|
||||
pub struct Rational(rug::Rational);
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
use ibig::UBig;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Deterministic random number generator using SHA256
|
||||
#[derive(Clone)]
|
||||
pub struct SHARandom<'r> {
|
||||
seed: &'r str,
|
||||
|
@ -25,6 +26,7 @@ pub struct SHARandom<'r> {
|
|||
}
|
||||
|
||||
impl<'r> SHARandom<'r> {
|
||||
/// Return a new [SHARandom] with the given seed
|
||||
pub fn new(seed: &'r str) -> Self {
|
||||
Self {
|
||||
seed: seed,
|
||||
|
@ -32,6 +34,7 @@ impl<'r> SHARandom<'r> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw a random number *n*, such that 0 ≤ *n* < `max`
|
||||
pub fn next(&mut self, max: usize) -> usize {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(format!("{},{}", self.seed, self.counter).as_bytes());
|
||||
|
|
|
@ -0,0 +1,313 @@
|
|||
/* 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::{NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder};
|
||||
|
||||
use crate::election::{Candidate, CountCard, CountState, Parcel, Vote};
|
||||
use crate::numbers::Number;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use std::cmp::max;
|
||||
use std::ops;
|
||||
|
||||
/// Distribute the largest surplus according to the Gregory 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>,
|
||||
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 mut has_surplus: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
|
||||
.map(|c| (c, state.candidates.get(c).unwrap()))
|
||||
.filter(|(_, cc)| &cc.votes > quota)
|
||||
.collect();
|
||||
let total_surpluses = has_surplus.iter()
|
||||
.fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota);
|
||||
|
||||
if !has_surplus.is_empty() {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
match opts.surplus_order {
|
||||
SurplusOrder::BySize => {
|
||||
// Compare b with a to sort high-low
|
||||
has_surplus.sort_by(|a, b| b.1.votes.cmp(&a.1.votes));
|
||||
}
|
||||
SurplusOrder::ByOrder => {
|
||||
has_surplus.sort_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute top candidate's surplus
|
||||
let elected_candidate;
|
||||
|
||||
// Handle ties
|
||||
if has_surplus.len() > 1 && has_surplus[0].1.votes == has_surplus[1].1.votes {
|
||||
let max_votes = &has_surplus[0].1.votes;
|
||||
let has_surplus = has_surplus.into_iter().filter_map(|(c, cc)| if &cc.votes == max_votes { Some(c) } else { None }).collect();
|
||||
elected_candidate = super::choose_highest(state, opts, has_surplus)?;
|
||||
} else {
|
||||
elected_candidate = has_surplus[0].0;
|
||||
}
|
||||
|
||||
distribute_surplus(state, &opts, elected_candidate);
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
/// Return the denominator of the transfer value
|
||||
fn calculate_surplus_denom<N: Number>(surplus: &N, result: &NextPreferencesResult<N>, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option<N>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>
|
||||
{
|
||||
if transferable_only {
|
||||
let total_units = if weighted { &result.total_votes } else { &result.total_ballots };
|
||||
let exhausted_units = if weighted { &result.exhausted.num_votes } else { &result.exhausted.num_ballots };
|
||||
let transferable_units = total_units - exhausted_units;
|
||||
|
||||
if transferable_votes > surplus {
|
||||
return Some(transferable_units);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
if weighted {
|
||||
return Some(result.total_votes.clone());
|
||||
} else {
|
||||
return Some(result.total_ballots.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the reweighted value of the vote after being transferred
|
||||
fn reweight_vote<N: Number>(
|
||||
num_votes: &N,
|
||||
num_ballots: &N,
|
||||
surplus: &N,
|
||||
weighted: bool,
|
||||
surplus_fraction: &Option<N>,
|
||||
surplus_denom: &Option<N>,
|
||||
round_tvs: Option<usize>,
|
||||
rounding: Option<usize>) -> N
|
||||
{
|
||||
let mut result;
|
||||
|
||||
match surplus_denom {
|
||||
Some(v) => {
|
||||
if let Some(_) = round_tvs {
|
||||
// Rounding requested: use the rounded transfer value
|
||||
if weighted {
|
||||
result = num_votes.clone() * surplus_fraction.as_ref().unwrap();
|
||||
} else {
|
||||
result = num_ballots.clone() * surplus_fraction.as_ref().unwrap();
|
||||
}
|
||||
} else {
|
||||
// Avoid unnecessary rounding error by first multiplying by the surplus
|
||||
if weighted {
|
||||
result = num_votes.clone() * surplus / v;
|
||||
} else {
|
||||
result = num_ballots.clone() * surplus / v;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
result = num_votes.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Round down if requested
|
||||
if let Some(dps) = rounding {
|
||||
result.floor_mut(dps);
|
||||
}
|
||||
|
||||
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>, 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::Div<&'r N, Output=N>,
|
||||
{
|
||||
match opts.sum_surplus_transfers {
|
||||
SumSurplusTransfersMode::SingleStep => {
|
||||
// Calculate transfer across all votes
|
||||
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals));
|
||||
return reweight_vote(&entry.num_votes, &entry.num_ballots, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes);
|
||||
}
|
||||
SumSurplusTransfersMode::ByValue => {
|
||||
// Sum transfers by value
|
||||
let mut result = N::new();
|
||||
|
||||
// Sort into parcels by value
|
||||
let mut votes: Vec<&Vote<N>> = entry.votes.iter().collect();
|
||||
votes.sort_unstable_by(|a, b| (&a.value / &a.ballot.orig_value).cmp(&(&b.value / &b.ballot.orig_value)));
|
||||
for (_value, parcel) in &votes.into_iter().group_by(|v| &v.value / &v.ballot.orig_value) {
|
||||
let mut num_votes = N::new();
|
||||
let mut num_ballots = N::new();
|
||||
for vote in parcel {
|
||||
num_votes += &vote.value;
|
||||
num_ballots += &vote.ballot.orig_value;
|
||||
}
|
||||
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$}.", num_ballots, num_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
result += reweight_vote(&num_votes, &num_ballots, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes);
|
||||
}
|
||||
|
||||
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 result = N::new();
|
||||
for vote in entry.votes.iter() {
|
||||
result += reweight_vote(&vote.value, &vote.ballot.orig_value, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes);
|
||||
}
|
||||
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus]
|
||||
fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate)
|
||||
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.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
|
||||
|
||||
let count_card = state.candidates.get(elected_candidate).unwrap();
|
||||
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
|
||||
|
||||
let votes;
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG => {
|
||||
// Inclusive Gregory
|
||||
votes = state.candidates.get(elected_candidate).unwrap().parcels.concat();
|
||||
}
|
||||
SurplusMethod::EG => {
|
||||
// Exclusive Gregory
|
||||
// Should be safe to unwrap() - or else how did we get a quota!
|
||||
votes = state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap();
|
||||
}
|
||||
_ => { panic!("Invalid --surplus for Gregory method"); }
|
||||
}
|
||||
|
||||
// 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!() }
|
||||
};
|
||||
|
||||
let transferable_votes = &result.total_votes - &result.exhausted.num_votes;
|
||||
let surplus_denom = calculate_surplus_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only);
|
||||
let mut surplus_fraction;
|
||||
match surplus_denom {
|
||||
Some(ref v) => {
|
||||
surplus_fraction = Some(surplus.clone() / v);
|
||||
|
||||
// Round down if requested
|
||||
if let Some(dps) = opts.round_tvs {
|
||||
surplus_fraction.as_mut().unwrap().floor_mut(dps);
|
||||
}
|
||||
|
||||
if opts.transferable_only {
|
||||
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", &result.total_ballots - &result.exhausted.num_ballots, 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} ballots, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", result.total_ballots, result.total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
surplus_fraction = None;
|
||||
|
||||
if opts.transferable_only {
|
||||
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, at values received.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, dps=opts.pp_decimals));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, at values received.", result.total_ballots, result.total_votes, dps=opts.pp_decimals));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut checksum = N::new();
|
||||
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
// Credit transferred votes
|
||||
let candidate_transfers = sum_surplus_transfers(&entry, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts);
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.transfer(&candidate_transfers);
|
||||
checksum += candidate_transfers;
|
||||
|
||||
let mut parcel = entry.votes as Parcel<N>;
|
||||
|
||||
// Reweight votes
|
||||
for vote in parcel.iter_mut() {
|
||||
vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_tvs, opts.round_weights);
|
||||
}
|
||||
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
|
||||
// Credit exhausted votes
|
||||
let mut exhausted_transfers;
|
||||
if opts.transferable_only {
|
||||
if transferable_votes > surplus {
|
||||
// No ballots exhaust
|
||||
exhausted_transfers = N::new();
|
||||
} else {
|
||||
exhausted_transfers = &surplus - &transferable_votes;
|
||||
|
||||
if let Some(dps) = opts.round_votes {
|
||||
exhausted_transfers.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
exhausted_transfers = sum_surplus_transfers(&result.exhausted, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts);
|
||||
}
|
||||
|
||||
state.exhausted.transfer(&exhausted_transfers);
|
||||
checksum += exhausted_transfers;
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = result.exhausted.votes as Parcel<N>;
|
||||
state.exhausted.parcels.push(parcel);
|
||||
|
||||
// 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;
|
||||
|
||||
// Update loss by fraction
|
||||
state.loss_fraction.transfer(&-checksum);
|
||||
}
|
325
src/stv/mod.rs
325
src/stv/mod.rs
|
@ -17,6 +17,8 @@
|
|||
|
||||
#![allow(mutable_borrow_reservation_conflict)]
|
||||
|
||||
pub mod gregory;
|
||||
|
||||
//#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm;
|
||||
|
||||
|
@ -32,6 +34,7 @@ use std::cmp::max;
|
|||
use std::collections::HashMap;
|
||||
use std::ops;
|
||||
|
||||
/// Options for conducting an STV count
|
||||
pub struct STVOptions {
|
||||
pub round_tvs: Option<usize>,
|
||||
pub round_weights: Option<usize>,
|
||||
|
@ -53,6 +56,7 @@ pub struct STVOptions {
|
|||
}
|
||||
|
||||
impl STVOptions {
|
||||
/// Returns a new [STVOptions] based on arguments given as strings
|
||||
pub fn new(
|
||||
round_tvs: Option<usize>,
|
||||
round_weights: Option<usize>,
|
||||
|
@ -134,6 +138,7 @@ impl STVOptions {
|
|||
};
|
||||
}
|
||||
|
||||
/// Converts the [STVOptions] into CLI argument representation
|
||||
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()) };
|
||||
|
@ -160,6 +165,7 @@ impl STVOptions {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::sum_surplus_transfers]
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
|
@ -170,6 +176,7 @@ pub enum SumSurplusTransfersMode {
|
|||
}
|
||||
|
||||
impl SumSurplusTransfersMode {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
SumSurplusTransfersMode::SingleStep => "--sum-surplus-transfers single_step",
|
||||
|
@ -179,6 +186,7 @@ impl SumSurplusTransfersMode {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::quota]
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
|
@ -190,6 +198,7 @@ pub enum QuotaType {
|
|||
}
|
||||
|
||||
impl QuotaType {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
QuotaType::Droop => "--quota droop",
|
||||
|
@ -200,6 +209,7 @@ impl QuotaType {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::quota_criterion]
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
|
@ -209,6 +219,7 @@ pub enum QuotaCriterion {
|
|||
}
|
||||
|
||||
impl QuotaCriterion {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
QuotaCriterion::GreaterOrEqual => "--quota-criterion geq",
|
||||
|
@ -217,6 +228,7 @@ impl QuotaCriterion {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::quota_mode]
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
|
@ -226,6 +238,7 @@ pub enum QuotaMode {
|
|||
}
|
||||
|
||||
impl QuotaMode {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
QuotaMode::Static => "--quota-mode static",
|
||||
|
@ -234,6 +247,7 @@ impl QuotaMode {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::surplus]
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
|
@ -245,6 +259,7 @@ pub enum SurplusMethod {
|
|||
}
|
||||
|
||||
impl SurplusMethod {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
SurplusMethod::WIG => "--surplus wig",
|
||||
|
@ -255,6 +270,7 @@ impl SurplusMethod {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::surplus_order]
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
|
@ -264,6 +280,7 @@ pub enum SurplusOrder {
|
|||
}
|
||||
|
||||
impl SurplusOrder {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
SurplusOrder::BySize => "--surplus-order by_size",
|
||||
|
@ -272,6 +289,7 @@ impl SurplusOrder {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::exclusion]
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
|
@ -282,6 +300,7 @@ pub enum ExclusionMethod {
|
|||
}
|
||||
|
||||
impl ExclusionMethod {
|
||||
/// Convert to CLI argument representation
|
||||
fn describe(self) -> String {
|
||||
match self {
|
||||
ExclusionMethod::SingleStage => "--exclusion single_stage",
|
||||
|
@ -291,6 +310,7 @@ impl ExclusionMethod {
|
|||
}
|
||||
}
|
||||
|
||||
/// An error during the STV count
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub enum STVError {
|
||||
|
@ -298,6 +318,7 @@ pub enum STVError {
|
|||
UnresolvedTie,
|
||||
}
|
||||
|
||||
/// Distribute first preferences, and initialise other states such as the random number generator and tie-breaking rules
|
||||
pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a STVOptions) {
|
||||
// Initialise RNG
|
||||
for t in opts.ties.iter() {
|
||||
|
@ -312,6 +333,7 @@ pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a ST
|
|||
init_tiebreaks(&mut state, opts);
|
||||
}
|
||||
|
||||
/// Perform a single stage of the STV count
|
||||
pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
|
@ -358,6 +380,7 @@ where
|
|||
panic!("Count incomplete but unable to proceed");
|
||||
}
|
||||
|
||||
/// See [next_preferences]
|
||||
struct NextPreferencesResult<'a, N> {
|
||||
candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>,
|
||||
exhausted: NextPreferencesEntry<'a, N>,
|
||||
|
@ -365,6 +388,7 @@ struct NextPreferencesResult<'a, N> {
|
|||
total_votes: N,
|
||||
}
|
||||
|
||||
/// See [next_preferences]
|
||||
struct NextPreferencesEntry<'a, N> {
|
||||
//count_card: Option<&'a CountCard<'a, N>>,
|
||||
votes: Vec<Vote<'a, N>>,
|
||||
|
@ -372,6 +396,7 @@ struct NextPreferencesEntry<'a, N> {
|
|||
num_votes: N,
|
||||
}
|
||||
|
||||
/// Count the given votes, grouping according to next available preference
|
||||
fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a, N>>) -> NextPreferencesResult<'a, N> {
|
||||
let mut result = NextPreferencesResult {
|
||||
candidates: HashMap::new(),
|
||||
|
@ -426,6 +451,7 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a
|
|||
return result;
|
||||
}
|
||||
|
||||
/// Distribute first preference votes
|
||||
fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
||||
let votes = state.election.ballots.iter().map(|b| Vote {
|
||||
ballot: b,
|
||||
|
@ -453,6 +479,7 @@ fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
|||
state.logger.log_literal("First preferences distributed.".to_string());
|
||||
}
|
||||
|
||||
/// Calculate the quota, given the total vote, according to [STVOptions::quota]
|
||||
fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N {
|
||||
match opts.quota {
|
||||
QuotaType::Droop | QuotaType::DroopExact => {
|
||||
|
@ -484,6 +511,7 @@ fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N
|
|||
return total;
|
||||
}
|
||||
|
||||
/// Calculate the quota according to [STVOptions::quota]
|
||||
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||
// Calculate quota
|
||||
if let None = state.quota {
|
||||
|
@ -565,6 +593,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Determine if the given candidate meets the quota, according to [STVOptions::quota_criterion]
|
||||
fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
|
||||
match opts.quota_criterion {
|
||||
QuotaCriterion::GreaterOrEqual => {
|
||||
|
@ -576,6 +605,7 @@ fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOption
|
|||
}
|
||||
}
|
||||
|
||||
/// Declare elected all candidates meeting the quota
|
||||
fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||
let vote_req = state.vote_required_election.as_ref().unwrap(); // Have to do this or else the borrow checker gets confused
|
||||
|
||||
|
@ -613,6 +643,9 @@ fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
|
|||
}
|
||||
}
|
||||
|
||||
/// Determine whether the transfer of all surpluses can be deferred
|
||||
///
|
||||
/// The value of [STVOptions::defer_surpluses] is not taken into account and must be handled by the caller
|
||||
fn can_defer_surpluses<N: Number>(state: &CountState<N>, opts: &STVOptions, total_surpluses: &N) -> bool
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>
|
||||
|
@ -641,291 +674,24 @@ where
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Distribute surpluses according to [STVOptions::surplus]
|
||||
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>,
|
||||
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 mut has_surplus: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
|
||||
.map(|c| (c, state.candidates.get(c).unwrap()))
|
||||
.filter(|(_, cc)| &cc.votes > quota)
|
||||
.collect();
|
||||
let total_surpluses = has_surplus.iter()
|
||||
.fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota);
|
||||
|
||||
if !has_surplus.is_empty() {
|
||||
// Determine if surplues can be deferred
|
||||
if opts.defer_surpluses {
|
||||
if 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);
|
||||
}
|
||||
}
|
||||
|
||||
match opts.surplus_order {
|
||||
SurplusOrder::BySize => {
|
||||
// Compare b with a to sort high-low
|
||||
has_surplus.sort_by(|a, b| b.1.votes.cmp(&a.1.votes));
|
||||
}
|
||||
SurplusOrder::ByOrder => {
|
||||
has_surplus.sort_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected));
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute top candidate's surplus
|
||||
let elected_candidate;
|
||||
|
||||
// Handle ties
|
||||
if has_surplus.len() > 1 && has_surplus[0].1.votes == has_surplus[1].1.votes {
|
||||
let max_votes = &has_surplus[0].1.votes;
|
||||
let has_surplus = has_surplus.into_iter().filter_map(|(c, cc)| if &cc.votes == max_votes { Some(c) } else { None }).collect();
|
||||
elected_candidate = choose_highest(state, opts, has_surplus)?;
|
||||
} else {
|
||||
elected_candidate = has_surplus[0].0;
|
||||
}
|
||||
|
||||
distribute_surplus(state, &opts, elected_candidate);
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
/// Return the denominator of the transfer value
|
||||
fn calculate_surplus_denom<N: Number>(surplus: &N, result: &NextPreferencesResult<N>, transferable_votes: &N, weighted: bool, transferable_only: bool) -> Option<N>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>
|
||||
{
|
||||
if transferable_only {
|
||||
let total_units = if weighted { &result.total_votes } else { &result.total_ballots };
|
||||
let exhausted_units = if weighted { &result.exhausted.num_votes } else { &result.exhausted.num_ballots };
|
||||
let transferable_units = total_units - exhausted_units;
|
||||
|
||||
if transferable_votes > surplus {
|
||||
return Some(transferable_units);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
if weighted {
|
||||
return Some(result.total_votes.clone());
|
||||
} else {
|
||||
return Some(result.total_ballots.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reweight_vote<N: Number>(
|
||||
num_votes: &N,
|
||||
num_ballots: &N,
|
||||
surplus: &N,
|
||||
weighted: bool,
|
||||
surplus_fraction: &Option<N>,
|
||||
surplus_denom: &Option<N>,
|
||||
round_tvs: Option<usize>,
|
||||
rounding: Option<usize>) -> N
|
||||
{
|
||||
let mut result;
|
||||
|
||||
match surplus_denom {
|
||||
Some(v) => {
|
||||
if let Some(_) = round_tvs {
|
||||
// Rounding requested: use the rounded transfer value
|
||||
if weighted {
|
||||
result = num_votes.clone() * surplus_fraction.as_ref().unwrap();
|
||||
} else {
|
||||
result = num_ballots.clone() * surplus_fraction.as_ref().unwrap();
|
||||
}
|
||||
} else {
|
||||
// Avoid unnecessary rounding error by first multiplying by the surplus
|
||||
if weighted {
|
||||
result = num_votes.clone() * surplus / v;
|
||||
} else {
|
||||
result = num_ballots.clone() * surplus / v;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
result = num_votes.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Round down if requested
|
||||
if let Some(dps) = rounding {
|
||||
result.floor_mut(dps);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn sum_surplus_transfers<N: Number>(entry: &NextPreferencesEntry<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::Div<&'r N, Output=N>,
|
||||
{
|
||||
match opts.sum_surplus_transfers {
|
||||
SumSurplusTransfersMode::SingleStep => {
|
||||
// Calculate transfer across all votes
|
||||
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals));
|
||||
return reweight_vote(&entry.num_votes, &entry.num_ballots, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes);
|
||||
}
|
||||
SumSurplusTransfersMode::ByValue => {
|
||||
// Sum transfers by value
|
||||
let mut result = N::new();
|
||||
|
||||
// Sort into parcels by value
|
||||
let mut votes: Vec<&Vote<N>> = entry.votes.iter().collect();
|
||||
votes.sort_unstable_by(|a, b| (&a.value / &a.ballot.orig_value).cmp(&(&b.value / &b.ballot.orig_value)));
|
||||
for (_value, parcel) in &votes.into_iter().group_by(|v| &v.value / &v.ballot.orig_value) {
|
||||
let mut num_votes = N::new();
|
||||
let mut num_ballots = N::new();
|
||||
for vote in parcel {
|
||||
num_votes += &vote.value;
|
||||
num_ballots += &vote.ballot.orig_value;
|
||||
}
|
||||
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$}.", num_ballots, num_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
result += reweight_vote(&num_votes, &num_ballots, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes);
|
||||
}
|
||||
|
||||
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 result = N::new();
|
||||
for vote in entry.votes.iter() {
|
||||
result += reweight_vote(&vote.value, &vote.ballot.orig_value, surplus, is_weighted, surplus_fraction, surplus_denom, opts.round_tvs, opts.round_votes);
|
||||
}
|
||||
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn distribute_surplus<N: Number>(state: &mut CountState<N>, opts: &STVOptions, elected_candidate: &Candidate)
|
||||
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.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
|
||||
|
||||
let count_card = state.candidates.get(elected_candidate).unwrap();
|
||||
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
|
||||
|
||||
let votes;
|
||||
match opts.surplus {
|
||||
SurplusMethod::WIG | SurplusMethod::UIG => {
|
||||
// Inclusive Gregory
|
||||
votes = state.candidates.get(elected_candidate).unwrap().parcels.concat();
|
||||
}
|
||||
SurplusMethod::EG => {
|
||||
// Exclusive Gregory
|
||||
// Should be safe to unwrap() - or else how did we get a quota!
|
||||
votes = state.candidates.get_mut(elected_candidate).unwrap().parcels.pop().unwrap();
|
||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
||||
return gregory::distribute_surpluses(state, opts);
|
||||
}
|
||||
SurplusMethod::Meek => {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
// Count next preferences
|
||||
let result = 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!() }
|
||||
};
|
||||
|
||||
let transferable_votes = &result.total_votes - &result.exhausted.num_votes;
|
||||
let surplus_denom = calculate_surplus_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only);
|
||||
let mut surplus_fraction;
|
||||
match surplus_denom {
|
||||
Some(ref v) => {
|
||||
surplus_fraction = Some(surplus.clone() / v);
|
||||
|
||||
// Round down if requested
|
||||
if let Some(dps) = opts.round_tvs {
|
||||
surplus_fraction.as_mut().unwrap().floor_mut(dps);
|
||||
}
|
||||
|
||||
if opts.transferable_only {
|
||||
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$}.", &result.total_ballots - &result.exhausted.num_ballots, 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} ballots, totalling {:.dps$} votes, with surplus fraction {:.dps2$}.", result.total_ballots, result.total_votes, surplus_fraction.as_ref().unwrap(), dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
surplus_fraction = None;
|
||||
|
||||
if opts.transferable_only {
|
||||
state.logger.log_literal(format!("Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, at values received.", &result.total_ballots - &result.exhausted.num_ballots, transferable_votes, dps=opts.pp_decimals));
|
||||
} else {
|
||||
state.logger.log_literal(format!("Transferring {:.0} ballots, totalling {:.dps$} votes, at values received.", result.total_ballots, result.total_votes, dps=opts.pp_decimals));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut checksum = N::new();
|
||||
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
// Credit transferred votes
|
||||
let candidate_transfers = sum_surplus_transfers(&entry, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts);
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.transfer(&candidate_transfers);
|
||||
checksum += candidate_transfers;
|
||||
|
||||
let mut parcel = entry.votes as Parcel<N>;
|
||||
|
||||
// Reweight votes
|
||||
for vote in parcel.iter_mut() {
|
||||
vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_tvs, opts.round_weights);
|
||||
}
|
||||
|
||||
count_card.parcels.push(parcel);
|
||||
}
|
||||
|
||||
// Credit exhausted votes
|
||||
let mut exhausted_transfers;
|
||||
if opts.transferable_only {
|
||||
if transferable_votes > surplus {
|
||||
// No ballots exhaust
|
||||
exhausted_transfers = N::new();
|
||||
} else {
|
||||
exhausted_transfers = &surplus - &transferable_votes;
|
||||
|
||||
if let Some(dps) = opts.round_votes {
|
||||
exhausted_transfers.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
exhausted_transfers = sum_surplus_transfers(&result.exhausted, &surplus, is_weighted, &surplus_fraction, &surplus_denom, state, opts);
|
||||
}
|
||||
|
||||
state.exhausted.transfer(&exhausted_transfers);
|
||||
checksum += exhausted_transfers;
|
||||
|
||||
// Transfer exhausted votes
|
||||
let parcel = result.exhausted.votes as Parcel<N>;
|
||||
state.exhausted.parcels.push(parcel);
|
||||
|
||||
// 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;
|
||||
|
||||
// Update loss by fraction
|
||||
state.loss_fraction.transfer(&-checksum);
|
||||
}
|
||||
|
||||
/// Declare all continuing candidates elected, if the number equals the number of remaining vacancies
|
||||
fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError> {
|
||||
if state.election.candidates.len() - state.num_excluded <= state.election.seats {
|
||||
state.kind = None;
|
||||
|
@ -973,6 +739,9 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result
|
|||
return Ok(false);
|
||||
}
|
||||
|
||||
/// Determine which continuing candidates could be excluded in a bulk exclusion
|
||||
///
|
||||
/// The value of [STVOptions::bulk_exclude] is not taken into account and must be handled by the caller
|
||||
fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &STVOptions) -> Vec<&'a Candidate> {
|
||||
let mut excluded_candidates = Vec::new();
|
||||
|
||||
|
@ -1012,6 +781,7 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST
|
|||
return excluded_candidates;
|
||||
}
|
||||
|
||||
/// Exclude the lowest-ranked hopeful candidate(s)
|
||||
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
|
@ -1058,6 +828,7 @@ where
|
|||
return Ok(true);
|
||||
}
|
||||
|
||||
/// Continue the exclusion of a candidate who is being excluded
|
||||
fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
|
||||
where
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
|
@ -1093,6 +864,7 @@ where
|
|||
return 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>)
|
||||
where
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
|
@ -1246,6 +1018,7 @@ where
|
|||
state.loss_fraction.transfer(&-checksum);
|
||||
}
|
||||
|
||||
/// Determine if the count is complete because the number of elected candidates equals the number of vacancies
|
||||
fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
|
||||
if state.num_elected >= state.election.seats {
|
||||
return true;
|
||||
|
@ -1253,6 +1026,9 @@ fn finished_before_stage<N: Number>(state: &CountState<N>) -> bool {
|
|||
return false;
|
||||
}
|
||||
|
||||
/// Break a tie between the given candidates according to [STVOptions::ties], selecting the highest candidate
|
||||
///
|
||||
/// The given candidates are assumed to be tied in this round
|
||||
fn choose_highest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
||||
for strategy in opts.ties.iter() {
|
||||
match strategy.choose_highest(state, &candidates) {
|
||||
|
@ -1271,6 +1047,9 @@ fn choose_highest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, c
|
|||
panic!("Unable to resolve tie");
|
||||
}
|
||||
|
||||
/// Break a tie between the given candidates according to [STVOptions::ties], selecting the lowest candidate
|
||||
///
|
||||
/// The given candidates are assumed to be tied in this round
|
||||
fn choose_lowest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
||||
for strategy in opts.ties.iter() {
|
||||
match strategy.choose_lowest(state, &candidates) {
|
||||
|
@ -1286,9 +1065,10 @@ fn choose_lowest<'c, N: Number>(state: &mut CountState<N>, opts: &STVOptions, ca
|
|||
}
|
||||
}
|
||||
}
|
||||
return Err(STVError::UnresolvedTie);
|
||||
panic!("Unable to resolve tie");
|
||||
}
|
||||
|
||||
/// If required, initialise the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties]
|
||||
fn init_tiebreaks<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||
if !opts.ties.iter().any(|t| t == &TieStrategy::Forwards) && !opts.ties.iter().any(|t| t == &TieStrategy::Backwards) {
|
||||
return;
|
||||
|
@ -1326,6 +1106,7 @@ fn init_tiebreaks<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
|||
}
|
||||
}
|
||||
|
||||
/// If required, update the state of the forwards or backwards tie-breaking strategies, according to [STVOptions::ties]
|
||||
fn update_tiebreaks<N: Number>(state: &mut CountState<N>, _opts: &STVOptions) {
|
||||
if let None = state.forwards_tiebreak {
|
||||
if let None = state.backwards_tiebreak {
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![allow(rustdoc::private_intra_doc_links)]
|
||||
|
||||
use crate::election::{CandidateState, CountState, Election};
|
||||
use crate::numbers::{Fixed, NativeFloat64, Number, Rational};
|
||||
use crate::stv;
|
||||
|
@ -26,6 +28,7 @@ use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
|
|||
|
||||
// Init
|
||||
|
||||
/// Wrapper for [Fixed::set_dps]
|
||||
#[wasm_bindgen]
|
||||
pub fn fixed_set_dps(dps: usize) {
|
||||
Fixed::set_dps(dps);
|
||||
|
@ -37,6 +40,7 @@ macro_rules! impl_type {
|
|||
($type:ident) => { paste::item! {
|
||||
// Counting
|
||||
|
||||
/// Wrapper for [Election::from_blt]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<election_from_blt_$type>](text: String) -> [<Election$type>] {
|
||||
|
@ -47,18 +51,21 @@ macro_rules! impl_type {
|
|||
return [<Election$type>](election);
|
||||
}
|
||||
|
||||
/// Wrapper for [Election::normalise_ballots]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<election_normalise_ballots_$type>](election: &mut [<Election$type>]) {
|
||||
election.0.normalise_ballots();
|
||||
}
|
||||
|
||||
/// Wrapper for [stv::count_init]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<count_init_$type>](state: &mut [<CountState$type>], opts: &STVOptions) {
|
||||
stv::count_init(&mut state.0, opts.as_static());
|
||||
}
|
||||
|
||||
/// Wrapper for [stv::count_one_stage]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &STVOptions) -> Result<bool, JsValue> {
|
||||
|
@ -71,36 +78,42 @@ macro_rules! impl_type {
|
|||
|
||||
// Reporting
|
||||
|
||||
/// Wrapper for [init_results_table]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions) -> String {
|
||||
return init_results_table(&election.0, &opts.0);
|
||||
}
|
||||
|
||||
/// Wrapper for [describe_count]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<describe_count_$type>](filename: String, election: &[<Election$type>], opts: &STVOptions) -> String {
|
||||
return describe_count(filename, &election.0, &opts.0);
|
||||
}
|
||||
|
||||
/// Wrapper for [update_results_table]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions) -> Array {
|
||||
return update_results_table(stage_num, &state.0, &opts.0);
|
||||
}
|
||||
|
||||
/// Wrapper for [update_stage_comments]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<update_stage_comments_$type>](state: &[<CountState$type>]) -> String {
|
||||
return update_stage_comments(&state.0);
|
||||
}
|
||||
|
||||
/// Wrapper for [finalise_results_table]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<finalise_results_table_$type>](state: &[<CountState$type>]) -> Array {
|
||||
return finalise_results_table(&state.0);
|
||||
}
|
||||
|
||||
/// Wrapper for [final_result_summary]
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<final_result_summary_$type>](state: &[<CountState$type>]) -> String {
|
||||
|
@ -110,6 +123,7 @@ macro_rules! impl_type {
|
|||
// Wrapper structs
|
||||
// Required as we cannot specify &'static in wasm-bindgen: issue #1187
|
||||
|
||||
/// Wrapper for [CountState]
|
||||
#[wasm_bindgen]
|
||||
pub struct [<CountState$type>](CountState<'static, $type>);
|
||||
#[wasm_bindgen]
|
||||
|
@ -119,6 +133,7 @@ macro_rules! impl_type {
|
|||
}
|
||||
}
|
||||
|
||||
/// Wrapper for [Election]
|
||||
#[wasm_bindgen]
|
||||
pub struct [<Election$type>](Election<$type>);
|
||||
#[wasm_bindgen]
|
||||
|
@ -140,11 +155,13 @@ impl_type!(Fixed);
|
|||
impl_type!(NativeFloat64);
|
||||
impl_type!(Rational);
|
||||
|
||||
/// Wrapper for [stv::STVOptions]
|
||||
#[wasm_bindgen]
|
||||
pub struct STVOptions(stv::STVOptions);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl STVOptions {
|
||||
/// Wrapper for [stv::STVOptions::new]
|
||||
pub fn new(
|
||||
round_tvs: Option<usize>,
|
||||
round_weights: Option<usize>,
|
||||
|
@ -189,6 +206,11 @@ impl STVOptions {
|
|||
}
|
||||
|
||||
impl STVOptions {
|
||||
/// Return the underlying [stv::STVOptions] as a `&'static stv::STVOptions`
|
||||
///
|
||||
/// # Safety
|
||||
/// Assumes that the underlying [stv::STVOptions] is valid for the `'static` lifetime, as it would be if the [stv::STVOptions] were created from Javascript
|
||||
///
|
||||
fn as_static(&self) -> &'static stv::STVOptions {
|
||||
unsafe {
|
||||
let ptr = &self.0 as *const stv::STVOptions;
|
||||
|
@ -199,18 +221,7 @@ impl STVOptions {
|
|||
|
||||
// Reporting
|
||||
|
||||
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String {
|
||||
let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
|
||||
for candidate in election.candidates.iter() {
|
||||
result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
|
||||
}
|
||||
result.push_str(r#"<tr class="info transfers"><td rowspan="2">Exhausted</td></tr><tr class="info votes"></tr><tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr><tr class="info votes"></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"#);
|
||||
if opts.quota_mode == stv::QuotaMode::ERS97 {
|
||||
result.push_str(r#"<tr class="info transfers"><td>Vote required for election</td></tr>"#);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generate the lead-in description of the count in HTML
|
||||
fn describe_count<N: Number>(filename: String, election: &Election<N>, opts: &stv::STVOptions) -> String {
|
||||
let mut result = String::from("<p>Count computed by OpenTally (revision ");
|
||||
result.push_str(crate::VERSION);
|
||||
|
@ -227,6 +238,20 @@ fn describe_count<N: Number>(filename: String, election: &Election<N>, opts: &st
|
|||
return result;
|
||||
}
|
||||
|
||||
/// Generate the first column of the HTML results table
|
||||
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String {
|
||||
let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
|
||||
for candidate in election.candidates.iter() {
|
||||
result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
|
||||
}
|
||||
result.push_str(r#"<tr class="info transfers"><td rowspan="2">Exhausted</td></tr><tr class="info votes"></tr><tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr><tr class="info votes"></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"#);
|
||||
if opts.quota_mode == stv::QuotaMode::ERS97 {
|
||||
result.push_str(r#"<tr class="info transfers"><td>Vote required for election</td></tr>"#);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generate subsequent columns of the HTML results table
|
||||
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions) -> Array {
|
||||
let result = Array::new();
|
||||
result.push(&format!(r#"<td>{}</td>"#, stage_num).into());
|
||||
|
@ -271,10 +296,12 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
|
|||
return result;
|
||||
}
|
||||
|
||||
/// Get the comment for the current stage
|
||||
fn update_stage_comments<N: Number>(state: &CountState<N>) -> String {
|
||||
return state.logger.render().join(" ");
|
||||
}
|
||||
|
||||
/// Generate the final column of the HTML results table
|
||||
fn finalise_results_table<N: Number>(state: &CountState<N>) -> Array {
|
||||
let result = Array::new();
|
||||
|
||||
|
@ -301,6 +328,7 @@ fn finalise_results_table<N: Number>(state: &CountState<N>) -> Array {
|
|||
return result;
|
||||
}
|
||||
|
||||
/// Generate the final lead-out text summarising the result of the election
|
||||
fn final_result_summary<N: Number>(state: &CountState<N>) -> String {
|
||||
let mut result = String::from("<p>Count complete. The winning candidates are, in order of election:</p><ol>");
|
||||
|
||||
|
@ -320,6 +348,7 @@ fn final_result_summary<N: Number>(state: &CountState<N>) -> String {
|
|||
return result;
|
||||
}
|
||||
|
||||
/// HTML pretty-print the number to the specified decimal places
|
||||
fn pp<N: Number>(n: &N, dps: usize) -> String {
|
||||
if n.is_zero() {
|
||||
return "".to_string();
|
||||
|
|
|
@ -26,6 +26,7 @@ use wasm_bindgen::prelude::wasm_bindgen;
|
|||
#[allow(unused_imports)]
|
||||
use std::io::{stdin, stdout, Write};
|
||||
|
||||
/// Strategy for breaking ties
|
||||
#[derive(PartialEq)]
|
||||
pub enum TieStrategy {
|
||||
Forwards,
|
||||
|
@ -35,6 +36,7 @@ pub enum TieStrategy {
|
|||
}
|
||||
|
||||
impl TieStrategy {
|
||||
/// Convert to CLI argument representation
|
||||
pub fn describe(&self) -> String {
|
||||
match self {
|
||||
Self::Forwards => "forwards",
|
||||
|
@ -44,6 +46,9 @@ impl TieStrategy {
|
|||
}.to_string()
|
||||
}
|
||||
|
||||
/// Break a tie between the given candidates, selecting the highest candidate
|
||||
///
|
||||
/// The given candidates are assumed to be tied in this round
|
||||
pub fn choose_highest<'c, N: Number>(&self, state: &mut CountState<N>, candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
||||
match self {
|
||||
Self::Forwards => {
|
||||
|
@ -89,6 +94,9 @@ impl TieStrategy {
|
|||
}
|
||||
}
|
||||
|
||||
/// Break a tie between the given candidates, selecting the lowest candidate
|
||||
///
|
||||
/// The given candidates are assumed to be tied in this round
|
||||
pub fn choose_lowest<'c, N: Number>(&self, state: &mut CountState<N>, candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
||||
match self {
|
||||
Self::Forwards => {
|
||||
|
@ -127,6 +135,7 @@ impl TieStrategy {
|
|||
}
|
||||
}
|
||||
|
||||
/// Prompt the candidate for input, depending on CLI or WebAssembly target
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
|
||||
println!("Multiple tied candidates:");
|
||||
|
|
|
@ -94,7 +94,7 @@ fn ers97_rational() {
|
|||
let mut candidate_votes: Vec<Option<Rational>> = records.iter().skip(2)
|
||||
.map(|r|
|
||||
if r[idx*2 + 1].len() > 0 {
|
||||
Some(opentally::numbers::From::from(r[idx*2 + 1].parse::<f64>().expect("Syntax Error")))
|
||||
Some(Rational::from(r[idx*2 + 1].parse::<f64>().expect("Syntax Error")))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
|
|
|
@ -140,5 +140,5 @@ fn get_cand_stage(candidate: &Element, idx: usize) -> &Element {
|
|||
fn parse_str(s: String) -> Fixed {
|
||||
if s == "-" { return Fixed::zero(); }
|
||||
let f: f64 = s.parse().expect("Syntax Error");
|
||||
return opentally::numbers::From::from(f);
|
||||
return Fixed::from(f);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue