From 27ead09960c6dc88a62e14badd2aca4a0a92e9d3 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Thu, 2 Sep 2021 22:35:10 +1000 Subject: [PATCH] Complete BLT writer and implement tests for file conversions --- src/cli/convert.rs | 14 ++++----- src/constraints.rs | 6 ++-- src/election.rs | 4 ++- src/parser/bin.rs | 5 +++ src/writer/blt.rs | 5 ++- tests/convert.rs | 57 ++++++++++++++++++++++++++++++++++ tests/data/ers97_wd.blt | 69 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 tests/convert.rs create mode 100644 tests/data/ers97_wd.blt diff --git a/src/cli/convert.rs b/src/cli/convert.rs index be2f0eb..bc4a1f3 100644 --- a/src/cli/convert.rs +++ b/src/cli/convert.rs @@ -29,19 +29,19 @@ use std::fs::File; #[clap(setting=AppSettings::DeriveDisplayOrder)] pub struct SubcmdOptions { /// Path to the input data file - #[clap(help_heading=Some("INPUT/OUTPUT"))] + #[clap(help_heading=Some("INPUT"))] infile: String, - /// Path to the output data file - #[clap(help_heading=Some("INPUT/OUTPUT"))] - outfile: String, - /// Format of input file - #[clap(help_heading=Some("INPUT/OUTPUT"), short, long, possible_values=&["bin", "blt", "csp"], value_name="format")] + #[clap(help_heading=Some("INPUT"), short, long, possible_values=&["bin", "blt", "csp"], value_name="format")] r#in: Option, + /// Path to the output data file + #[clap(help_heading=Some("OUTPUT"))] + outfile: String, + /// Format of output file - #[clap(help_heading=Some("INPUT/OUTPUT"), short, long, possible_values=&["bin", "blt", "csp"], value_name="format")] + #[clap(help_heading=Some("OUTPUT"), short, long, possible_values=&["bin", "blt", "csp"], value_name="format")] out: Option, /// Number of seats diff --git a/src/constraints.rs b/src/constraints.rs index ae041bf..edf9080 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -30,7 +30,7 @@ use std::fmt; use std::ops; /// Constraints for an [crate::election::Election] -#[derive(Debug)] +#[derive(Clone, Debug)] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] pub struct Constraints(pub Vec); @@ -124,7 +124,7 @@ impl Constraints { } /// A single dimension of constraint -#[derive(Debug)] +#[derive(Clone, Debug)] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] pub struct Constraint { /// Name of this constraint @@ -134,7 +134,7 @@ pub struct Constraint { } /// A group of candidates, of which a certain minimum and maximum must be elected -#[derive(Debug)] +#[derive(Clone, Debug)] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] pub struct ConstrainedGroup { /// Name of this group diff --git a/src/election.rs b/src/election.rs index 196679e..3e1a895 100644 --- a/src/election.rs +++ b/src/election.rs @@ -28,6 +28,7 @@ use std::cmp::max; use std::collections::HashMap; /// An election to be counted +#[derive(Clone)] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] pub struct Election { /// Name of the election @@ -65,7 +66,7 @@ impl Election { } /// A candidate in an [Election] -#[derive(PartialEq, Eq, Hash)] +#[derive(Clone, Eq, Hash, PartialEq)] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] pub struct Candidate { /// Name of the candidate @@ -368,6 +369,7 @@ pub struct Vote<'a, N> { } /// A record of a voter's preferences +#[derive(Clone)] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] pub struct Ballot { /// Original value/weight of the ballot diff --git a/src/parser/bin.rs b/src/parser/bin.rs index 02705ed..15f3be5 100644 --- a/src/parser/bin.rs +++ b/src/parser/bin.rs @@ -26,6 +26,11 @@ use std::path::Path; /// Parse the given BIN file pub fn parse_path, N: Number>(path: P) -> Election { let content = fs::read(path).expect("IO Error"); + return parse_bytes(&content); +} + +/// Parse the given BIN file +pub fn parse_bytes(content: &[u8]) -> Election { let archived = unsafe { archived_root::>(&content) }; diff --git a/src/writer/blt.rs b/src/writer/blt.rs index 8baf735..c8e042e 100644 --- a/src/writer/blt.rs +++ b/src/writer/blt.rs @@ -18,6 +18,8 @@ use crate::election::Election; use crate::numbers::Number; +use itertools::Itertools; + use std::io::{BufWriter, Write}; /// Write the [Election] into BLT format @@ -29,7 +31,8 @@ pub fn write(election: Election, output: W) { // Write withdrawn candidates if !election.withdrawn_candidates.is_empty() { - todo!(); + output.write(election.withdrawn_candidates.into_iter().map(|idx| format!("-{}", idx + 1)).join(" ").as_bytes()).expect("IO Error"); + output.write(b"\n").expect("IO Error"); } // Write ballots diff --git a/tests/convert.rs b/tests/convert.rs new file mode 100644 index 0000000..aec5899 --- /dev/null +++ b/tests/convert.rs @@ -0,0 +1,57 @@ +/* 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::Election; +use opentally::numbers::Rational; +use opentally::parser; +use opentally::writer; + +use std::fs; + +#[test] +fn ers97_wd_conversions() { + let blt_input = fs::read_to_string("tests/data/ers97_wd.blt").expect("IO Error"); + + let election: Election = parser::blt::parse_path("tests/data/ers97_wd.blt").expect("Parse Error"); + + // Check BLT + let mut buf: Vec = Vec::new(); + writer::blt::write(election.clone(), &mut buf); + assert_eq!(blt_input, std::str::from_utf8(&buf).expect("UTF-8 Error")); + + // Check BIN + buf.clear(); + writer::bin::write(election.clone(), &mut buf); + let election2: Election = parser::bin::parse_bytes(&buf); + + buf.clear(); + writer::blt::write(election2, &mut buf); + assert_eq!(blt_input, std::str::from_utf8(&buf).expect("UTF-8 Error")); + + // Check CSP + buf.clear(); + writer::csp::write(election.clone(), &mut buf); + let mut election2: Election = parser::csp::parse_reader(&buf[..]); + + election2.seats = election.seats; + election2.name = election.name; + election2.withdrawn_candidates = election.withdrawn_candidates.clone(); + + buf.clear(); + writer::blt::write(election2, &mut buf); + assert_eq!(blt_input, std::str::from_utf8(&buf).expect("UTF-8 Error")); +} diff --git a/tests/data/ers97_wd.blt b/tests/data/ers97_wd.blt new file mode 100644 index 0000000..fb6a742 --- /dev/null +++ b/tests/data/ers97_wd.blt @@ -0,0 +1,69 @@ +11 6 +-1 -2 +8 1 2 0 +3 1 3 0 +14 1 4 0 +34 1 5 0 +1 1 6 0 +4 1 7 0 +1 1 8 0 +4 1 9 3 0 +3 1 9 4 0 +2 1 9 6 0 +3 1 9 7 0 +4 1 9 8 0 +9 1 9 0 +4 1 10 3 0 +5 1 10 4 0 +3 1 10 6 0 +3 1 10 7 0 +6 1 10 8 0 +10 1 10 0 +1 1 11 5 0 +1 1 11 0 +11 1 0 +105 2 0 +91 3 0 +90 4 0 +81 5 0 +64 6 0 +11 7 6 0 +36 7 8 0 +12 7 0 +55 8 0 +3 9 3 0 +2 9 4 0 +2 9 5 3 0 +1 9 5 4 0 +15 9 5 0 +2 9 6 0 +2 9 8 0 +2 10 3 0 +2 10 4 0 +2 10 5 6 0 +1 10 5 7 6 0 +2 10 5 8 0 +11 10 5 0 +1 10 6 0 +1 10 7 4 0 +1 10 8 0 +1 10 0 +2 11 2 0 +4 11 3 0 +1 11 4 0 +5 11 7 8 0 +10 11 8 0 +1 11 0 +0 +"Smith" +"Duke" +"Prince" +"Freeman" +"Carpenter" +"Baron" +"Abbot" +"Vicar" +"Wright" +"Glazier" +"Monk" +"ERS Model - 1997 Edition"