Add ERS97 test case

This commit is contained in:
RunasSudo 2021-06-09 20:09:20 +10:00
parent a714ba6a90
commit 7ef5ae99b6
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
9 changed files with 216 additions and 24 deletions

View File

@ -274,9 +274,7 @@ impl ops::Neg for &Fixed {
impl ops::Add<Self> for &Fixed {
type Output = Fixed;
fn add(self, _rhs: Self) -> Self::Output {
todo!()
}
fn add(self, rhs: Self) -> Self::Output { Fixed(&self.0 + &rhs.0) }
}
impl ops::Sub<Self> for &Fixed {

View File

@ -207,31 +207,29 @@ impl ops::Neg for &NativeFloat64 {
fn neg(self) -> Self::Output { NativeFloat64(-&self.0) }
}
impl ops::Add<&NativeFloat64> for &NativeFloat64 {
impl ops::Add<Self> for &NativeFloat64 {
type Output = NativeFloat64;
fn add(self, _rhs: &NativeFloat64) -> Self::Output {
todo!()
}
fn add(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 + &rhs.0) }
}
impl ops::Sub<&NativeFloat64> for &NativeFloat64 {
impl ops::Sub<Self> for &NativeFloat64 {
type Output = NativeFloat64;
fn sub(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 - &rhs.0) }
}
impl ops::Mul<&NativeFloat64> for &NativeFloat64 {
impl ops::Mul<Self> for &NativeFloat64 {
type Output = NativeFloat64;
fn mul(self, _rhs: &NativeFloat64) -> Self::Output {
todo!()
}
}
impl ops::Div<&NativeFloat64> for &NativeFloat64 {
impl ops::Div<Self> for &NativeFloat64 {
type Output = NativeFloat64;
fn div(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 / &rhs.0) }
}
impl ops::Rem<&NativeFloat64> for &NativeFloat64 {
impl ops::Rem<Self> for &NativeFloat64 {
type Output = NativeFloat64;
fn rem(self, _rhs: &NativeFloat64) -> Self::Output {
todo!()

View File

@ -254,31 +254,29 @@ impl ops::Neg for &Rational {
fn neg(self) -> Self::Output { Rational(-&self.0) }
}
impl ops::Add<&Rational> for &Rational {
impl ops::Add<Self> for &Rational {
type Output = Rational;
fn add(self, _rhs: &Rational) -> Self::Output {
todo!()
}
fn add(self, rhs: &Rational) -> Self::Output { Rational(&self.0 + &rhs.0) }
}
impl ops::Sub<&Rational> for &Rational {
impl ops::Sub<Self> for &Rational {
type Output = Rational;
fn sub(self, rhs: &Rational) -> Self::Output { Rational(&self.0 - &rhs.0) }
}
impl ops::Mul<&Rational> for &Rational {
impl ops::Mul<Self> for &Rational {
type Output = Rational;
fn mul(self, _rhs: &Rational) -> Self::Output {
todo!()
}
}
impl ops::Div<&Rational> for &Rational {
impl ops::Div<Self> for &Rational {
type Output = Rational;
fn div(self, rhs: &Rational) -> Self::Output { Rational(&self.0 / &rhs.0) }
}
impl ops::Rem<&Rational> for &Rational {
impl ops::Rem<Self> for &Rational {
type Output = Rational;
fn rem(self, _rhs: &Rational) -> Self::Output {
todo!()

View File

@ -254,9 +254,7 @@ impl ops::Neg for &Rational {
impl ops::Add<Self> for &Rational {
type Output = Rational;
fn add(self, _rhs: Self) -> Self::Output {
todo!()
}
fn add(self, rhs: Self) -> Self::Output { Rational(rug::Rational::from(&self.0 + &rhs.0)) }
}
impl ops::Sub<Self> for &Rational {

51
tests/data/ers97.blt Normal file
View File

@ -0,0 +1,51 @@
11 6
35 1 2
3 1 3 9
11 1 3 6
5 1 3 7
6 1 3 8
20 1 4 0
7 1 4 9
4 1 4 11
8 1 5
4 1 8
1 1 10 5 7
1 1 10 9
18 1 11
11 1 0
81 2
23 3 1 2 0
2 3 1 2 7
1 3 1 2 8
1 3 1 0
3 4 1 3 7
1 4 8
11 4 5 6
3 4 2 0
5 4 2 11
1 4 3 0
105 5
91 6
64 7
5 8
42 8 9
12 8 7
55 9
2 10 5
5 10 8
14 10 9
2 10 1 11
90 11
0
"Smith"
"Carpenter"
"Wright"
"Glazier"
"Duke"
"Prince"
"Baron"
"Abbot"
"Vicar"
"Monk"
"Freeman"
"ERS97 Model Election"

15
tests/data/ers97.csv Normal file
View File

@ -0,0 +1,15 @@
Stage:,1,,2,,3,,4,,5,,6,,7,,8,
Comment:,First preferences,,Surplus of Smith,,Exclusion of Monk,,Exclusion of Monk,,"Exclusion of Glazier, Wright",,"Exclusion of Glazier, Wright",,Surplus of Carpenter,,Exclusion of Abbot,
Smith,134,PEL,107.58,EL,107.58,EL,107.58,EL,107.58,EL,107.58,EL,107.58,EL,107.58,EL
Carpenter,81,,88.35,,88.35,,88.35,,122.35,PEL,122.35,PEL,107.58,EL,107.58,EL
Wright,27,,32.25,,32.25,,32.25,,5.25,EXCLUDING,0,EX,0,EX,0,EX
Glazier,24,,30.51,,30.51,,30.51,,6.51,EXCLUDING,0,EX,0,EX,0,EX
Duke,105,,106.68,,108.68,PEL,108.68,PEL,108.68,PEL,108.68,PEL,108.68,PEL,108.68,PEL
Prince,91,,91,,91,,91,,102,,104.31,,104.31,PEL,104.31,PEL
Baron,64,,64,,64,,64.21,,67.21,,68.26,,70.26,,82.26,
Abbot,59,,59.84,,64.84,,64.84,,65.84,,67.1,,68.1,,2.1,EXCLUDING
Vicar,55,,55,,69,,69.21,,69.21,,71.31,,71.31,,113.31,PEL
Monk,23,,23.42,,0.42,EXCLUDING,0,EX,0,EX,0,EX,0,EX,0,EX
Freeman,90,,93.78,,95.78,,95.78,,95.78,,96.62,,101.62,,101.62,PEL
Non-transferable,0,,0.59,,0.59,,0.59,,2.59,,6.79,,13.56,,25.56,
Votes required,,,,,,,,,,,105.87,,104.13,,96.41,
1 Stage: 1 2 3 4 5 6 7 8
2 Comment: First preferences Surplus of Smith Exclusion of Monk Exclusion of Monk Exclusion of Glazier, Wright Exclusion of Glazier, Wright Surplus of Carpenter Exclusion of Abbot
3 Smith 134 PEL 107.58 EL 107.58 EL 107.58 EL 107.58 EL 107.58 EL 107.58 EL 107.58 EL
4 Carpenter 81 88.35 88.35 88.35 122.35 PEL 122.35 PEL 107.58 EL 107.58 EL
5 Wright 27 32.25 32.25 32.25 5.25 EXCLUDING 0 EX 0 EX 0 EX
6 Glazier 24 30.51 30.51 30.51 6.51 EXCLUDING 0 EX 0 EX 0 EX
7 Duke 105 106.68 108.68 PEL 108.68 PEL 108.68 PEL 108.68 PEL 108.68 PEL 108.68 PEL
8 Prince 91 91 91 91 102 104.31 104.31 PEL 104.31 PEL
9 Baron 64 64 64 64.21 67.21 68.26 70.26 82.26
10 Abbot 59 59.84 64.84 64.84 65.84 67.1 68.1 2.1 EXCLUDING
11 Vicar 55 55 69 69.21 69.21 71.31 71.31 113.31 PEL
12 Monk 23 23.42 0.42 EXCLUDING 0 EX 0 EX 0 EX 0 EX 0 EX
13 Freeman 90 93.78 95.78 95.78 95.78 96.62 101.62 101.62 PEL
14 Non-transferable 0 0.59 0.59 0.59 2.59 6.79 13.56 25.56
15 Votes required 105.87 104.13 96.41

BIN
tests/data/ers97.ods Normal file

Binary file not shown.

135
tests/ers97.rs Normal file
View File

@ -0,0 +1,135 @@
/* 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/>.
*/
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 {
round_tvs: Some(2),
round_weights: Some(2),
round_votes: Some(2),
round_quota: Some(2),
quota: stv::QuotaType::DroopExact,
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
quota_mode: stv::QuotaMode::ERS97,
surplus: stv::SurplusMethod::EG,
surplus_order: stv::SurplusOrder::BySize,
transferable_only: true,
exclusion: stv::ExclusionMethod::ByValue,
bulk_exclude: true,
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<StringRecord> = 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<usize> = 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<Rational> = 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), false);
stage_num += 1;
}
println!("Col at idx {}", idx);
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")))
} 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);
}
}
}
}

View File

@ -42,12 +42,11 @@ pub fn read_validate_election<N: Number>(csv_file: &str, blt_file: &str, stv_opt
let stages: Vec<usize> = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect();
// Decompress BLT
// Read 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<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
validate_election(stages, records, election, stv_opts);