Switch to handwritten BLT parser
This commit is contained in:
parent
470f1e550e
commit
3801d30527
152
Cargo.lock
generated
152
Cargo.lock
generated
@ -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"
|
||||
|
@ -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]
|
||||
|
@ -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<N> {
|
||||
@ -43,62 +42,10 @@ pub struct Election<N> {
|
||||
|
||||
impl<N: Number> Election<N> {
|
||||
/// 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<I: Iterator<Item=char>>(input: Peekable<I>) -> 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
|
||||
|
@ -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");
|
||||
|
16
src/main.rs
16
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<Rational> = Election::from_blt(&file);
|
||||
let mut election: Election<Rational> = Election::from_blt(chars);
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||
|
||||
// Must specify ::<N> here and in a few other places because ndarray causes E0275 otherwise
|
||||
count_election::<Rational>(election, cmd_opts);
|
||||
} else if cmd_opts.numbers == "float64" {
|
||||
let mut election: Election<NativeFloat64> = Election::from_blt(&file);
|
||||
let mut election: Election<NativeFloat64> = Election::from_blt(chars);
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||
count_election::<NativeFloat64>(election, cmd_opts);
|
||||
} else if cmd_opts.numbers == "fixed" {
|
||||
Fixed::set_dps(cmd_opts.decimals);
|
||||
let mut election: Election<Fixed> = Election::from_blt(&file);
|
||||
let mut election: Election<Fixed> = Election::from_blt(chars);
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||
count_election::<Fixed>(election, cmd_opts);
|
||||
} else if cmd_opts.numbers == "gfixed" {
|
||||
GuardedFixed::set_dps(cmd_opts.decimals);
|
||||
|
||||
let mut election: Election<GuardedFixed> = Election::from_blt(&file);
|
||||
let mut election: Election<GuardedFixed> = Election::from_blt(chars);
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||
count_election::<GuardedFixed>(election, cmd_opts);
|
||||
}
|
||||
|
@ -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) }
|
@ -15,30 +15,320 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![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<N: Number, I: Iterator<Item=char>> {
|
||||
/// The peekable iterator of chars representing the BLT file
|
||||
chars: Peekable<I>,
|
||||
|
||||
/// Convert the specified [Pair<Rule>] into a &str
|
||||
pub fn unwrap_string(string: Pair<Rule>) -> &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<N>,
|
||||
}
|
||||
Rule::quoted_string => {
|
||||
let string = string.as_str();
|
||||
return &string[1..string.len()-1];
|
||||
|
||||
/// An error when parsing a BLT file
|
||||
#[derive(Debug)]
|
||||
pub enum ParseError {
|
||||
/// Invalid syntax
|
||||
Invalid
|
||||
}
|
||||
_ => unreachable!()
|
||||
|
||||
impl<N: Number, I: Iterator<Item=char>> BLTParser<N, I> {
|
||||
// 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;
|
||||
}
|
||||
} 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<usize, ParseError> {
|
||||
return Ok(self.integer()?.parse().expect("Invalid usize"));
|
||||
}
|
||||
|
||||
/// Parse an integer as a [String]
|
||||
fn integer(&mut self) -> Result<String, ParseError> {
|
||||
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<String, ParseError> {
|
||||
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<String, ParseError> {
|
||||
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<String, ParseError> {
|
||||
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<char, ParseError> {
|
||||
if self.lookahead() >= '1' && self.lookahead() <= '9' {
|
||||
return Ok(self.accept());
|
||||
} else {
|
||||
return Err(ParseError::Invalid);
|
||||
}
|
||||
}
|
||||
|
||||
/// Read any digit
|
||||
fn digit(&mut self) -> Result<char, ParseError> {
|
||||
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<char, ParseError> {
|
||||
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<I>) -> 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<N> {
|
||||
return self.election;
|
||||
}
|
||||
}
|
||||
|
@ -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$type>](election);
|
||||
}
|
||||
|
||||
|
12
tests/aec.rs
12
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<Rational> = Election::from_blt(&input);
|
||||
let election: Election<Rational> = Election::from_blt(chars);
|
||||
|
||||
// Validate candidate names
|
||||
for (i, candidate) in candidates.iter().enumerate() {
|
||||
|
@ -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<Rational> = 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<Rational> = 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<Rational> = 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<Rational> = 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<Rational> = 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<Rational> = Election::from_blt(chars);
|
||||
|
||||
// Read CON
|
||||
let file = File::open("tests/data/prsa1_constr3.con").expect("IO Error");
|
||||
|
@ -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<Fixed> = 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<Fixed> = 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<Fixed> = 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<Fixed> = Election::from_blt(chars);
|
||||
|
||||
// Initialise count state
|
||||
let mut state = CountState::new(&election);
|
||||
|
@ -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<N> = 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<N> = Election::from_blt(chars);
|
||||
|
||||
// !!! FOR SCOTTISH STV !!!
|
||||
election.normalise_ballots();
|
||||
|
@ -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<usize> = 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<N> = Election::from_blt(&file);
|
||||
let election: Election<N> = Election::from_blt(chars);
|
||||
|
||||
// Validate candidate names
|
||||
for (i, candidate) in candidates.iter().enumerate() {
|
||||
|
Loading…
Reference in New Issue
Block a user