Refactor and add documentation

This commit is contained in:
RunasSudo 2021-06-14 20:43:36 +10:00
parent f9d47533ee
commit 3bbef933bb
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
15 changed files with 482 additions and 298 deletions

View File

@ -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)]

View File

@ -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() }

View File

@ -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());
}

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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};

View File

@ -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);

View File

@ -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());

313
src/stv/gregory.rs Normal file
View File

@ -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);
}

View File

@ -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 {

View File

@ -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();

View File

@ -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:");

View File

@ -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
})

View File

@ -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);
}