OpenTally/tests/tests_impl/scotland.rs

163 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* OpenTally: Open-source election vote counting
* Copyright © 20212022 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 opentally::election::{CandidateState, CountState, Election};
use opentally::numbers::{Fixed, GuardedFixed, Number};
use opentally::parser::blt;
use opentally::stv;
use xmltree::Element;
use std::fs::File;
use std::ops;
#[test]
fn scotland_linn07_fixed5() {
let stv_opts = stv::STVOptionsBuilder::default()
.round_surplus_fractions(Some(5))
//.round_values(Some(5))
//.round_votes(Some(5))
.round_quota(Some(0))
.round_subtransfers(stv::RoundSubtransfersMode::PerBallot)
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
.early_bulk_elect(false)
.pp_decimals(5)
.build().unwrap();
Fixed::set_dps(5);
assert_eq!(stv_opts.describe::<Fixed>(), "--numbers fixed --decimals 5 --round-surplus-fractions 5 --round-quota 0 --round-subtransfers per_ballot --quota-criterion geq --no-early-bulk-elect --pp-decimals 5");
scotland_linn07::<Fixed>(stv_opts);
}
#[test]
fn scotland_linn07_gfixed5() {
let stv_opts = stv::STVOptionsBuilder::default()
.round_surplus_fractions(Some(5))
.round_values(Some(5)) // Must specify rounding as guarded decimals represented to 10 dps internally
.round_votes(Some(5))
.round_quota(Some(0))
.round_subtransfers(stv::RoundSubtransfersMode::PerBallot)
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
.early_bulk_elect(false)
.pp_decimals(5)
.build().unwrap();
GuardedFixed::set_dps(5);
assert_eq!(stv_opts.describe::<GuardedFixed>(), "--numbers gfixed --decimals 5 --round-surplus-fractions 5 --round-values 5 --round-votes 5 --round-quota 0 --round-subtransfers per_ballot --quota-criterion geq --no-early-bulk-elect --pp-decimals 5");
scotland_linn07::<GuardedFixed>(stv_opts);
}
fn scotland_linn07<N: Number>(stv_opts: stv::STVOptions)
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<Output=N>,
{
// Read XML file
let file = File::open("tests/data/linn07.xml").expect("IO Error");
let root = Element::parse(file).expect("Parse Error");
let mut candidates: Vec<&Element> = root.children.iter()
.filter_map(|n| match n {
xmltree::XMLNode::Element(e) => if e.name == "candidate" { Some(e) } else { None },
_ => None,
})
.collect();
let cand_nt = candidates.pop().unwrap();
// TODO: Validate candidate names
let num_stages = root.get_child("headerrow").expect("Syntax Error").children.len();
// Read BLT
let mut election: Election<N> = blt::parse_path("tests/data/linn07.blt").expect("Syntax Error");
// !!! FOR SCOTTISH STV !!!
election.normalise_ballots();
// Initialise count state
let mut state = CountState::new(&election);
// Distribute first preferences
stv::count_init(&mut state, &stv_opts).unwrap();
let mut stage_num = 1;
for i in 0..num_stages {
println!("Stage {}", stage_num);
// Validate NT
let nt_votes = get_cand_stage(cand_nt, i)
.get_child("value").unwrap()
.get_text().unwrap()
.to_string();
assert!((&state.exhausted.votes + &state.loss_fraction.votes) == parse_str(&nt_votes), "Failed to validate NTs. Expected {}, got {}", nt_votes, &state.exhausted.votes + &state.loss_fraction.votes);
for (candidate, cand_xml) in state.election.candidates.iter().zip(candidates.iter()) {
let count_card = state.candidates.get(candidate).unwrap();
// Validate candidate votes
let cand_votes = get_cand_stage(cand_xml, i)
.get_child("value").unwrap()
.get_text().unwrap()
.to_string();
let cand_votes = parse_str(&cand_votes);
assert!(count_card.votes == cand_votes, "Failed to validate votes for candidate {}. Expected {}, got {}", candidate.name, cand_votes, count_card.votes);
// Validate candidate states
let cand_state = get_cand_stage(cand_xml, i)
.get_child("status").unwrap()
.get_text().unwrap()
.to_string();
if cand_state == "Continuing" {
assert!(count_card.state == CandidateState::Hopeful);
} else if cand_state == "Elected" {
assert!(count_card.state == CandidateState::Elected);
} else if cand_state == "Excluded" {
assert!(count_card.state == CandidateState::Excluded);
} else {
panic!("Unknown state descriptor {}", cand_state);
}
}
assert_eq!(stv::count_one_stage(&mut state, &stv_opts).unwrap(), false);
stage_num += 1;
}
assert_eq!(stv::count_one_stage(&mut state, &stv_opts).unwrap(), true);
}
fn get_cand_stage(candidate: &Element, idx: usize) -> &Element {
return candidate.children.iter()
.filter_map(|n| match n {
xmltree::XMLNode::Element(e) => if e.name == "stage" { Some(e) } else { None },
_ => None,
})
.nth(idx).unwrap();
}
fn parse_str<N: Number>(s: &str) -> N {
if s == "-" { return N::zero(); }
return N::parse(s);
}