Implement --round-{tvs,weights,quota}
Add test case for PRSA example election
This commit is contained in:
parent
f1a730e885
commit
f6fba85049
15
src/main.rs
15
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<usize>,
|
||||
|
||||
/// Round ballot weights to specified decimal places
|
||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||
round_weights: Option<usize>,
|
||||
|
||||
/// Round votes to specified decimal places
|
||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||
round_votes: Option<usize>,
|
||||
|
||||
/// Round quota to specified decimal places
|
||||
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
|
||||
round_quota: Option<usize>,
|
||||
|
||||
// ------------------
|
||||
// -- 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,
|
||||
|
@ -33,11 +33,19 @@ pub trait Assign<Src=Self> {
|
||||
fn assign(&mut self, src: Src);
|
||||
}
|
||||
|
||||
//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<Output=Self> + Ord + Assign + Clone + fmt::Display where for<'a> Self: Assign<&'a Self> {
|
||||
fn new() -> Self;
|
||||
fn from(n: usize) -> Self;
|
||||
pub trait From<T> {
|
||||
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<Output=Self> + Ord + Assign + From<usize> + From<f64> + Clone + fmt::Display
|
||||
where
|
||||
for<'a> Self: Assign<&'a Self>
|
||||
{
|
||||
fn new() -> Self;
|
||||
|
||||
fn pow_assign(&mut self, exponent: i32);
|
||||
fn floor_mut(&mut self, dps: usize);
|
||||
|
||||
fn parse(s: &str) -> Self {
|
||||
|
@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<usize> for NativeFloat64 {
|
||||
fn from(n: usize) -> Self { Self(n as ImplType) }
|
||||
}
|
||||
|
||||
impl From<f64> 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 {
|
||||
|
@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::{Assign, Number};
|
||||
use super::{Assign, From, Number};
|
||||
|
||||
use num_bigint::{BigInt, ParseBigIntError};
|
||||
use num_rational::BigRational; // TODO: Can we do Ratio<IBig> 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<usize> for Rational {
|
||||
fn from(n: usize) -> Self { Self(BigRational::from_integer(BigInt::from(n))) }
|
||||
}
|
||||
|
||||
impl From<f64> 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 {
|
||||
|
@ -15,10 +15,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<usize> for Rational {
|
||||
fn from(n: usize) -> Self { Self(rug::Rational::from(n)) }
|
||||
}
|
||||
|
||||
impl From<f64> 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 {
|
||||
|
@ -30,7 +30,10 @@ use std::ops;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct STVOptions {
|
||||
pub round_tvs: Option<usize>,
|
||||
pub round_weights: Option<usize>,
|
||||
pub round_votes: Option<usize>,
|
||||
pub round_quota: Option<usize>,
|
||||
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<usize>,
|
||||
round_weights: Option<usize>,
|
||||
round_votes: Option<usize>,
|
||||
round_quota: Option<usize>,
|
||||
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<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||
// TODO: Different quotas
|
||||
state.quota /= N::from(state.election.seats + 1);
|
||||
|
||||
// TODO: Different rounding rules
|
||||
state.quota += N::one();
|
||||
// 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<N: Number>(num_votes: &N, num_ballots: &N, surplus: &N, weighted: bool, transfer_denom: &Option<N>) -> N {
|
||||
fn reweight_vote<N: Number>(
|
||||
num_votes: &N,
|
||||
num_ballots: &N,
|
||||
surplus: &N,
|
||||
weighted: bool,
|
||||
transfer_value: &Option<N>,
|
||||
transfer_denom: &Option<N>,
|
||||
round_tvs: Option<usize>,
|
||||
rounding: Option<usize>) -> N
|
||||
{
|
||||
let mut result;
|
||||
|
||||
match transfer_denom {
|
||||
Some(v) => {
|
||||
if let Some(_) = round_tvs {
|
||||
// Rounding requested: use the rounded transfer value
|
||||
if weighted {
|
||||
return num_votes.clone() * surplus / v;
|
||||
result = num_votes.clone() * transfer_value.as_ref().unwrap();
|
||||
} else {
|
||||
return num_ballots.clone() * surplus / v;
|
||||
result = num_ballots.clone() * transfer_value.as_ref().unwrap();
|
||||
}
|
||||
} else {
|
||||
// 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<N: Number>(state: &mut CountState<N>, 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 {
|
||||
|
60
tests/aec.rs
60
tests/aec.rs
@ -15,8 +15,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
@ -38,6 +40,8 @@ fn aec_tas19_rational() {
|
||||
// Remove exhausted/LBF 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();
|
||||
|
||||
// Decompress BLT
|
||||
@ -50,11 +54,11 @@ fn aec_tas19_rational() {
|
||||
// Read BLT
|
||||
let election: Election<Rational> = 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<N: Number>(idx: usize, state: &CountState<N>, records: &Vec<StringRecord>) {
|
||||
// Validate candidate votes
|
||||
let mut candidate_votes: Vec<N> = 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);
|
||||
}
|
||||
|
75
tests/data/prsa1.blt
Normal file
75
tests/data/prsa1.blt
Normal file
@ -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"
|
11
tests/data/prsa1.csv
Normal file
11
tests/data/prsa1.csv
Normal file
@ -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,
|
|
BIN
tests/data/prsa1.ods
Normal file
BIN
tests/data/prsa1.ods
Normal file
Binary file not shown.
37
tests/prsa.rs
Normal file
37
tests/prsa.rs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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::<Rational>("tests/data/prsa1.csv", "tests/data/prsa1.blt", stv_opts);
|
||||
}
|
110
tests/utils/mod.rs
Normal file
110
tests/utils/mod.rs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<N: Number>(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<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 exhausted/LBF 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();
|
||||
|
||||
// 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<Rational> = 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<N: Number>(stages: Vec<usize>, records: Vec<StringRecord>, election: Election<N>, 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<Output=N>,
|
||||
{
|
||||
// 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<N: Number>(idx: usize, state: &CountState<N>, records: &Vec<StringRecord>) {
|
||||
println!("Col at idx {}", idx);
|
||||
|
||||
// Validate candidate votes
|
||||
//let mut candidate_votes: Vec<N> = records.iter().skip(2).map(|r| N::parse(&r[idx*2 + 1])).collect();
|
||||
let mut candidate_votes: Vec<N> = records.iter().skip(2).map(|r| N::from(r[idx*2 + 1].parse::<f64>().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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user