Implement --transfers-detail
This commit is contained in:
parent
056242514d
commit
9817d6c199
|
@ -14,7 +14,7 @@ version = "0.7.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.3",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
|
@ -28,6 +28,12 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.5.2"
|
||||
|
@ -48,6 +54,17 @@ dependencies = [
|
|||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
|
@ -60,12 +77,29 @@ version = "1.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "822d7d63e0c0260a050f6b1f0d316f5c79b9eab830aca526ed904e1011bd64ca"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||
|
||||
[[package]]
|
||||
name = "blake2b_simd"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"constant_time_eq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.9.0"
|
||||
|
@ -174,6 +208,12 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27d614f23f34f7b5165a77dc1591f497e2518f9cec4b4f4b92bfc4dc6cf7a190"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
|
@ -198,6 +238,16 @@ dependencies = [
|
|||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.1.6"
|
||||
|
@ -314,6 +364,17 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "doc-comment"
|
||||
version = "0.3.3"
|
||||
|
@ -326,6 +387,12 @@ version = "1.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.20"
|
||||
|
@ -363,6 +430,17 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.3"
|
||||
|
@ -371,7 +449,7 @@ checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
|
|||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.10.2+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -430,6 +508,15 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ibig"
|
||||
version = "0.3.2"
|
||||
|
@ -632,6 +719,7 @@ dependencies = [
|
|||
"num-traits",
|
||||
"paste",
|
||||
"predicates",
|
||||
"prettytable-rs",
|
||||
"rkyv",
|
||||
"rug",
|
||||
"sha2",
|
||||
|
@ -690,6 +778,20 @@ dependencies = [
|
|||
"treeline",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettytable-rs"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"csv",
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"term",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
|
@ -779,6 +881,23 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
"redox_syscall",
|
||||
"rust-argon2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.4"
|
||||
|
@ -850,6 +969,18 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-argon2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blake2b_simd",
|
||||
"constant_time_eq",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.3.3"
|
||||
|
@ -931,6 +1062,17 @@ dependencies = [
|
|||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"dirs",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.13.4"
|
||||
|
@ -961,6 +1103,12 @@ version = "1.7.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
|
@ -997,6 +1145,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
|
|
|
@ -32,6 +32,7 @@ paste = "1.0.5"
|
|||
assert_cmd = "1.0.5"
|
||||
csv = "1.1.6"
|
||||
flate2 = "1.0"
|
||||
prettytable-rs = "0.8.0"
|
||||
rkyv = "0.7.15"
|
||||
utf8-chars = "1.0.2"
|
||||
xmltree = "0.10.3"
|
||||
|
|
|
@ -391,13 +391,20 @@ fn print_stage<N: Number>(stage_num: u32, state: &CountState<N>, opts: &STVOptio
|
|||
println!("{}. {}", stage_num, state.title);
|
||||
println!("{}", state.logger.render().join(" "));
|
||||
|
||||
if opts.transfers_detail {
|
||||
if let Some(tt) = &state.transfer_table {
|
||||
println!();
|
||||
println!("{}", tt.render_text(state, opts));
|
||||
}
|
||||
}
|
||||
|
||||
// Print candidates
|
||||
print!("{}", state.describe_candidates(opts));
|
||||
|
||||
// Print summary rows
|
||||
print!("{}", state.describe_summary(opts));
|
||||
|
||||
println!("");
|
||||
println!();
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
|
|
|
@ -20,8 +20,8 @@ use crate::logger::Logger;
|
|||
use crate::numbers::Number;
|
||||
use crate::sharandom::SHARandom;
|
||||
use crate::stv::{self, STVOptions};
|
||||
use crate::stv::gregory::TransferTable;
|
||||
use crate::stv::meek::BallotTree;
|
||||
use crate::stv::transfers::TransferTable;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
|
|
|
@ -15,13 +15,15 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
mod transfers;
|
||||
pub use transfers::{TransferTable, TransferTableCell, TransferTableColumn};
|
||||
|
||||
use super::{ExclusionMethod, STVError, STVOptions, SurplusMethod, SurplusOrder};
|
||||
use super::sample;
|
||||
|
||||
use crate::constraints;
|
||||
use crate::election::{Candidate, CandidateState, CountState, Parcel, StageKind, Vote};
|
||||
use crate::numbers::Number;
|
||||
use crate::stv::transfers::TransferTable;
|
||||
use crate::ties;
|
||||
|
||||
use std::cmp::max;
|
||||
|
@ -299,7 +301,7 @@ where
|
|||
|
||||
// Reweight and transfer parcels
|
||||
|
||||
let mut transfer_table = TransferTable::new();
|
||||
let mut transfer_table = TransferTable::new_surplus(surplus.clone(), surplus_fraction.clone(), surplus_numer.clone(), surplus_denom.clone());
|
||||
|
||||
for (value_fraction, result) in parcels_next_prefs {
|
||||
for (candidate, entry) in result.candidates.into_iter() {
|
||||
|
@ -350,7 +352,9 @@ where
|
|||
let mut checksum = N::new();
|
||||
|
||||
// Credit transferred votes
|
||||
checksum += transfer_table.apply_to(state, opts, Some(&surplus), &surplus_numer, &surplus_denom);
|
||||
transfer_table.calculate(opts);
|
||||
checksum += transfer_table.apply_to(state, opts);
|
||||
state.transfer_table = Some(transfer_table);
|
||||
|
||||
// Finalise candidate votes
|
||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||
|
@ -549,7 +553,7 @@ where
|
|||
|
||||
let value = match parcels.first() { Some(p) => Some(p.value_fraction.clone()), _ => None };
|
||||
|
||||
let mut transfer_table = TransferTable::new();
|
||||
let mut transfer_table = TransferTable::new_exclusion();
|
||||
|
||||
for parcel in parcels {
|
||||
// Count next preferences
|
||||
|
@ -603,7 +607,9 @@ where
|
|||
}
|
||||
|
||||
// Credit transferred votes
|
||||
checksum += transfer_table.apply_to(state, opts, None, &None, &None);
|
||||
transfer_table.calculate(opts);
|
||||
checksum += transfer_table.apply_to(state, opts);
|
||||
state.transfer_table = Some(transfer_table);
|
||||
|
||||
if !votes_remain {
|
||||
// Finalise candidate votes
|
|
@ -0,0 +1,499 @@
|
|||
/* 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 prettytable::{Cell, Row, Table};
|
||||
|
||||
use crate::election::{Candidate, CountState};
|
||||
use crate::numbers::Number;
|
||||
use crate::stv::{STVOptions, SumSurplusTransfersMode};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Table describing vote transfers during a surplus distribution or exclusion
|
||||
pub struct TransferTable<'e, N: Number> {
|
||||
/// Columns in the table
|
||||
pub columns: Vec<TransferTableColumn<'e, N>>,
|
||||
|
||||
/// Total column
|
||||
pub total: TransferTableColumn<'e, N>,
|
||||
|
||||
/// Size of surplus, or `None` if an exclusion
|
||||
pub surplus: Option<N>,
|
||||
/// Surplus fraction, or `None` if votes not reweighted/an exclusion (for display/optimisation only)
|
||||
pub surpfrac: Option<N>,
|
||||
/// Numerator of surplus fraction, or `None` if votes not reweighted/an exclusion
|
||||
pub surpfrac_numer: Option<N>,
|
||||
/// Denominator of surplus fraction, or `None`
|
||||
pub surpfrac_denom: Option<N>,
|
||||
}
|
||||
|
||||
impl<'e, N: Number> TransferTable<'e, N> {
|
||||
/// Return a new [TransferTable] for an exclusion
|
||||
pub fn new_exclusion() -> Self {
|
||||
TransferTable {
|
||||
columns: Vec::new(),
|
||||
total: TransferTableColumn {
|
||||
value_fraction: N::new(),
|
||||
cells: HashMap::new(),
|
||||
exhausted: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
},
|
||||
surplus: None,
|
||||
surpfrac: None,
|
||||
surpfrac_numer: None,
|
||||
surpfrac_denom: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a new [TransferTable] for a surplus distribution
|
||||
pub fn new_surplus(surplus: N, surpfrac: Option<N>, surpfrac_numer: Option<N>, surpfrac_denom: Option<N>) -> Self {
|
||||
TransferTable {
|
||||
columns: Vec::new(),
|
||||
total: TransferTableColumn {
|
||||
value_fraction: N::new(),
|
||||
cells: HashMap::new(),
|
||||
exhausted: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
},
|
||||
surplus: Some(surplus),
|
||||
surpfrac,
|
||||
surpfrac_numer,
|
||||
surpfrac_denom,
|
||||
}
|
||||
}
|
||||
|
||||
/// Record the specified transfer
|
||||
pub fn add_transfers(&mut self, value_fraction: &N, candidate: &'e Candidate, ballots: &N) {
|
||||
for col in self.columns.iter_mut() {
|
||||
if &col.value_fraction == value_fraction {
|
||||
col.add_transfers(candidate, ballots);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mut col = TransferTableColumn {
|
||||
value_fraction: value_fraction.clone(),
|
||||
cells: HashMap::new(),
|
||||
exhausted: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
};
|
||||
col.add_transfers(candidate, ballots);
|
||||
self.columns.push(col);
|
||||
}
|
||||
|
||||
/// Record the specified exhaustion
|
||||
pub fn add_exhausted(&mut self, value_fraction: &N, ballots: &N) {
|
||||
for col in self.columns.iter_mut() {
|
||||
if &col.value_fraction == value_fraction {
|
||||
col.exhausted.ballots += ballots;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let col = TransferTableColumn {
|
||||
value_fraction: value_fraction.clone(),
|
||||
cells: HashMap::new(),
|
||||
exhausted: TransferTableCell { ballots: ballots.clone(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
};
|
||||
self.columns.push(col);
|
||||
}
|
||||
|
||||
/// Calculate the votes to be transferred according to this table
|
||||
pub fn calculate(&mut self, opts: &STVOptions) {
|
||||
// Use weighted rules if exclusion or WIGM
|
||||
let is_weighted = self.surplus.is_none() || opts.surplus.is_weighted();
|
||||
|
||||
// Iterate through columns
|
||||
for column in self.columns.iter_mut() {
|
||||
let mut new_value_fraction = N::new();
|
||||
if self.surplus.is_some() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
|
||||
if is_weighted {
|
||||
new_value_fraction = column.value_fraction.clone();
|
||||
// If surplus, multiply by surplus fraction
|
||||
if let Some(n) = &self.surpfrac_numer {
|
||||
new_value_fraction *= n;
|
||||
}
|
||||
if let Some(n) = &self.surpfrac_denom {
|
||||
new_value_fraction /= n;
|
||||
}
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_values {
|
||||
new_value_fraction.floor_mut(dps);
|
||||
}
|
||||
} else {
|
||||
if let Some(n) = &self.surpfrac_numer {
|
||||
new_value_fraction = n.clone();
|
||||
} else {
|
||||
// Transferred at original value
|
||||
new_value_fraction = column.value_fraction.clone();
|
||||
}
|
||||
if let Some(n) = &self.surpfrac_denom {
|
||||
new_value_fraction /= n;
|
||||
}
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_values {
|
||||
new_value_fraction.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Candidate votes
|
||||
for (candidate, cell) in column.cells.iter_mut() {
|
||||
column.total.ballots += &cell.ballots;
|
||||
self.total.add_transfers(*candidate, &cell.ballots);
|
||||
self.total.total.ballots += &cell.ballots;
|
||||
|
||||
let votes_in = cell.ballots.clone() * &column.value_fraction;
|
||||
cell.votes_in += &votes_in;
|
||||
column.total.votes_in += &votes_in;
|
||||
self.total.cells.get_mut(*candidate).unwrap().votes_in += &votes_in;
|
||||
self.total.total.votes_in += votes_in;
|
||||
|
||||
if self.surplus.is_some() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
|
||||
let votes_out = cell.ballots.clone() * &new_value_fraction;
|
||||
cell.votes_out += &votes_out;
|
||||
column.total.votes_out += &votes_out;
|
||||
self.total.cells.get_mut(*candidate).unwrap().votes_out += &votes_out;
|
||||
self.total.total.votes_out += votes_out;
|
||||
}
|
||||
}
|
||||
|
||||
// Exhausted votes
|
||||
column.total.ballots += &column.exhausted.ballots;
|
||||
self.total.exhausted.ballots += &column.exhausted.ballots;
|
||||
self.total.total.ballots += &column.exhausted.ballots;
|
||||
|
||||
let votes_in = column.exhausted.ballots.clone() * &column.value_fraction;
|
||||
column.exhausted.votes_in += &votes_in;
|
||||
column.total.votes_in += &votes_in;
|
||||
self.total.exhausted.votes_in += &votes_in;
|
||||
self.total.total.votes_in += votes_in;
|
||||
|
||||
if self.surplus.is_some() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
|
||||
if !opts.transferable_only {
|
||||
let votes_out = column.exhausted.ballots.clone() * &new_value_fraction;
|
||||
column.exhausted.votes_out += &votes_out;
|
||||
column.total.votes_out += &votes_out;
|
||||
self.total.exhausted.votes_out += &votes_out;
|
||||
self.total.total.votes_out += votes_out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need to calculate votes_out?
|
||||
if self.surplus.is_none() || opts.sum_surplus_transfers == SumSurplusTransfersMode::ByValue {
|
||||
for (_candidate, cell) in self.total.cells.iter_mut() {
|
||||
let mut votes_out;
|
||||
if is_weighted {
|
||||
votes_out = cell.votes_in.clone();
|
||||
} else {
|
||||
votes_out = cell.ballots.clone();
|
||||
}
|
||||
|
||||
// If surplus, multiply by surplus fraction
|
||||
if let Some(n) = &self.surpfrac_numer {
|
||||
votes_out *= n;
|
||||
}
|
||||
if let Some(n) = &self.surpfrac_denom {
|
||||
votes_out /= n;
|
||||
}
|
||||
|
||||
cell.votes_out = votes_out; // Rounded later
|
||||
}
|
||||
|
||||
if self.surplus.is_none() || !opts.transferable_only {
|
||||
let mut votes_out;
|
||||
if is_weighted {
|
||||
votes_out = self.total.exhausted.votes_in.clone();
|
||||
} else {
|
||||
votes_out = self.total.exhausted.ballots.clone();
|
||||
}
|
||||
|
||||
// If surplus, multiply by surplus fraction
|
||||
if let Some(n) = &self.surpfrac_numer {
|
||||
votes_out *= n;
|
||||
}
|
||||
if let Some(n) = &self.surpfrac_denom {
|
||||
votes_out /= n;
|
||||
}
|
||||
|
||||
self.total.exhausted.votes_out = votes_out; // Rounded later
|
||||
}
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
for (_candidate, cell) in self.total.cells.iter_mut() {
|
||||
cell.votes_out.floor_mut(dps);
|
||||
}
|
||||
|
||||
self.total.exhausted.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the transfers described in the table to the count sheet
|
||||
///
|
||||
/// Credit continuing candidates and exhausted pile with the appropriate number of ballot papers and votes.
|
||||
pub fn apply_to(&self, state: &mut CountState<N>, opts: &STVOptions) -> N {
|
||||
let mut checksum = N::new();
|
||||
|
||||
// Credit transferred votes
|
||||
for (candidate, count_card) in state.candidates.iter_mut() {
|
||||
if let Some(cell) = self.total.cells.get(candidate) {
|
||||
count_card.transfer(&cell.votes_out);
|
||||
count_card.ballot_transfers += &cell.ballots;
|
||||
checksum += &cell.votes_out;
|
||||
}
|
||||
}
|
||||
|
||||
// Credit exhausted votes
|
||||
// If exclusion or not --transferable-only
|
||||
if self.surplus.is_none() || !opts.transferable_only {
|
||||
// Standard rules
|
||||
state.exhausted.transfer(&self.total.exhausted.votes_out);
|
||||
state.exhausted.ballot_transfers += &self.total.exhausted.ballots;
|
||||
checksum += &self.total.exhausted.votes_out;
|
||||
} else {
|
||||
// Credit only nontransferable difference
|
||||
if self.surpfrac_numer.is_none() {
|
||||
// TODO: Is there a purer way of calculating this?
|
||||
let difference = self.surplus.as_ref().unwrap().clone() - &checksum;
|
||||
state.exhausted.transfer(&difference);
|
||||
checksum += difference;
|
||||
|
||||
for column in self.columns.iter() {
|
||||
state.exhausted.ballot_transfers += &column.exhausted.ballots;
|
||||
}
|
||||
} else {
|
||||
// No ballots exhaust
|
||||
}
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// Render table as plain text
|
||||
pub fn render_text(&self, state: &CountState<N>, opts: &STVOptions) -> String {
|
||||
let mut table = Table::new();
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
|
||||
|
||||
let show_transfers_per_ballot = !self.surpfrac.is_none() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot;
|
||||
|
||||
let num_cols;
|
||||
if show_transfers_per_ballot {
|
||||
num_cols = state.candidates.len() * 3 + 4;
|
||||
} else {
|
||||
if self.surpfrac.is_none() {
|
||||
num_cols = state.candidates.len() * 2 + 3;
|
||||
} else {
|
||||
num_cols = state.candidates.len() * 2 + 4;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------
|
||||
// Header row
|
||||
|
||||
let mut row = Vec::with_capacity(num_cols);
|
||||
row.push(Cell::new("Preference"));
|
||||
for column in self.columns.iter() {
|
||||
row.push(Cell::new(&format!("Ballots @ {:.dps$}", column.value_fraction, dps=opts.pp_decimals)).style_spec("cH2"));
|
||||
|
||||
if show_transfers_per_ballot {
|
||||
row.push(Cell::new(&format!("× {:.dps$}", self.surpfrac.as_ref().unwrap(), dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
}
|
||||
row.push(Cell::new("Total").style_spec("cH2"));
|
||||
if self.surpfrac.is_some() {
|
||||
row.push(Cell::new(&format!("× {:.dps$}", self.surpfrac.as_ref().unwrap(), dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
table.set_titles(Row::new(row));
|
||||
|
||||
// --------------
|
||||
// Candidate rows
|
||||
|
||||
for candidate in state.election.candidates.iter() {
|
||||
let mut row = Vec::with_capacity(num_cols);
|
||||
row.push(Cell::new(&candidate.name));
|
||||
for column in self.columns.iter() {
|
||||
if let Some(cell) = column.cells.get(candidate) {
|
||||
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if show_transfers_per_ballot {
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if show_transfers_per_ballot {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Totals
|
||||
if let Some(cell) = self.total.cells.get(candidate) {
|
||||
row.push(Cell::new(&format!("{:.0}", cell.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if self.surpfrac.is_some() {
|
||||
row.push(Cell::new(&format!("{:.dps$}", cell.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if self.surpfrac.is_some() {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
|
||||
table.add_row(Row::new(row));
|
||||
}
|
||||
|
||||
// -------------
|
||||
// Exhausted row
|
||||
|
||||
let mut row = Vec::with_capacity(num_cols);
|
||||
row.push(Cell::new("Exhausted"));
|
||||
for column in self.columns.iter() {
|
||||
if !column.exhausted.ballots.is_zero() {
|
||||
row.push(Cell::new(&format!("{:.0}", column.exhausted.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", column.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if show_transfers_per_ballot {
|
||||
if column.exhausted.votes_out.is_zero() {
|
||||
row.push(Cell::new("-").style_spec("c"));
|
||||
} else {
|
||||
row.push(Cell::new(&format!("{:.dps$}", column.exhausted.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if show_transfers_per_ballot {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Totals
|
||||
if !self.total.exhausted.ballots.is_zero() {
|
||||
row.push(Cell::new(&format!("{:.0}", self.total.exhausted.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", self.total.exhausted.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if self.surpfrac.is_some() {
|
||||
if self.total.exhausted.votes_out.is_zero() {
|
||||
row.push(Cell::new("-").style_spec("c"));
|
||||
} else {
|
||||
row.push(Cell::new(&format!("{:.dps$}", self.total.exhausted.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
row.push(Cell::new(""));
|
||||
row.push(Cell::new(""));
|
||||
if self.surpfrac.is_some() {
|
||||
row.push(Cell::new(""));
|
||||
}
|
||||
}
|
||||
|
||||
table.add_row(Row::new(row));
|
||||
|
||||
// ----------
|
||||
// Totals row
|
||||
|
||||
let mut row = Vec::with_capacity(num_cols);
|
||||
row.push(Cell::new("Total"));
|
||||
|
||||
for column in self.columns.iter() {
|
||||
row.push(Cell::new(&format!("{:.0}", column.total.ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if show_transfers_per_ballot {
|
||||
row.push(Cell::new(&format!("{:.dps$}", column.total.votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
}
|
||||
|
||||
// Grand total cell
|
||||
|
||||
let mut gt_ballots = N::new();
|
||||
let mut gt_votes_in = N::new();
|
||||
let mut gt_votes_out = N::new();
|
||||
|
||||
for candidate in state.election.candidates.iter() {
|
||||
if let Some(cell) = self.total.cells.get(candidate) {
|
||||
gt_ballots += &cell.ballots;
|
||||
gt_votes_in += &cell.votes_in;
|
||||
gt_votes_out += &cell.votes_out;
|
||||
}
|
||||
}
|
||||
gt_ballots += &self.total.exhausted.ballots;
|
||||
gt_votes_in += &self.total.exhausted.votes_in;
|
||||
gt_votes_out += &self.total.exhausted.votes_out;
|
||||
|
||||
row.push(Cell::new(&format!("{:.0}", gt_ballots)).style_spec("r"));
|
||||
row.push(Cell::new(&format!("{:.dps$}", gt_votes_in, dps=opts.pp_decimals)).style_spec("r"));
|
||||
if self.surpfrac.is_some() {
|
||||
row.push(Cell::new(&format!("{:.dps$}", gt_votes_out, dps=opts.pp_decimals)).style_spec("r"));
|
||||
}
|
||||
|
||||
table.add_row(Row::new(row));
|
||||
|
||||
return table.to_string();
|
||||
}
|
||||
|
||||
/// Render table as HTML
|
||||
pub fn render_html(&self) -> String {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
/// Column in a [TransferTable]
|
||||
pub struct TransferTableColumn<'e, N: Number> {
|
||||
/// Value fraction of ballots counted in this column
|
||||
pub value_fraction: N,
|
||||
|
||||
/// Cells in this column
|
||||
pub cells: HashMap<&'e Candidate, TransferTableCell<N>>,
|
||||
|
||||
/// Exhausted cell
|
||||
pub exhausted: TransferTableCell<N>,
|
||||
|
||||
/// Totals cell
|
||||
pub total: TransferTableCell<N>,
|
||||
}
|
||||
|
||||
impl<'e, N: Number> TransferTableColumn<'e, N> {
|
||||
/// Record the specified transfer
|
||||
pub fn add_transfers(&mut self, candidate: &'e Candidate, ballots: &N) {
|
||||
if let Some(cell) = self.cells.get_mut(candidate) {
|
||||
cell.ballots += ballots;
|
||||
} else {
|
||||
let cell = TransferTableCell {
|
||||
ballots: ballots.clone(),
|
||||
votes_in: N::new(),
|
||||
votes_out: N::new(),
|
||||
};
|
||||
self.cells.insert(candidate, cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cell in a [TransferTable], representing transfers to one candidate at a particular value
|
||||
pub struct TransferTableCell<N: Number> {
|
||||
/// Ballots expressing a next preference for the continuing candidate
|
||||
pub ballots: N,
|
||||
/// Value of votes when received by the transferring candidate
|
||||
pub votes_in: N,
|
||||
/// Votes transferred to the continuing candidate
|
||||
pub votes_out: N,
|
||||
}
|
|
@ -23,8 +23,6 @@ pub mod gregory;
|
|||
pub mod meek;
|
||||
/// Random sample methods of surplus distributions
|
||||
pub mod sample;
|
||||
/// Transfer tables
|
||||
pub mod transfers;
|
||||
|
||||
/// WebAssembly wrappers
|
||||
//#[cfg(target_arch = "wasm32")]
|
||||
|
@ -149,15 +147,15 @@ pub struct STVOptions {
|
|||
#[builder(default="ConstraintMode::GuardDoom")]
|
||||
pub constraint_mode: ConstraintMode,
|
||||
|
||||
/// Hide excluded candidates from results report
|
||||
/// (CLI) Hide excluded candidates from results report
|
||||
#[builder(default="false")]
|
||||
pub hide_excluded: bool,
|
||||
|
||||
/// Sort candidates by votes in results report
|
||||
/// (CLI) Sort candidates by votes in results report
|
||||
#[builder(default="false")]
|
||||
pub sort_votes: bool,
|
||||
|
||||
/// Show details of transfers to candidates during surplus distributions/candidate exclusions
|
||||
/// (CLI) Show details of transfers to candidates during surplus distributions/candidate exclusions
|
||||
#[builder(default="false")]
|
||||
pub transfers_detail: bool,
|
||||
|
||||
|
@ -206,6 +204,7 @@ impl STVOptions {
|
|||
}
|
||||
if self.hide_excluded { flags.push(format!("--hide-excluded")); }
|
||||
if self.sort_votes { flags.push(format!("--sort-votes")); }
|
||||
if self.transfers_detail { flags.push(format!("--transfers-detail")); }
|
||||
if self.pp_decimals != 2 { flags.push(format!("--pp-decimals {}", self.pp_decimals)); }
|
||||
return flags.join(" ");
|
||||
}
|
||||
|
@ -641,6 +640,7 @@ where
|
|||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Neg<Output=N>,
|
||||
{
|
||||
state.transfer_table = None;
|
||||
state.logger.entries.clear();
|
||||
state.step_all();
|
||||
|
||||
|
|
|
@ -1,250 +0,0 @@
|
|||
/* 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::{STVOptions, SumSurplusTransfersMode};
|
||||
|
||||
use crate::election::{Candidate, CountState};
|
||||
use crate::numbers::Number;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Table describing vote transfers during a surplus distribution or exclusion
|
||||
pub struct TransferTable<'e, N: Number> {
|
||||
/// Columns in the table
|
||||
pub columns: Vec<TransferTableColumn<'e, N>>,
|
||||
}
|
||||
|
||||
impl<'e, N: Number> TransferTable<'e, N> {
|
||||
/// Return a new [TransferTable]
|
||||
pub fn new() -> Self {
|
||||
TransferTable {
|
||||
columns: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record the specified transfer
|
||||
pub fn add_transfers(&mut self, value_fraction: &N, candidate: &'e Candidate, ballots: &N) {
|
||||
for col in self.columns.iter_mut() {
|
||||
if &col.value_fraction == value_fraction {
|
||||
col.add_transfers(candidate, ballots);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mut col = TransferTableColumn {
|
||||
value_fraction: value_fraction.clone(),
|
||||
cells: HashMap::new(),
|
||||
exhausted: TransferTableCell { ballots: N::new() },
|
||||
};
|
||||
col.add_transfers(candidate, ballots);
|
||||
self.columns.push(col);
|
||||
}
|
||||
|
||||
/// Record the specified exhaustion
|
||||
pub fn add_exhausted(&mut self, value_fraction: &N, ballots: &N) {
|
||||
for col in self.columns.iter_mut() {
|
||||
if &col.value_fraction == value_fraction {
|
||||
col.exhausted.ballots += ballots;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let col = TransferTableColumn {
|
||||
value_fraction: value_fraction.clone(),
|
||||
cells: HashMap::new(),
|
||||
exhausted: TransferTableCell { ballots: ballots.clone() },
|
||||
};
|
||||
self.columns.push(col);
|
||||
}
|
||||
|
||||
/// Apply the transfers described in the table to the count sheet
|
||||
///
|
||||
/// Credit continuing candidates and exhausted pile with the appropriate number of ballot papers and votes.
|
||||
pub fn apply_to(&self, state: &mut CountState<N>, opts: &STVOptions, surplus: Option<&N>, surplus_numer: &Option<N>, surplus_denom: &Option<N>) -> N {
|
||||
// Use weighted rules if exclusion or WIGM
|
||||
let is_weighted = surplus.is_none() || opts.surplus.is_weighted();
|
||||
|
||||
let mut checksum = N::new();
|
||||
|
||||
// Credit transferred votes
|
||||
for (candidate, count_card) in state.candidates.iter_mut() {
|
||||
let mut votes_transferred = N::new();
|
||||
let mut ballots_transferred = N::new();
|
||||
|
||||
// If exclusion, or surplus at present value, or SumSurplusTransfersMode::ByValue
|
||||
if surplus_numer.is_none() || opts.sum_surplus_transfers == SumSurplusTransfersMode::ByValue {
|
||||
// Calculate transfer across all votes in this parcel
|
||||
for column in self.columns.iter() {
|
||||
if let Some(cell) = column.cells.get(*candidate) {
|
||||
if is_weighted {
|
||||
votes_transferred += cell.ballots.clone() * &column.value_fraction;
|
||||
}
|
||||
ballots_transferred += &cell.ballots;
|
||||
}
|
||||
}
|
||||
|
||||
if !is_weighted {
|
||||
votes_transferred = ballots_transferred.clone();
|
||||
}
|
||||
|
||||
// If surplus, multiply by surplus fraction
|
||||
if let Some(n) = &surplus_numer {
|
||||
votes_transferred *= n;
|
||||
}
|
||||
if let Some(n) = &surplus_denom {
|
||||
votes_transferred /= n;
|
||||
}
|
||||
} else if opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot {
|
||||
// Sum transfer per each individual ballot
|
||||
for column in self.columns.iter() {
|
||||
if let Some(cell) = column.cells.get(*candidate) {
|
||||
ballots_transferred += &cell.ballots;
|
||||
|
||||
let mut new_value_fraction;
|
||||
if is_weighted {
|
||||
new_value_fraction = column.value_fraction.clone();
|
||||
// If surplus, multiply by surplus fraction
|
||||
if let Some(n) = &surplus_numer {
|
||||
new_value_fraction *= n;
|
||||
}
|
||||
if let Some(n) = &surplus_denom {
|
||||
new_value_fraction /= n;
|
||||
}
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_values {
|
||||
new_value_fraction.floor_mut(dps);
|
||||
}
|
||||
} else {
|
||||
if let Some(n) = &surplus_numer {
|
||||
new_value_fraction = n.clone();
|
||||
} else {
|
||||
// Transferred at original value
|
||||
new_value_fraction = column.value_fraction.clone();
|
||||
}
|
||||
if let Some(n) = &surplus_denom {
|
||||
new_value_fraction /= n;
|
||||
}
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_values {
|
||||
new_value_fraction.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
|
||||
votes_transferred += cell.ballots.clone() * new_value_fraction;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
votes_transferred.floor_mut(dps);
|
||||
}
|
||||
|
||||
count_card.transfer(&votes_transferred);
|
||||
count_card.ballot_transfers += ballots_transferred;
|
||||
|
||||
checksum += votes_transferred;
|
||||
}
|
||||
|
||||
// Credit exhausted votes
|
||||
// If exclusion or not --transferable-only
|
||||
if surplus.is_none() || !opts.transferable_only {
|
||||
// Standard rules
|
||||
let mut votes_transferred = N::new();
|
||||
let mut ballots_transferred = N::new();
|
||||
|
||||
for column in self.columns.iter() {
|
||||
if is_weighted {
|
||||
votes_transferred += column.exhausted.ballots.clone() * &column.value_fraction;
|
||||
}
|
||||
ballots_transferred += &column.exhausted.ballots;
|
||||
}
|
||||
|
||||
if !is_weighted {
|
||||
votes_transferred = ballots_transferred.clone();
|
||||
}
|
||||
|
||||
// If surplus, multiply by surplus fraction
|
||||
if let Some(n) = &surplus_numer {
|
||||
votes_transferred *= n;
|
||||
}
|
||||
if let Some(n) = &surplus_denom {
|
||||
votes_transferred /= n;
|
||||
}
|
||||
|
||||
// Round if required
|
||||
if let Some(dps) = opts.round_votes {
|
||||
votes_transferred.floor_mut(dps);
|
||||
}
|
||||
|
||||
state.exhausted.transfer(&votes_transferred);
|
||||
state.exhausted.ballot_transfers += ballots_transferred;
|
||||
|
||||
checksum += votes_transferred;
|
||||
} else {
|
||||
// Credit only nontransferable difference
|
||||
if surplus_numer.is_none() {
|
||||
// TODO: Is there a purer way of calculating this?
|
||||
let difference = surplus.unwrap().clone() - &checksum;
|
||||
state.exhausted.transfer(&difference);
|
||||
checksum += difference;
|
||||
|
||||
for column in self.columns.iter() {
|
||||
state.exhausted.ballot_transfers += &column.exhausted.ballots;
|
||||
}
|
||||
} else {
|
||||
// No ballots exhaust
|
||||
}
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
}
|
||||
|
||||
/// Column in a [TransferTable]
|
||||
pub struct TransferTableColumn<'e, N: Number> {
|
||||
/// Value fraction of ballots counted in this column
|
||||
pub value_fraction: N,
|
||||
|
||||
/// Cells in this column
|
||||
pub cells: HashMap<&'e Candidate, TransferTableCell<N>>,
|
||||
|
||||
/// Exhausted cell
|
||||
pub exhausted: TransferTableCell<N>,
|
||||
}
|
||||
|
||||
impl<'e, N: Number> TransferTableColumn<'e, N> {
|
||||
/// Record the specified transfer
|
||||
pub fn add_transfers(&mut self, candidate: &'e Candidate, ballots: &N) {
|
||||
if let Some(cell) = self.cells.get_mut(candidate) {
|
||||
cell.ballots += ballots;
|
||||
} else {
|
||||
let cell = TransferTableCell {
|
||||
ballots: ballots.clone(),
|
||||
};
|
||||
self.cells.insert(candidate, cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cell in a [TransferTable], representing transfers to one candidate at a particular value
|
||||
pub struct TransferTableCell<N: Number> {
|
||||
/// Ballots transferred to this candidate
|
||||
pub ballots: N,
|
||||
}
|
|
@ -240,7 +240,6 @@ impl STVOptions {
|
|||
min_threshold: String,
|
||||
constraints_path: Option<String>,
|
||||
constraint_mode: &str,
|
||||
transfers_detail: bool,
|
||||
pp_decimals: usize,
|
||||
) -> Self {
|
||||
Self(stv::STVOptions::new(
|
||||
|
@ -271,7 +270,7 @@ impl STVOptions {
|
|||
constraint_mode.into(),
|
||||
false,
|
||||
false,
|
||||
transfers_detail,
|
||||
false,
|
||||
pp_decimals,
|
||||
))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue