From f6fba85049d65698de640103cffb6f84fb26cd3f Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 1 Jun 2021 21:20:38 +1000 Subject: [PATCH] Implement --round-{tvs,weights,quota} Add test case for PRSA example election --- src/main.rs | 15 +++++ src/numbers/mod.rs | 12 +++- src/numbers/native.rs | 18 ++++-- src/numbers/rational_num.rs | 25 +++++--- src/numbers/rational_rug.rs | 31 ++++++---- src/stv/mod.rs | 92 ++++++++++++++++++++++-------- tests/aec.rs | 62 ++++---------------- tests/data/prsa1.blt | 75 ++++++++++++++++++++++++ tests/data/prsa1.csv | 11 ++++ tests/data/prsa1.ods | Bin 0 -> 13027 bytes tests/prsa.rs | 37 ++++++++++++ tests/utils/mod.rs | 110 ++++++++++++++++++++++++++++++++++++ 12 files changed, 388 insertions(+), 100 deletions(-) create mode 100644 tests/data/prsa1.blt create mode 100644 tests/data/prsa1.csv create mode 100644 tests/data/prsa1.ods create mode 100644 tests/prsa.rs create mode 100644 tests/utils/mod.rs diff --git a/src/main.rs b/src/main.rs index e1740fd..ffaf5b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,10 +61,22 @@ struct STV { // ----------------------- // -- Rounding settings -- + /// Round transfer values to specified decimal places + #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] + round_tvs: Option, + + /// Round ballot weights to specified decimal places + #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] + round_weights: Option, + /// Round votes to specified decimal places #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] round_votes: Option, + /// Round quota to specified decimal places + #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] + round_quota: Option, + // ------------------ // -- STV variants -- @@ -126,7 +138,10 @@ where { // Copy applicable options let stv_opts = stv::STVOptions::new( + cmd_opts.round_tvs, + cmd_opts.round_weights, cmd_opts.round_votes, + cmd_opts.round_quota, &cmd_opts.surplus, &cmd_opts.surplus_order, cmd_opts.transferable_only, diff --git a/src/numbers/mod.rs b/src/numbers/mod.rs index 9c5e4b0..ad0066d 100644 --- a/src/numbers/mod.rs +++ b/src/numbers/mod.rs @@ -33,11 +33,19 @@ pub trait Assign { fn assign(&mut self, src: Src); } +pub trait From { + fn from(n: T) -> Self; +} + //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 + Ord + Assign + Clone + fmt::Display where for<'a> Self: Assign<&'a Self> { +pub trait Number: + NumRef + NumAssignRef + ops::Neg + Ord + Assign + From + From + Clone + fmt::Display +where + for<'a> Self: Assign<&'a Self> +{ fn new() -> Self; - fn from(n: usize) -> Self; + fn pow_assign(&mut self, exponent: i32); fn floor_mut(&mut self, dps: usize); fn parse(s: &str) -> Self { diff --git a/src/numbers/native.rs b/src/numbers/native.rs index d83c51e..c136850 100644 --- a/src/numbers/native.rs +++ b/src/numbers/native.rs @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -use super::{Assign, Number}; +use super::{Assign, From, Number}; use num_traits::{Num, One, Zero}; @@ -31,7 +31,9 @@ pub struct NativeFloat64(ImplType); impl Number for NativeFloat64 { fn new() -> Self { Self(0.0) } - fn from(n: usize) -> Self { Self(n as ImplType) } + fn pow_assign(&mut self, exponent: i32) { + self.0 = self.0.powi(exponent); + } fn floor_mut(&mut self, dps: usize) { let factor = 10.0_f64.powi(dps as i32); @@ -58,6 +60,14 @@ impl Assign<&NativeFloat64> for NativeFloat64 { fn assign(&mut self, src: &NativeFloat64) { self.0 = src.0 } } +impl From for NativeFloat64 { + fn from(n: usize) -> Self { Self(n as ImplType) } +} + +impl From for NativeFloat64 { + fn from(n: f64) -> Self { Self(n as ImplType) } +} + impl Clone for NativeFloat64 { fn clone(&self) -> Self { Self(self.0) } } @@ -167,9 +177,7 @@ impl ops::SubAssign for NativeFloat64 { } impl ops::MulAssign for NativeFloat64 { - fn mul_assign(&mut self, _rhs: Self) { - todo!() - } + fn mul_assign(&mut self, rhs: Self) { self.0 *= rhs.0 } } impl ops::DivAssign for NativeFloat64 { diff --git a/src/numbers/rational_num.rs b/src/numbers/rational_num.rs index 13da822..d161368 100644 --- a/src/numbers/rational_num.rs +++ b/src/numbers/rational_num.rs @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -use super::{Assign, Number}; +use super::{Assign, From, Number}; use num_bigint::{BigInt, ParseBigIntError}; use num_rational::BigRational; // TODO: Can we do Ratio and combine with ibig? @@ -30,7 +30,9 @@ pub struct Rational(BigRational); impl Number for Rational { fn new() -> Self { Self(BigRational::zero()) } - fn from(n: usize) -> Self { Self(BigRational::from_integer(BigInt::from(n))) } + fn pow_assign(&mut self, exponent: i32) { + self.0 = self.0.pow(exponent); + } fn floor_mut(&mut self, dps: usize) { if dps == 0 { @@ -65,6 +67,17 @@ impl Assign<&Rational> for Rational { fn assign(&mut self, src: &Rational) { self.0 = src.0.clone() } } +impl From for Rational { + fn from(n: usize) -> Self { Self(BigRational::from_integer(BigInt::from(n))) } +} + +impl From for Rational { + fn from(n: f64) -> Self { + // FIXME: This is very broken! + return Self(BigRational::from_float(n).unwrap() * BigRational::from_integer(BigInt::from(100000)).round() / BigRational::from_integer(BigInt::from(100000))); + } +} + impl Clone for Rational { fn clone(&self) -> Self { Self(self.0.clone()) } } @@ -201,15 +214,11 @@ impl ops::SubAssign for Rational { } impl ops::MulAssign for Rational { - fn mul_assign(&mut self, _rhs: Self) { - todo!() - } + fn mul_assign(&mut self, rhs: Self) { self.0 *= rhs.0 } } impl ops::DivAssign for Rational { - fn div_assign(&mut self, rhs: Self) { - self.0 /= &rhs.0; - } + fn div_assign(&mut self, rhs: Self) { self.0 /= &rhs.0 } } impl ops::RemAssign for Rational { diff --git a/src/numbers/rational_rug.rs b/src/numbers/rational_rug.rs index 8c5fb5f..0411b3b 100644 --- a/src/numbers/rational_rug.rs +++ b/src/numbers/rational_rug.rs @@ -15,10 +15,10 @@ * along with this program. If not, see . */ -use super::{Assign, Number}; +use super::{Assign, From, Number}; use num_traits::{Num, One, Zero}; -use rug::{self, ops::Pow, rational::ParseRationalError}; +use rug::{self, ops::Pow, ops::PowAssign, rational::ParseRationalError}; use rug::Assign as RugAssign; use std::cmp::{Ord, Ordering, PartialEq, PartialOrd}; @@ -30,7 +30,9 @@ pub struct Rational(rug::Rational); impl Number for Rational { fn new() -> Self { Self(rug::Rational::new()) } - fn from(n: usize) -> Self { Self(rug::Rational::from(n)) } + fn pow_assign(&mut self, exponent: i32) { + self.0.pow_assign(exponent); + } fn floor_mut(&mut self, dps: usize) { if dps == 0 { @@ -63,6 +65,17 @@ impl Assign<&Rational> for Rational { fn assign(&mut self, src: &Rational) { self.0.assign(&src.0) } } +impl From for Rational { + fn from(n: usize) -> Self { Self(rug::Rational::from(n)) } +} + +impl From for Rational { + fn from(n: f64) -> Self { + // FIXME: This is very broken! + return Self((rug::Rational::from_f64(n).unwrap() * rug::Rational::from(100000)).round() / rug::Rational::from(100000)); + } +} + impl Clone for Rational { fn clone(&self) -> Self { Self(self.0.clone()) } } @@ -199,15 +212,11 @@ impl ops::SubAssign for Rational { } impl ops::MulAssign for Rational { - fn mul_assign(&mut self, _rhs: Self) { - todo!() - } + fn mul_assign(&mut self, rhs: Self) { self.0 *= rhs.0 } } impl ops::DivAssign for Rational { - fn div_assign(&mut self, rhs: Self) { - self.0 /= &rhs.0; - } + fn div_assign(&mut self, rhs: Self) { self.0 /= rhs.0 } } impl ops::RemAssign for Rational { @@ -225,9 +234,7 @@ impl ops::SubAssign<&Rational> for Rational { } impl ops::MulAssign<&Rational> for Rational { - fn mul_assign(&mut self, _rhs: &Rational) { - todo!() - } + fn mul_assign(&mut self, rhs: &Rational) { self.0 *= &rhs.0 } } impl ops::DivAssign<&Rational> for Rational { diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 2ab4d1f..863ec21 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -30,7 +30,10 @@ use std::ops; #[wasm_bindgen] pub struct STVOptions { + pub round_tvs: Option, + pub round_weights: Option, pub round_votes: Option, + pub round_quota: Option, pub surplus: SurplusMethod, pub surplus_order: SurplusOrder, pub transferable_only: bool, @@ -41,7 +44,10 @@ pub struct STVOptions { #[wasm_bindgen] impl STVOptions { pub fn new( + round_tvs: Option, + round_weights: Option, round_votes: Option, + round_quota: Option, surplus: &str, surplus_order: &str, transferable_only: bool, @@ -49,7 +55,10 @@ impl STVOptions { pp_decimals: usize, ) -> Self { return STVOptions { - round_votes: round_votes, + round_tvs, + round_weights, + round_votes, + round_quota, surplus: match surplus { "wig" => SurplusMethod::WIG, "uig" => SurplusMethod::UIG, @@ -62,14 +71,14 @@ impl STVOptions { "by_order" => SurplusOrder::ByOrder, _ => panic!("Invalid --surplus-order"), }, - transferable_only: transferable_only, + transferable_only, exclusion: match exclusion { "single_stage" => ExclusionMethod::SingleStage, "by_value" => ExclusionMethod::ByValue, "parcels_by_order" => ExclusionMethod::ParcelsByOrder, _ => panic!("Invalid --exclusion"), }, - pp_decimals: pp_decimals, + pp_decimals, }; } } @@ -250,9 +259,16 @@ fn calculate_quota(state: &mut CountState, opts: &STVOptions) { // TODO: Different quotas state.quota /= N::from(state.election.seats + 1); - // TODO: Different rounding rules - state.quota += N::one(); - state.quota.floor_mut(0); + // Increment to next available increment + if let Some(dps) = opts.round_quota { + let mut factor = N::from(10); + factor.pow_assign(dps as i32); + state.quota *= &factor; + state.quota.floor_mut(0); + state.quota += N::one(); + state.quota /= factor; + } + log.push_str(format!("{:.dps$}.", state.quota, dps=opts.pp_decimals).as_str()); state.logger.log_literal(log); @@ -299,7 +315,8 @@ where if has_surplus.len() > 0 { match opts.surplus_order { SurplusOrder::BySize => { - has_surplus.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); + // Compare b with a to sort high-low + has_surplus.sort_unstable_by(|a, b| b.1.votes.partial_cmp(&a.1.votes).unwrap()); } SurplusOrder::ByOrder => { has_surplus.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap()); @@ -340,19 +357,47 @@ where } } -fn reweight_vote(num_votes: &N, num_ballots: &N, surplus: &N, weighted: bool, transfer_denom: &Option) -> N { +fn reweight_vote( + num_votes: &N, + num_ballots: &N, + surplus: &N, + weighted: bool, + transfer_value: &Option, + transfer_denom: &Option, + round_tvs: Option, + rounding: Option) -> N +{ + let mut result; + match transfer_denom { Some(v) => { - if weighted { - return num_votes.clone() * surplus / v; + if let Some(_) = round_tvs { + // Rounding requested: use the rounded transfer value + if weighted { + result = num_votes.clone() * transfer_value.as_ref().unwrap(); + } else { + result = num_ballots.clone() * transfer_value.as_ref().unwrap(); + } } else { - return num_ballots.clone() * surplus / v; + // 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 => { - return num_votes.clone(); + result = num_votes.clone(); } } + + // Round down if requested + if let Some(dps) = rounding { + result.floor_mut(dps); + } + + return result; } fn distribute_surplus(state: &mut CountState, opts: &STVOptions, elected_candidate: &Candidate) @@ -395,13 +440,20 @@ where let transferable_votes = &result.total_votes - &result.exhausted.num_votes; let transfer_denom = calculate_transfer_denom(&surplus, &result, &transferable_votes, is_weighted, opts.transferable_only); - let transfer_value; + let mut transfer_value; match transfer_denom { Some(ref v) => { - transfer_value = surplus.clone() / v; - state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps$}.", elected_candidate.name, transfer_value, dps=opts.pp_decimals)); + transfer_value = Some(surplus.clone() / v); + + // Round down if requested + if let Some(dps) = opts.round_tvs { + transfer_value.as_mut().unwrap().floor_mut(dps); + } + + state.logger.log_literal(format!("Surplus of {} distributed at value {:.dps$}.", elected_candidate.name, transfer_value.as_ref().unwrap(), dps=opts.pp_decimals)); } None => { + transfer_value = None; state.logger.log_literal(format!("Surplus of {} distributed at values received.", elected_candidate.name)); } } @@ -413,17 +465,13 @@ where // Reweight votes for vote in parcel.iter_mut() { - vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &transfer_denom); + vote.value = reweight_vote(&vote.value, &vote.ballot.orig_value, &surplus, is_weighted, &transfer_value, &transfer_denom, opts.round_tvs, opts.round_weights); } let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); - let mut candidate_transfers = reweight_vote(&entry.num_votes, &entry.num_ballots, &surplus, is_weighted, &transfer_denom); - // Round transfers - if let Some(dps) = opts.round_votes { - candidate_transfers.floor_mut(dps); - } + let candidate_transfers = reweight_vote(&entry.num_votes, &entry.num_ballots, &surplus, is_weighted, &transfer_value, &transfer_denom, opts.round_tvs, opts.round_votes); count_card.transfer(&candidate_transfers); checksum += candidate_transfers; } @@ -441,7 +489,7 @@ where exhausted_transfers = &surplus - &transferable_votes; } } else { - exhausted_transfers = reweight_vote(&result.exhausted.num_votes, &result.exhausted.num_ballots, &surplus, is_weighted, &transfer_denom); + exhausted_transfers = reweight_vote(&result.exhausted.num_votes, &result.exhausted.num_ballots, &surplus, is_weighted, &transfer_value, &transfer_denom, opts.round_tvs, opts.round_votes); } if let Some(dps) = opts.round_votes { diff --git a/tests/aec.rs b/tests/aec.rs index d7a8bc5..ec5792c 100644 --- a/tests/aec.rs +++ b/tests/aec.rs @@ -15,15 +15,17 @@ * along with this program. If not, see . */ -use opentally::election::{CandidateState, CountState, Election}; -use opentally::numbers::{Number, Rational}; +mod utils; + +use opentally::election::Election; +use opentally::numbers::Rational; use opentally::stv; use csv::StringRecord; use flate2::bufread::GzDecoder; use std::fs::File; -use std::io::{self, BufRead}; +use std::io::{self, BufRead}; #[test] fn aec_tas19_rational() { @@ -38,6 +40,8 @@ fn aec_tas19_rational() { // Remove exhausted/LBF rows candidates.truncate(candidates.len() - 2); + // TODO: Validate candidate names + let stages: Vec = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect(); // Decompress BLT @@ -50,11 +54,11 @@ fn aec_tas19_rational() { // Read BLT let election: Election = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); - // TODO: Validate candidate names - - // Initialise options let stv_opts = stv::STVOptions { + round_tvs: None, + round_weights: None, round_votes: Some(0), + round_quota: Some(0), surplus: stv::SurplusMethod::UIG, surplus_order: stv::SurplusOrder::ByOrder, transferable_only: false, @@ -62,49 +66,5 @@ fn aec_tas19_rational() { pp_decimals: 2, }; - // Initialise count state - let mut state = CountState::new(&election); - - // Distribute first preferences - stv::count_init(&mut state, &stv_opts); - let mut stage_num = 1; - - for (idx, stage) in stages.into_iter().enumerate() { - while stage_num < stage { - // Step through stages - // Assert count not yet done - assert_eq!(stv::count_one_stage(&mut state, &stv_opts), false); - stage_num += 1; - } - validate_stage(idx, &state, &records); - } -} - -fn validate_stage(idx: usize, state: &CountState, records: &Vec) { - // Validate candidate votes - let mut candidate_votes: Vec = records.iter().skip(2).map(|r| N::parse(&r[idx*2 + 1])).collect(); - // Remove exhausted/LBF rows - candidate_votes.truncate(candidate_votes.len() - 2); - - for (candidate, votes) in state.election.candidates.iter().zip(candidate_votes) { - let count_card = state.candidates.get(candidate).unwrap(); - assert!(count_card.votes == votes); - } - - // Validate candidate states - let mut candidate_states: Vec<&str> = records.iter().skip(2).map(|r| &r[idx*2 + 2]).collect(); - candidate_states.truncate(candidate_states.len() - 2); - - for (candidate, candidate_state) in state.election.candidates.iter().zip(candidate_states) { - let count_card = state.candidates.get(candidate).unwrap(); - if candidate_state == "" { - assert!(count_card.state == CandidateState::HOPEFUL); - } else if candidate_state == "EL" || candidate_state == "PEL" { - assert!(count_card.state == CandidateState::ELECTED); - } else if candidate_state == "EX" || candidate_state == "EXCLUDING" { - assert!(count_card.state == CandidateState::EXCLUDED); - } else { - panic!("Unknown state descriptor {}", candidate_state); - } - } + utils::validate_election(stages, records, election, stv_opts); } diff --git a/tests/data/prsa1.blt b/tests/data/prsa1.blt new file mode 100644 index 0000000..fbc54ae --- /dev/null +++ b/tests/data/prsa1.blt @@ -0,0 +1,75 @@ +7 4 +1 1 3 0 +1 2 4 7 0 +1 2 6 1 7 0 +1 2 6 5 3 0 +1 3 0 +1 4 0 +1 3 0 +1 4 0 +1 4 0 +1 3 0 +1 5 7 0 +1 4 1 0 +1 6 0 +1 4 0 +1 2 3 0 +1 3 0 +1 7 0 +1 4 0 +1 2 4 7 0 +1 2 4 7 0 +1 4 0 +1 2 3 0 +1 4 0 +1 7 0 +1 2 6 0 +1 2 1 0 +1 7 0 +1 2 4 1 3 0 +1 4 0 +1 6 0 +1 6 0 +1 2 4 3 7 0 +1 6 0 +1 2 7 0 +1 2 7 0 +1 2 6 1 7 0 +1 3 0 +1 2 4 7 0 +1 2 6 0 +1 2 4 3 7 0 +1 2 6 0 +1 2 4 7 0 +1 4 0 +1 2 6 7 0 +1 2 6 1 0 +1 2 4 7 0 +1 6 0 +1 2 4 7 0 +1 2 3 0 +1 6 0 +1 2 6 5 3 0 +1 2 4 1 7 0 +1 6 0 +1 6 0 +1 2 1 0 +1 2 4 7 0 +1 2 6 5 1 7 0 +1 2 7 0 +1 2 4 3 7 0 +1 6 0 +1 2 3 0 +1 2 5 7 0 +1 2 4 5 1 3 0 +1 6 0 +1 6 0 +0 +"Evans" +"Grey" +"Thomson" +"Ames" +"Reid" +"Spears" +"White" +"1976 Committee of the Utopia Tennis Club" diff --git a/tests/data/prsa1.csv b/tests/data/prsa1.csv new file mode 100644 index 0000000..1ec24bf --- /dev/null +++ b/tests/data/prsa1.csv @@ -0,0 +1,11 @@ +Stage:,1,,2,,3,,4,,5,,6,,7,,8,,9,,10,,11,,12,,13,,14,,15,,16,,17, +Comment:,First preferences,,Surplus of Grey,,Surplus of Ames,,Surplus of Spears,,Exclusion of Reid,,Exclusion of Reid,,Exclusion of Reid,,Exclusion of Reid,,Exclusion of Evans,,Exclusion of Evans,,Exclusion of Evans,,Exclusion of Evans,,Exclusion of Evans,,Exclusion of Evans,,Exclusion of Thomson,,Exclusion of Thomson,,Exclusion of Thomson, +Evans,1,,2.234,,3.038,,4.823,,4.823,,4.823,,5.225,,5.82,,4.82,EX,3.586,EX,2.782,EX,0.997,EX,0.595,EX,0,EX,0,EX,0,EX,0,EX +Grey,34,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL +Thomson,5,,7.468,,8.674,,8.674,,8.674,,8.674,,8.674,,9.864,,10.864,,10.864,,11.266,,11.266,,11.668,,11.668,,6.668,EX,4.2,EX,2.994,EX +Ames,10,,18.638,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL +Reid,1,,1.617,,2.019,,3.804,,2.804,EX,2.187,EX,1.785,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX,0,EX +Spears,11,,17.17,EL,17.17,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL,13.001,EL +White,3,,4.851,,8.067,,8.662,,9.662,,10.279,,10.279,,10.279,,10.279,,10.279,,10.681,,11.871,,11.871,,12.466,,12.466,,12.466,,13.672,EL +Exhausted,0,,0,,0,,0,,0,,0,,0,,0,,0,,1.234,,1.234,,1.829,,1.829,,1.829,,6.829,,9.297,,9.297, +Loss to fraction,0,,0.021,,0.03,,0.034,,0.034,,0.034,,0.034,,0.034,,0.034,,0.034,,0.034,,0.034,,0.034,,0.034,,0.034,,0.034,,0.034, diff --git a/tests/data/prsa1.ods b/tests/data/prsa1.ods new file mode 100644 index 0000000000000000000000000000000000000000..09eb3fdf5faccd07238ce8f2f829a576bd37e962 GIT binary patch literal 13027 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 literal 0 HcmV?d00001 diff --git a/tests/prsa.rs b/tests/prsa.rs new file mode 100644 index 0000000..0cc78a2 --- /dev/null +++ b/tests/prsa.rs @@ -0,0 +1,37 @@ +/* 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 . + */ + +mod utils; + +use opentally::numbers::Rational; +use opentally::stv; + +#[test] +fn prsa1_rational() { + let stv_opts = stv::STVOptions { + round_tvs: Some(3), + round_weights: Some(3), + round_votes: Some(3), + round_quota: Some(3), + surplus: stv::SurplusMethod::EG, + surplus_order: stv::SurplusOrder::ByOrder, + transferable_only: true, + exclusion: stv::ExclusionMethod::ParcelsByOrder, + pp_decimals: 2, + }; + utils::read_validate_election::("tests/data/prsa1.csv", "tests/data/prsa1.blt", stv_opts); +} diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs new file mode 100644 index 0000000..bd0325b --- /dev/null +++ b/tests/utils/mod.rs @@ -0,0 +1,110 @@ +/* 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 . + */ + +use opentally::election::{CandidateState, CountState, Election}; +use opentally::numbers::{Number, Rational}; +use opentally::stv; + +use csv::StringRecord; + +use std::fs::File; +use std::io::{self, BufRead}; +use std::ops; + +#[allow(dead_code)] // Suppress false positive +pub fn read_validate_election(csv_file: &str, blt_file: &str, stv_opts: stv::STVOptions) { + // Read CSV file + let reader = csv::ReaderBuilder::new() + .has_headers(false) + .from_path(csv_file) + .expect("IO Error"); + let records: Vec = reader.into_records().map(|r| r.expect("Syntax Error")).collect(); + + let mut candidates: Vec<&str> = records.iter().skip(2).map(|r| &r[0]).collect(); + // Remove exhausted/LBF rows + candidates.truncate(candidates.len() - 2); + + // TODO: Validate candidate names + + let stages: Vec = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect(); + + // Decompress BLT + let file = File::open(blt_file).expect("IO Error"); + let file_reader = io::BufReader::new(file); + let lines = file_reader.lines(); + + // Read BLT + let election: Election = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); + + validate_election(stages, records, election, stv_opts); +} + +pub fn validate_election(stages: Vec, records: Vec, election: Election, stv_opts: stv::STVOptions) +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, +{ + // Initialise count state + let mut state = CountState::new(&election); + + // Distribute first preferences + stv::count_init(&mut state, &stv_opts); + let mut stage_num = 1; + + for (idx, stage) in stages.into_iter().enumerate() { + while stage_num < stage { + // Step through stages + // Assert count not yet done + assert_eq!(stv::count_one_stage(&mut state, &stv_opts), false); + stage_num += 1; + } + validate_stage(idx, &state, &records); + } +} + +fn validate_stage(idx: usize, state: &CountState, records: &Vec) { + println!("Col at idx {}", idx); + + // Validate candidate votes + //let mut candidate_votes: Vec = records.iter().skip(2).map(|r| N::parse(&r[idx*2 + 1])).collect(); + let mut candidate_votes: Vec = records.iter().skip(2).map(|r| N::from(r[idx*2 + 1].parse::().expect("Syntax Error"))).collect(); + // Remove exhausted/LBF rows + candidate_votes.truncate(candidate_votes.len() - 2); + + for (candidate, votes) in state.election.candidates.iter().zip(candidate_votes) { + let count_card = state.candidates.get(candidate).unwrap(); + assert!(count_card.votes == votes, "Failed to validate votes for candidate {}. Expected {:}, got {:}", candidate.name, votes, count_card.votes); + } + + // Validate candidate states + let mut candidate_states: Vec<&str> = records.iter().skip(2).map(|r| &r[idx*2 + 2]).collect(); + candidate_states.truncate(candidate_states.len() - 2); + + for (candidate, candidate_state) in state.election.candidates.iter().zip(candidate_states) { + let count_card = state.candidates.get(candidate).unwrap(); + if candidate_state == "" { + assert!(count_card.state == CandidateState::HOPEFUL); + } else if candidate_state == "EL" || candidate_state == "PEL" { + assert!(count_card.state == CandidateState::ELECTED); + } else if candidate_state == "EX" || candidate_state == "EXCLUDING" { + assert!(count_card.state == CandidateState::EXCLUDED); + } else { + panic!("Unknown state descriptor {}", candidate_state); + } + } +}