From 3801d305271ece5b3719301f4d2ebf1e9b35e68c Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Thu, 29 Jul 2021 03:24:51 +1000 Subject: [PATCH] Switch to handwritten BLT parser --- Cargo.lock | 152 +++----------------- Cargo.toml | 5 +- src/election.rs | 65 +-------- src/lib.rs | 2 - src/main.rs | 16 ++- src/parser/blt.pest | 34 ----- src/parser/blt.rs | 328 ++++++++++++++++++++++++++++++++++++++++--- src/stv/wasm.rs | 2 +- tests/aec.rs | 12 +- tests/constraints.rs | 20 +-- tests/meek.rs | 14 +- tests/scotland.rs | 9 +- tests/utils/mod.rs | 9 +- 13 files changed, 388 insertions(+), 280 deletions(-) delete mode 100644 src/parser/blt.pest diff --git a/Cargo.lock b/Cargo.lock index 6d5d79e..443bc79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "assert_cmd" version = "1.0.5" @@ -47,34 +53,13 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.4", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", + "generic-array", ] [[package]] @@ -95,12 +80,6 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - [[package]] name = "byteorder" version = "1.4.3" @@ -225,22 +204,13 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.4", + "generic-array", ] [[package]] @@ -255,12 +225,6 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "flate2" version = "1.0.20" @@ -282,15 +246,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.4" @@ -416,12 +371,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "matrixmultiply" version = "0.3.1" @@ -517,12 +466,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - [[package]] name = "opaque-debug" version = "0.3.0" @@ -548,11 +491,10 @@ dependencies = [ "num-rational", "num-traits", "paste", - "pest", - "pest_derive", "predicates", "rug", "sha2", + "utf8-chars", "wasm-bindgen", "xmltree", ] @@ -569,49 +511,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" -dependencies = [ - "maplit", - "pest", - "sha-1", -] - [[package]] name = "predicates" version = "1.0.8" @@ -759,29 +658,17 @@ version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha2" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" dependencies = [ - "block-buffer 0.9.0", + "block-buffer", "cfg-if 1.0.0", "cpufeatures", - "digest 0.9.0", - "opaque-debug 0.3.0", + "digest", + "opaque-debug", ] [[package]] @@ -819,12 +706,6 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - [[package]] name = "unicode-segmentation" version = "1.7.1" @@ -837,6 +718,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "utf8-chars" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1348d8face79d019be7cbc0198e36bf93e160ddbfaa7bb54c9592627b9ec841" +dependencies = [ + "arrayvec", +] + [[package]] name = "vec_map" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index efec249..0c703e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,6 @@ git-version = "0.3.4" ibig = "0.3.2" itertools = "0.10.1" ndarray = "0.15.3" -pest = "2.1.3" -pest_derive = "2.1.0" predicates = "1.0.8" num-traits = "0.2" sha2 = "0.9.5" @@ -28,11 +26,12 @@ num-bigint = "0.4.0" num-rational = "0.4.0" paste = "1.0.5" -# For tests +# For tests/CLI only [target.'cfg(not(target_arch = "wasm32"))'.dependencies] assert_cmd = "1.0.5" csv = "1.1.6" flate2 = "1.0" +utf8-chars = "1.0.2" xmltree = "0.10.3" [target.'cfg(not(target_arch = "wasm32"))'.dependencies.rug] diff --git a/src/election.rs b/src/election.rs index 20d67c8..fafe41a 100644 --- a/src/election.rs +++ b/src/election.rs @@ -18,12 +18,11 @@ use crate::constraints::{Constraints, ConstraintMatrix}; use crate::logger::Logger; use crate::numbers::Number; -use crate::parser::blt; +use crate::parser::blt::BLTParser; use crate::sharandom::SHARandom; -use pest::Parser; - use std::collections::HashMap; +use std::iter::Peekable; /// An election to be counted pub struct Election { @@ -43,62 +42,10 @@ pub struct Election { impl Election { /// Parse the given BLT file and return an [Election] - pub fn from_blt(input: &str) -> Self { - let mut main = blt::BLTParser::parse(blt::Rule::main, input).expect("Syntax Error").next().unwrap().into_inner(); - - let mut header = main.next().unwrap().into_inner(); - let num_candidates = header.next().unwrap().as_str().parse().unwrap(); - let seats = header.next().unwrap().as_str().parse().unwrap(); - - // Initialise the Election object - let mut election = Election { - name: String::new(), - seats: seats, - candidates: Vec::with_capacity(num_candidates), - withdrawn_candidates: Vec::new(), - ballots: Vec::new(), - constraints: None, - }; - - // Read withdrawn candidates - let withdrawn_pairs = main.next().unwrap().into_inner(); - for withdrawn_cand in withdrawn_pairs { - let val = withdrawn_cand.as_str(); - let val: usize = val[1..val.len()].parse().unwrap(); - election.withdrawn_candidates.push(val - 1); - } - - // Read ballots - let ballot_list = main.next().unwrap().into_inner(); - for ballot in ballot_list { - let mut ballot_pairs = ballot.into_inner(); - - let value = N::parse(ballot_pairs.next().unwrap().as_str()); - - let mut ballot = Ballot { - orig_value: value, - preferences: Vec::new(), - }; - - for preference in ballot_pairs { - let preference: usize = preference.as_str().parse().unwrap(); - ballot.preferences.push(preference - 1); - } - - election.ballots.push(ballot); - } - - // Read candidates - for _ in 0..num_candidates { - let string = main.next().expect("Expected candidate name"); - election.candidates.push(Candidate { name: blt::unwrap_string(string).to_string() }); - } - - // Read name - let string = main.next().expect("Expected election title"); - election.name.push_str(blt::unwrap_string(string)); - - return election; + pub fn from_blt>(input: Peekable) -> Self { + let mut parser = BLTParser::new(input); + parser.parse_blt().expect("Syntax Error"); + return parser.as_election(); } /// Convert ballots with weight >1 to multiple ballots of weight 1 diff --git a/src/lib.rs b/src/lib.rs index cffff22..ea04e81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,8 +38,6 @@ pub mod ties; use git_version::git_version; use wasm_bindgen::prelude::wasm_bindgen; -#[macro_use] -extern crate pest_derive; /// The git revision of this OpenTally build pub const VERSION: &str = git_version!(args=["--always", "--dirty=-dev"], fallback="unknown"); diff --git a/src/main.rs b/src/main.rs index d951093..ed0bdd6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,10 +21,11 @@ use opentally::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational}; use opentally::stv; use clap::{AppSettings, Clap}; +use utf8_chars::BufReadCharsExt; use std::cmp::max; -use std::fs::{self, File}; -use std::io::{self, BufRead}; +use std::fs::File; +use std::io::{self, BufRead, BufReader}; use std::ops; /// Open-source election vote counting @@ -190,28 +191,29 @@ fn main() { let Command::STV(cmd_opts) = opts.command; // Read BLT file - let file = fs::read_to_string(&cmd_opts.filename).expect("IO Error"); + let mut reader = BufReader::new(File::open(&cmd_opts.filename).expect("IO Error")); + let chars = reader.chars().map(|r| r.expect("IO Error")).peekable(); // Create and count election according to --numbers if cmd_opts.numbers == "rational" { - let mut election: Election = Election::from_blt(&file); + let mut election: Election = Election::from_blt(chars); maybe_load_constraints(&mut election, &cmd_opts.constraints); // Must specify :: here and in a few other places because ndarray causes E0275 otherwise count_election::(election, cmd_opts); } else if cmd_opts.numbers == "float64" { - let mut election: Election = Election::from_blt(&file); + let mut election: Election = Election::from_blt(chars); maybe_load_constraints(&mut election, &cmd_opts.constraints); count_election::(election, cmd_opts); } else if cmd_opts.numbers == "fixed" { Fixed::set_dps(cmd_opts.decimals); - let mut election: Election = Election::from_blt(&file); + let mut election: Election = Election::from_blt(chars); maybe_load_constraints(&mut election, &cmd_opts.constraints); count_election::(election, cmd_opts); } else if cmd_opts.numbers == "gfixed" { GuardedFixed::set_dps(cmd_opts.decimals); - let mut election: Election = Election::from_blt(&file); + let mut election: Election = Election::from_blt(chars); maybe_load_constraints(&mut election, &cmd_opts.constraints); count_election::(election, cmd_opts); } diff --git a/src/parser/blt.pest b/src/parser/blt.pest deleted file mode 100644 index 468f9d4..0000000 --- a/src/parser/blt.pest +++ /dev/null @@ -1,34 +0,0 @@ -main = { - SOI ~ NEWLINE* - ~ header ~ NEWLINE* - ~ withdrawn ~ NEWLINE* - ~ ballot_list - ~ "0" ~ NEWLINE* - ~ (string ~ NEWLINE*)+ - ~ EOI -} - -header = { - integer ~ integer -} - -withdrawn = { - (withdrawn_cand ~ NEWLINE*)* -} -withdrawn_cand = @{ "-" ~ integer } - -ballot_list = { (ballot ~ NEWLINE*)* } -ballot = { - number ~ integer* ~ ("0" ~ NEWLINE? | NEWLINE) -} - -integer = @{ ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* } -decimal = @{ "0." ~ ASCII_DIGIT+ | integer ~ "." ~ ASCII_DIGIT+ } -number = { decimal | integer } - -quoted_string = @{ "\"" ~ (!"\"" ~ !NEWLINE ~ ANY)* ~ "\"" } -raw_string = @{ (!" " ~ !NEWLINE ~ ANY)+ } -string = { quoted_string | raw_string } - -WHITESPACE = _{ " " } -COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* ~ (&NEWLINE | &EOI) } diff --git a/src/parser/blt.rs b/src/parser/blt.rs index 10050e9..73baa8e 100644 --- a/src/parser/blt.rs +++ b/src/parser/blt.rs @@ -15,30 +15,320 @@ * along with this program. If not, see . */ -#![allow(missing_docs)] +use crate::election::{Ballot, Candidate, Election}; +use crate::numbers::Number; -use pest::iterators::Pair; +use std::iter::Peekable; -#[derive(Parser)] -#[grammar = "parser/blt.pest"] -pub struct BLTParser; +/// Utility for parsing a BLT file +pub struct BLTParser> { + /// The peekable iterator of chars representing the BLT file + chars: Peekable, -/// Convert the specified [Pair] into a &str -pub fn unwrap_string(string: Pair) -> &str { - match string.as_rule() { - Rule::string => { - let string = string.into_inner().next().unwrap(); - match string.as_rule() { - Rule::raw_string => { - return string.as_str(); + /// Temporary buffer for parsing ballot values + buf_ballot_value: String, + + /// Number of candidates + num_candidates: usize, + /// Parsed [Election] + election: Election, +} + +/// An error when parsing a BLT file +#[derive(Debug)] +pub enum ParseError { + /// Invalid syntax + Invalid +} + +impl> BLTParser { + // NON-TERMINALS - HIGHER LEVEL + + /// Parse the BLT file + pub fn parse_blt(&mut self) -> Result<(), ParseError> { + self.delimiter(); + + self.header()?; + self.withdrawn_candidates()?; + self.ballots()?; + self.strings()?; + + return Ok(()); + } + + /// Parse the header + fn header(&mut self) -> Result<(), ParseError> { + self.num_candidates = self.usize()?; + self.delimiter(); + self.election.seats = self.usize()?; + self.delimiter(); + return Ok(()); + } + + /// Parse the withdrawn candidates (if any) + fn withdrawn_candidates(&mut self) -> Result<(), ParseError> { + while self.lookahead() == '-' { + self.accept(); // Minus sign + let index = self.usize()? - 1; + self.election.withdrawn_candidates.push(index); + self.delimiter(); + } + return Ok(()); + } + + /// Parse the list of ballots + fn ballots(&mut self) -> Result<(), ParseError> { + loop { + if self.lookahead() == '0' { + // End of ballots, or start of decimal? + self.accept(); + + if self.lookahead() == '.' { + // Decimal + self.buf_ballot_value.clear(); + self.buf_ballot_value.push('0'); + self.ballot()?; + } else { + // End of ballots + self.delimiter(); + break; } - Rule::quoted_string => { - let string = string.as_str(); - return &string[1..string.len()-1]; - } - _ => unreachable!() + } else { + self.buf_ballot_value.clear(); + self.ballot()?; } } - _ => panic!("Non-string passed to unwrap_string") + return Ok(()); + } + + /// Parse a ballot + fn ballot(&mut self) -> Result<(), ParseError> { + self.ballot_value()?; + + self.delimiter_not_nl(); + + // Read preferences + let mut preferences = Vec::new(); + loop { + if self.lookahead() == '0' || self.lookahead() == '\n' { + // End of preferences + self.accept(); + break; + } else { + preferences.push(self.usize()? - 1); + self.delimiter_not_nl(); + } + } + + self.delimiter(); + + let ballot = Ballot { + orig_value: N::parse(&self.buf_ballot_value), + preferences: preferences, + }; + self.election.ballots.push(ballot); + + return Ok(()); + } + + /// Parse the list of strings at the end of the BLT file + fn strings(&mut self) -> Result<(), ParseError> { + for _ in 0..self.num_candidates { + let name = self.string()?; + self.election.candidates.push(Candidate { + name + }); + } + let name = self.string()?; + self.election.name = name; + return Ok(()); + } + + // NON-TERMINALS - LOWER LEVEL + + /// Parse an integer into a [usize] + fn usize(&mut self) -> Result { + return Ok(self.integer()?.parse().expect("Invalid usize")); + } + + /// Parse an integer as a [String] + fn integer(&mut self) -> Result { + let mut result = String::from(self.digit_nonzero()?); + loop { + match self.digit() { + Err(_) => { break; } + Ok(d) => { result.push(d); } + } + } + return Ok(result); + } + + /// Parse a number as an instance of N + fn ballot_value(&mut self) -> Result<(), ParseError> { + loop { + match self.ballot_value_element() { + Err(_) => { break; } + Ok(d) => { self.buf_ballot_value.push(d); } + } + } + return Ok(()); + } + + /// Parse a quoted or raw string + fn string(&mut self) -> Result { + match self.quoted_string() { + Ok(s) => { return Ok(s); } + Err(_) => {} + } + match self.raw_string() { + Ok(s) => { return Ok(s); } + Err(_) => {} + } + return Err(ParseError::Invalid); + } + + /// Parse a quoted string + fn quoted_string(&mut self) -> Result { + if self.lookahead() == '"' { + self.accept(); // Opening quotation mark + let mut result = String::new(); + while self.lookahead() != '"' { + // TODO: BufRead::read_until ? + result.push(self.accept()); + } + self.accept(); // Closing quotation mark + if !self.eof() { + self.delimiter(); + } + return Ok(result); + } else { + return Err(ParseError::Invalid); + } + } + + /// Parse a raw (unquoted) string + fn raw_string(&mut self) -> Result { + let mut result = String::new(); + while !self.lookahead().is_whitespace() && !self.eof() { + result.push(self.accept()); + } + if !self.eof() { + self.delimiter(); + } + return Ok(result); + } + + /// Skip any sequence of whitespace or comments + fn delimiter(&mut self) { + loop { + if self.eof() { + break; + } else if self.lookahead() == '#' { + self.dnl(); + if !self.eof() { + self.accept(); // Trailing newline + } + } else if self.lookahead().is_whitespace() { + self.accept(); + while !self.eof() && self.lookahead().is_whitespace() { self.accept(); } + } else { + break; + } + } + } + + /// Skip any sequence of whitespace or comments, but do not accept any newline and leave it trailing + fn delimiter_not_nl(&mut self) { + loop { + if self.eof() { + break; + } else if self.lookahead() == '#' { + self.dnl(); + } else if self.lookahead().is_whitespace() && self.lookahead() != '\n' { + self.accept(); + while !self.eof() && self.lookahead().is_whitespace() && self.lookahead() != '\n' { self.accept(); } + } else { + break; + } + } + } + + /// Skip to the next newline + fn dnl(&mut self) { + while !self.eof() && self.lookahead() != '\n' { + // TODO: BufRead::read_until ? + self.accept(); + } + } + + // TERMINALS + + /// Read a nonzero digit + fn digit_nonzero(&mut self) -> Result { + if self.lookahead() >= '1' && self.lookahead() <= '9' { + return Ok(self.accept()); + } else { + return Err(ParseError::Invalid); + } + } + + /// Read any digit + fn digit(&mut self) -> Result { + if self.lookahead() >= '0' && self.lookahead() <= '9' { + return Ok(self.accept()); + } else { + return Err(ParseError::Invalid); + } + } + + /// Read any element of a valid number, i.e. a digit, decimal point or slash + fn ballot_value_element(&mut self) -> Result { + if (self.lookahead() >= '0' && self.lookahead() <= '9') || self.lookahead() == '.' || self.lookahead() == '/' { + return Ok(self.accept()); + } else { + return Err(ParseError::Invalid); + } + } + + // UTILITIES + + /// Return if this is the end of the file + fn eof(&mut self) -> bool { + return self.chars.peek().is_none(); + } + + /// Peek at the next character in the stream + fn lookahead(&mut self) -> char { + // TODO: Cache this? + return *self.chars.peek().expect("Unexpected EOF"); + } + + /// Read and return one character from the stream + fn accept(&mut self) -> char { + return self.chars.next().expect("Unexpected EOF"); + } + + // PUBLIC API + + /// Return a new [BLTParser] + pub fn new(chars: Peekable) -> Self { + Self { + chars, + buf_ballot_value: String::new(), + num_candidates: 0, + election: Election { + name: String::new(), + seats: 0, + candidates: Vec::new(), + withdrawn_candidates: Vec::new(), + ballots: Vec::new(), + constraints: None, + }, + } + } + + /// Return the parsed [Election] + pub fn as_election(self) -> Election { + return self.election; } } diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 37cd359..a7c9473 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -56,7 +56,7 @@ macro_rules! impl_type { // Install panic! hook console_error_panic_hook::set_once(); - let election: Election<$type> = Election::from_blt(&text); + let election: Election<$type> = Election::from_blt(text.chars().peekable()); return [](election); } diff --git a/tests/aec.rs b/tests/aec.rs index 3ff13b8..185956e 100644 --- a/tests/aec.rs +++ b/tests/aec.rs @@ -23,9 +23,10 @@ use opentally::stv; use csv::StringRecord; use flate2::bufread::GzDecoder; +use utf8_chars::BufReadCharsExt; use std::fs::File; -use std::io::{self, Read}; +use std::io::{self, BufReader}; #[test] fn aec_tas19_rational() { @@ -45,12 +46,13 @@ fn aec_tas19_rational() { // Decompress BLT let file = File::open("tests/data/aec-senate-formalpreferences-24310-TAS.blt.gz").expect("IO Error"); let file_reader = io::BufReader::new(file); - let mut gz_decoder = GzDecoder::new(file_reader); - let mut input = String::new(); - gz_decoder.read_to_string(&mut input).expect("IO Error"); + let gz_decoder = GzDecoder::new(file_reader); + + let mut reader = BufReader::new(gz_decoder); + let chars = reader.chars().map(|r| r.expect("IO Error")).peekable(); // Read BLT - let election: Election = Election::from_blt(&input); + let election: Election = Election::from_blt(chars); // Validate candidate names for (i, candidate) in candidates.iter().enumerate() { diff --git a/tests/constraints.rs b/tests/constraints.rs index f774792..d74cec6 100644 --- a/tests/constraints.rs +++ b/tests/constraints.rs @@ -21,17 +21,19 @@ use opentally::constraints::Constraints; use opentally::election::{CandidateState, CountState, Election}; use opentally::numbers::Rational; use opentally::stv; +use utf8_chars::BufReadCharsExt; -use std::fs::{self, File}; -use std::io::{self, BufRead}; +use std::fs::File; +use std::io::{self, BufRead, BufReader}; #[test] fn prsa1_constr1_rational() { // FIXME: This is unvalidated! // Read BLT - let file = fs::read_to_string("tests/data/prsa1.blt").expect("IO Error"); - let mut election: Election = Election::from_blt(&file); + let mut reader = BufReader::new(File::open("tests/data/prsa1.blt").expect("IO Error")); + let chars = reader.chars().map(|r| r.expect("IO Error")).peekable(); + let mut election: Election = Election::from_blt(chars); // Read CON let file = File::open("tests/data/prsa1_constr1.con").expect("IO Error"); @@ -92,8 +94,9 @@ fn prsa1_constr2_rational() { // FIXME: This is unvalidated! // Read BLT - let file = fs::read_to_string("tests/data/prsa1.blt").expect("IO Error"); - let mut election: Election = Election::from_blt(&file); + let mut reader = BufReader::new(File::open("tests/data/prsa1.blt").expect("IO Error")); + let chars = reader.chars().map(|r| r.expect("IO Error")).peekable(); + let mut election: Election = Election::from_blt(chars); // Read CON let file = File::open("tests/data/prsa1_constr2.con").expect("IO Error"); @@ -154,8 +157,9 @@ fn prsa1_constr3_rational() { // FIXME: This is unvalidated! // Read BLT - let file = fs::read_to_string("tests/data/prsa1.blt").expect("IO Error"); - let mut election: Election = Election::from_blt(&file); + let mut reader = BufReader::new(File::open("tests/data/prsa1.blt").expect("IO Error")); + let chars = reader.chars().map(|r| r.expect("IO Error")).peekable(); + let mut election: Election = Election::from_blt(chars); // Read CON let file = File::open("tests/data/prsa1_constr3.con").expect("IO Error"); diff --git a/tests/meek.rs b/tests/meek.rs index 37e0bf8..423cea2 100644 --- a/tests/meek.rs +++ b/tests/meek.rs @@ -20,8 +20,10 @@ mod utils; use opentally::election::{CandidateState, CountState, Election}; use opentally::numbers::{Fixed, NativeFloat64, Number}; use opentally::stv; +use utf8_chars::BufReadCharsExt; -use std::fs; +use std::fs::File; +use std::io::BufReader; // Compare ers97.blt count with result produced by 1987 Hill–Wichmann–Woodall reference implementation #[test] @@ -85,8 +87,9 @@ fn meek06_ers97_fixed12() { Fixed::set_dps(12); // Read BLT - let file = fs::read_to_string("tests/data/ers97.blt").expect("IO Error"); - let election: Election = Election::from_blt(&file); + let mut reader = BufReader::new(File::open("tests/data/ers97.blt").expect("IO Error")); + let chars = reader.chars().map(|r| r.expect("IO Error")).peekable(); + let election: Election = Election::from_blt(chars); // Initialise count state let mut state = CountState::new(&election); @@ -158,8 +161,9 @@ fn meeknz_ers97_fixed12() { Fixed::set_dps(12); // Read BLT - let file = fs::read_to_string("tests/data/ers97.blt").expect("IO Error"); - let election: Election = Election::from_blt(&file); + let mut reader = BufReader::new(File::open("tests/data/ers97.blt").expect("IO Error")); + let chars = reader.chars().map(|r| r.expect("IO Error")).peekable(); + let election: Election = Election::from_blt(chars); // Initialise count state let mut state = CountState::new(&election); diff --git a/tests/scotland.rs b/tests/scotland.rs index a5e97d4..0a4a61b 100644 --- a/tests/scotland.rs +++ b/tests/scotland.rs @@ -19,9 +19,11 @@ use opentally::election::{CandidateState, CountState, Election}; use opentally::numbers::{Fixed, GuardedFixed, Number}; use opentally::stv; +use utf8_chars::BufReadCharsExt; use xmltree::Element; -use std::fs::{self, File}; +use std::fs::File; +use std::io::BufReader; use std::ops; #[test] @@ -111,8 +113,9 @@ where let num_stages = root.get_child("headerrow").expect("Syntax Error").children.len(); // Read BLT - let file = fs::read_to_string("tests/data/linn07.blt").expect("IO Error"); - let mut election: Election = Election::from_blt(&file); + let mut reader = BufReader::new(File::open("tests/data/linn07.blt").expect("IO Error")); + let chars = reader.chars().map(|r| r.expect("IO Error")).peekable(); + let mut election: Election = Election::from_blt(chars); // !!! FOR SCOTTISH STV !!! election.normalise_ballots(); diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index cc68275..61fd980 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -20,8 +20,10 @@ use opentally::numbers::Number; use opentally::stv; use csv::StringRecord; +use utf8_chars::BufReadCharsExt; -use std::fs; +use std::fs::File; +use std::io::BufReader; use std::ops; #[allow(dead_code)] // Suppress false positive @@ -47,9 +49,10 @@ where let stages: Vec = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect(); // Read BLT - let file = fs::read_to_string(blt_file).expect("IO Error"); + let mut reader = BufReader::new(File::open(blt_file).expect("IO Error")); + let chars = reader.chars().map(|r| r.expect("IO Error")).peekable(); - let election: Election = Election::from_blt(&file); + let election: Election = Election::from_blt(chars); // Validate candidate names for (i, candidate) in candidates.iter().enumerate() {