diff --git a/src/numbers/fixed.rs b/src/numbers/fixed.rs index 01bf8c9..013f951 100644 --- a/src/numbers/fixed.rs +++ b/src/numbers/fixed.rs @@ -38,7 +38,7 @@ fn get_factor() -> &'static IBig { } /// Fixed-point number -#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct Fixed(IBig); impl Fixed { diff --git a/src/numbers/gfixed.rs b/src/numbers/gfixed.rs index 22d0e54..45ab786 100644 --- a/src/numbers/gfixed.rs +++ b/src/numbers/gfixed.rs @@ -44,7 +44,7 @@ fn get_factor_cmp() -> &'static IBig { } /// Guarded fixed-point number -#[derive(Clone, Eq)] +#[derive(Clone, Debug, Eq)] pub struct GuardedFixed(IBig); impl GuardedFixed { diff --git a/src/numbers/mod.rs b/src/numbers/mod.rs index c1e115f..37d317e 100644 --- a/src/numbers/mod.rs +++ b/src/numbers/mod.rs @@ -45,7 +45,7 @@ pub trait Assign { /// 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 + Ord + Assign + From + From + Clone + fmt::Display + NumRef + NumAssignRef + ops::Neg + Ord + Assign + From + From + Clone + fmt::Debug + fmt::Display where for<'a> Self: Assign<&'a Self> { diff --git a/src/numbers/native.rs b/src/numbers/native.rs index e036db5..e6de00f 100644 --- a/src/numbers/native.rs +++ b/src/numbers/native.rs @@ -27,7 +27,7 @@ use std::ops; type ImplType = f64; /// Native 64-bit floating-point number -#[derive(Clone, Display, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Display, PartialEq, PartialOrd)] pub struct NativeFloat64(ImplType); impl Number for NativeFloat64 { diff --git a/src/numbers/rational_num.rs b/src/numbers/rational_num.rs index 23e6c8d..69438a2 100644 --- a/src/numbers/rational_num.rs +++ b/src/numbers/rational_num.rs @@ -26,7 +26,7 @@ use std::ops; type RatioBase = num_bigint::BigInt; type RatioType = num_rational::BigRational; -#[derive(Clone, PartialEq, PartialOrd)] +#[derive(Clone, Debug, PartialEq, PartialOrd)] pub struct Rational(RatioType); impl Number for Rational { diff --git a/src/numbers/rational_rug.rs b/src/numbers/rational_rug.rs index 9252137..513a996 100644 --- a/src/numbers/rational_rug.rs +++ b/src/numbers/rational_rug.rs @@ -26,7 +26,7 @@ use std::fmt; use std::ops; /// Rational number -#[derive(Clone, PartialEq, PartialOrd)] +#[derive(Clone, Debug, PartialEq, PartialOrd)] pub struct Rational(rug::Rational); impl Number for Rational { diff --git a/tests/aec.rs b/tests/aec.rs index f5e4080..271dfe6 100644 --- a/tests/aec.rs +++ b/tests/aec.rs @@ -73,6 +73,5 @@ fn aec_tas19_rational() { defer_surpluses: false, pp_decimals: 2, }; - - utils::validate_election(stages, records, election, stv_opts); + utils::validate_election(stages, records, election, stv_opts, None, &["exhausted", "lbf"]); } diff --git a/tests/data/ers97_meek.csv b/tests/data/ers97_meek.csv new file mode 100644 index 0000000..b25c65c --- /dev/null +++ b/tests/data/ers97_meek.csv @@ -0,0 +1,15 @@ +Stage:,1,,2,,4,,6,,9,,11, +Comment:,First preferences,,Surpluses distributed,,Surpluses distributed,,Surpluses distributed,,Surpluses distributed,,Surpluses distributed, +Smith,134,EL,107.26,EL,106.96,EL,104.42,EL,101.58,EL,73,EL +Carpenter,81,,87.98,,88.47,,97.71,,101.58,EL,73,EL +Wright,27,,31.99,,32.34,,34.97,,0,EX,0,EX +Glazier,24,,30.19,,30.62,,0,EX,0,EX,0,EX +Duke,105,,106.6,,106.96,EL,104.42,EL,101.58,EL,73,EL +Prince,91,,91,,91,,92.45,,97.07,,73,EL +Baron,64,,64,,64,,64.03,,67.24,,0,EX +Abbot,59,,59.8,,64.85,,66,,68.92,,70.83, +Vicar,55,,55,,69.21,,70.99,,73.27,,75.16,EL +Monk,23,,23.4,,0,EX,0,EX,0,EX,0,EX +Freeman,90,,93.59,,94.27,,95.97,,99.81,,73,EL +Exhausted,0,,2.2,,4.31,,22.05,,41.95,,242.01, +Quota,107.57,,107.26,,106.96,,104.42,,101.58,,73, diff --git a/tests/data/ers97_meek.ods b/tests/data/ers97_meek.ods new file mode 100644 index 0000000..31940a0 Binary files /dev/null and b/tests/data/ers97_meek.ods differ diff --git a/tests/ers97.rs b/tests/ers97.rs index 9a93396..e94620c 100644 --- a/tests/ers97.rs +++ b/tests/ers97.rs @@ -17,15 +17,9 @@ mod utils; -use opentally::election::{CandidateState, CountState, Election}; use opentally::numbers::Rational; use opentally::stv; -use csv::StringRecord; - -use std::fs::File; -use std::io::{self, BufRead}; - #[test] fn ers97_rational() { let stv_opts = stv::STVOptions { @@ -47,92 +41,5 @@ fn ers97_rational() { defer_surpluses: true, pp_decimals: 2, }; - - // --------------------------------------------- - // Custom implementation due to nontransferables - // and vote required for election - - // Read CSV file - let reader = csv::ReaderBuilder::new() - .has_headers(false) - .from_path("tests/data/ers97.csv") - .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 NT/VRE 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(); - - // Read BLT - let file = File::open("tests/data/ers97.blt").expect("IO Error"); - let file_reader = io::BufReader::new(file); - let lines = file_reader.lines(); - - let election: Election = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); - - // 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).unwrap(), false); - stage_num += 1; - } - - println!("Col at idx {}", idx); - - let mut candidate_votes: Vec> = records.iter().skip(2) - .map(|r| - if r[idx*2 + 1].len() > 0 { - Some(Rational::from(r[idx*2 + 1].parse::().expect("Syntax Error"))) - } else { - None - }) - .collect(); - - // Validate NT/VRE - let vre_votes = candidate_votes.pop().unwrap(); - let nt_votes = candidate_votes.pop().unwrap(); - - assert!(&state.exhausted.votes + &state.loss_fraction.votes == nt_votes.unwrap()); - if let Some(v) = vre_votes { - assert!(state.vote_required_election.as_ref().unwrap() == &v); - } - - // Remove NT/VRE rows - candidate_votes.truncate(candidate_votes.len() - 2); - - // Validate candidate votes - for (candidate, votes) in state.election.candidates.iter().zip(candidate_votes) { - let count_card = state.candidates.get(candidate).unwrap(); - assert!(&count_card.votes == votes.as_ref().unwrap(), "Failed to validate votes for candidate {}. Expected {:}, got {:}", candidate.name, votes.unwrap(), 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); - } - } - } + utils::read_validate_election::("tests/data/ers97.csv", "tests/data/ers97.blt", stv_opts, None, &["nt", "vre"]); } diff --git a/tests/meek.rs b/tests/meek.rs new file mode 100644 index 0000000..7e84da2 --- /dev/null +++ b/tests/meek.rs @@ -0,0 +1,45 @@ +/* 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::NativeFloat64; +use opentally::stv; + +#[test] +fn meek_ers97_float64() { + let stv_opts = stv::STVOptions { + round_tvs: None, + round_weights: None, + round_votes: None, + round_quota: None, + sum_surplus_transfers: stv::SumSurplusTransfersMode::SingleStep, + normalise_ballots: false, + quota: stv::QuotaType::DroopExact, + quota_criterion: stv::QuotaCriterion::GreaterOrEqual, + quota_mode: stv::QuotaMode::Static, + ties: vec![], + surplus: stv::SurplusMethod::Meek, + surplus_order: stv::SurplusOrder::BySize, + transferable_only: false, + exclusion: stv::ExclusionMethod::SingleStage, + bulk_exclude: false, + defer_surpluses: false, + pp_decimals: 2, + }; + utils::read_validate_election::("tests/data/ers97_meek.csv", "tests/data/ers97.blt", stv_opts, Some(2), &["exhausted", "quota"]); +} diff --git a/tests/prsa.rs b/tests/prsa.rs index 7da42ba..2316417 100644 --- a/tests/prsa.rs +++ b/tests/prsa.rs @@ -41,5 +41,5 @@ fn prsa1_rational() { defer_surpluses: false, pp_decimals: 2, }; - utils::read_validate_election::("tests/data/prsa1.csv", "tests/data/prsa1.blt", stv_opts); + utils::read_validate_election::("tests/data/prsa1.csv", "tests/data/prsa1.blt", stv_opts, None, &["exhausted", "lbf"]); } diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 5f86777..9da9522 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -16,7 +16,7 @@ */ use opentally::election::{CandidateState, CountState, Election}; -use opentally::numbers::{Number, Rational}; +use opentally::numbers::Number; use opentally::stv; use csv::StringRecord; @@ -26,7 +26,14 @@ 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) { +pub fn read_validate_election(csv_file: &str, blt_file: &str, stv_opts: stv::STVOptions, cmp_dps: Option, sum_rows: &[&str]) +where + for<'r> &'r N: ops::Add<&'r N, Output=N>, + for<'r> &'r N: ops::Sub<&'r N, Output=N>, + for<'r> &'r N: ops::Mul<&'r N, Output=N>, + for<'r> &'r N: ops::Div<&'r N, Output=N>, + for<'r> &'r N: ops::Neg, +{ // Read CSV file let reader = csv::ReaderBuilder::new() .has_headers(false) @@ -47,18 +54,23 @@ pub fn read_validate_election(csv_file: &str, blt_file: &str, stv_opt let file_reader = io::BufReader::new(file); let lines = file_reader.lines(); - let election: Election = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); + let election: Election = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); - validate_election(stages, records, election, stv_opts); + validate_election(stages, records, election, stv_opts, cmp_dps, sum_rows); } -pub fn validate_election(stages: Vec, records: Vec, election: Election, stv_opts: stv::STVOptions) +pub fn validate_election(stages: Vec, records: Vec, mut election: Election, stv_opts: stv::STVOptions, cmp_dps: Option, sum_rows: &[&str]) where + for<'r> &'r N: ops::Add<&'r N, Output=N>, for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Mul<&'r N, Output=N>, for<'r> &'r N: ops::Div<&'r N, Output=N>, for<'r> &'r N: ops::Neg, { + if stv_opts.normalise_ballots { + election.normalise_ballots(); + } + // Initialise count state let mut state = CountState::new(&election); @@ -73,29 +85,38 @@ where assert_eq!(stv::count_one_stage(&mut state, &stv_opts).unwrap(), false); stage_num += 1; } - validate_stage(idx, &state, &records); + validate_stage(idx, &state, &records, cmp_dps, sum_rows); } } -fn validate_stage(idx: usize, state: &CountState, records: &Vec) { - println!("Col at idx {}", idx); - - let mut candidate_votes: Vec = records.iter().skip(2).map(|r| N::from(r[idx*2 + 1].parse::().expect("Syntax Error"))).collect(); +fn validate_stage(idx: usize, state: &CountState, records: &Vec, cmp_dps: Option, sum_rows: &[&str]) +where + for<'r> &'r N: ops::Add<&'r N, Output=N>, +{ + let mut candidate_votes: Vec> = records.iter().skip(2) + .map(|r| if r[idx*2 + 1].len() > 0 { Some(N::from(r[idx*2 + 1].parse::().expect("Syntax Error"))) } else { None }) + .collect(); // Validate exhausted/LBF - let lbf_votes = candidate_votes.pop().unwrap(); - let exhausted_votes = candidate_votes.pop().unwrap(); - - assert!(state.exhausted.votes == exhausted_votes); - assert!(state.loss_fraction.votes == lbf_votes); - - // Remove exhausted/LBF rows - candidate_votes.truncate(candidate_votes.len() - 2); + for kind in sum_rows.iter().rev() { + let votes = candidate_votes.pop().unwrap_or(None); + if let Some(votes) = votes { + match kind { + &"exhausted" => approx_eq(&state.exhausted.votes, &votes, cmp_dps, idx, "exhausted votes"), + &"lbf" => approx_eq(&state.loss_fraction.votes, &votes, cmp_dps, idx, "LBF"), + &"nt" => approx_eq(&(&state.exhausted.votes + &state.loss_fraction.votes), &votes, cmp_dps, idx, "NTs"), + &"quota" => approx_eq(state.quota.as_ref().unwrap(), &votes, cmp_dps, idx, "quota"), + &"vre" => approx_eq(state.vote_required_election.as_ref().unwrap(), &votes, cmp_dps, idx, "VRE"), + _ => panic!("Unknown sum_rows"), + } + } + } // Validate candidate votes 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); + let votes = votes.unwrap(); + approx_eq(&count_card.votes, &votes, cmp_dps, idx, &format!("votes for candidate {}", candidate.name)); } // Validate candidate states @@ -115,3 +136,17 @@ fn validate_stage(idx: usize, state: &CountState, records: &Vec(n1: &N, n2: &N, cmp_dps: Option, idx: usize, description: &str) { + match cmp_dps { + Some(dps) => { + let s1 = format!("{:.dps$}", n1, dps=dps); + let s2 = format!("{:.dps$}", n2, dps=dps); + assert!(s1 == s2, "Failed to verify {} for idx {}. Expected {}, got {}", description, idx, s2, s1); + } + None => { + assert!(n1 == n2, "Failed to verify {} for idx {}. Expected {}, got {}", description, idx, n2, n1); + } + } + +}