Use Pest-based parser for BLT files

Support comments, optional newlines, etc.
This commit is contained in:
RunasSudo 2021-07-23 16:45:54 +10:00
parent 3b8ccd097e
commit cca097f943
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
15 changed files with 309 additions and 106 deletions

136
Cargo.lock generated
View File

@ -47,13 +47,34 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 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]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.9.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [ dependencies = [
"generic-array", "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",
] ]
[[package]] [[package]]
@ -74,6 +95,12 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
[[package]]
name = "byte-tools"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.4.3" version = "1.4.3"
@ -198,13 +225,22 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 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]] [[package]]
name = "digest" name = "digest"
version = "0.9.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.4",
] ]
[[package]] [[package]]
@ -219,6 +255,12 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "fake-simd"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.20" version = "1.0.20"
@ -240,6 +282,15 @@ dependencies = [
"num-traits", "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]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.4" version = "0.14.4"
@ -365,6 +416,12 @@ dependencies = [
"cfg-if 1.0.0", "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]] [[package]]
name = "matrixmultiply" name = "matrixmultiply"
version = "0.3.1" version = "0.3.1"
@ -460,6 +517,12 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "opaque-debug"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
version = "0.3.0" version = "0.3.0"
@ -485,6 +548,8 @@ dependencies = [
"num-rational", "num-rational",
"num-traits", "num-traits",
"paste", "paste",
"pest",
"pest_derive",
"predicates", "predicates",
"rug", "rug",
"sha2", "sha2",
@ -504,6 +569,49 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" 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]] [[package]]
name = "predicates" name = "predicates"
version = "1.0.8" version = "1.0.8"
@ -651,17 +759,29 @@ version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" 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]] [[package]]
name = "sha2" name = "sha2"
version = "0.9.5" version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
dependencies = [ dependencies = [
"block-buffer", "block-buffer 0.9.0",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"cpufeatures", "cpufeatures",
"digest", "digest 0.9.0",
"opaque-debug", "opaque-debug 0.3.0",
] ]
[[package]] [[package]]
@ -699,6 +819,12 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
[[package]]
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.7.1" version = "1.7.1"

View File

@ -13,6 +13,8 @@ git-version = "0.3.4"
ibig = "0.3.2" ibig = "0.3.2"
itertools = "0.10.1" itertools = "0.10.1"
ndarray = "0.15.3" ndarray = "0.15.3"
pest = "2.1.3"
pest_derive = "2.1.0"
predicates = "1.0.8" predicates = "1.0.8"
num-traits = "0.2" num-traits = "0.2"
sha2 = "0.9.5" sha2 = "0.9.5"

View File

@ -1,6 +1,6 @@
# BLT file format # BLT file format
OpenTally accepts ballot data in the BLT file format, as described by [Hill, Wichmann & Woodall](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) for their implementation of Meek STV. The BLT file format is also used by [OpenSTV/OpaVote](https://www.opavote.com/openstv), Lundell's [Droop](https://github.com/jklundell/droop/wiki/BltFileFormat) and Otten's [eSTV](https://web.archive.org/web/20020606014623/http://estv.otten.co.uk/) (where it is known as a DAT data transfer file). OpenTally accepts ballot data in the BLT file format, as described by [Hill, Wichmann & Woodall](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) for their implementation of Meek STV. The BLT file format is also used by [OpenSTV/OpaVote](https://www.opavote.com/help/overview#blt-file-format), Lundell's [Droop](https://github.com/jklundell/droop/wiki/BltFileFormat) and Otten's [eSTV](https://web.archive.org/web/20020606014623/http://estv.otten.co.uk/) (where it is known as a DAT data transfer file).
The file format is as follows: The file format is as follows:
@ -21,14 +21,16 @@ The file format is as follows:
"Title" "Title"
``` ```
The first line (`4 2`) indicates that there are 4 candidates for 2 vacancies. This must be on its own line. The first line (`4 2`) indicates that there are 4 candidates for 2 vacancies.
The second line (`-2`), which is optional, indicates that the 2nd candidate (Basil) has withdrawn. Multiple withdrawn candidates may be specified on this line, e.g. `-2 -3 -4`. This must, if present, be on its own line. The second line (`-2`), which is optional, indicates that the 2nd candidate (Basil) has withdrawn. Multiple withdrawn candidates may be specified on this line, e.g. `-2 -3 -4`.
The third line (second, if there are no withdrawn candidates) begins the ballot data. `3 1 3 4 0` indicates that there were 3 ballots which voted, in order of preference, for the 1st candidate (Adam), then the 3rd candidate (Charlotte), then the 4th candidate (Donald). A `0` optionally indicates the end of the list of preferences. Each such set of ballots must be on its own line. The third line (second, if there are no withdrawn candidates) begins the ballot data. `3 1 3 4 0` indicates that there were 3 ballots which voted, in order of preference, for the 1st candidate (Adam), then the 3rd candidate (Charlotte), then the 4th candidate (Donald). A `0` optionally indicates the end of the list of preferences.
The end of the list of ballots must be indicated with a single `0`, which must be on its own line. The end of the list of ballots must be indicated with a single `0`.
The next lines give the names of the candidates, up to the number of candidates specified on the first line (in this case, 4). Each candidate's name must be surrounded by quotation marks, and must appear on its own line. The next lines give the names of the candidates, up to the number of candidates specified on the first line (in this case, 4). Each candidate's name, unless a single word, must be surrounded by quotation marks.
The final line gives the name of the election, which must appear on its own line. The final line gives the name of the election, which, unless a single word, must be surrounded by quotation marks.
Newlines are optional, but if not provided, the `0` at the end of each ballot's preferences is mandatory.

View File

@ -18,8 +18,11 @@
use crate::constraints::{Constraints, ConstraintMatrix}; use crate::constraints::{Constraints, ConstraintMatrix};
use crate::logger::Logger; use crate::logger::Logger;
use crate::numbers::Number; use crate::numbers::Number;
use crate::parser::blt;
use crate::sharandom::SHARandom; use crate::sharandom::SHARandom;
use pest::Parser;
use std::collections::HashMap; use std::collections::HashMap;
/// An election to be counted /// An election to be counted
@ -40,12 +43,12 @@ pub struct Election<N> {
impl<N: Number> Election<N> { impl<N: Number> Election<N> {
/// Parse the given BLT file and return an [Election] /// Parse the given BLT file and return an [Election]
pub fn from_blt<I: Iterator<Item=String>>(mut lines: I) -> Self { pub fn from_blt(input: &str) -> Self {
// Read first line let mut main = blt::BLTParser::parse(blt::Rule::main, input).expect("Syntax Error").next().unwrap().into_inner();
let line = lines.next().expect("Unexpected EOF");
let mut bits = line.split(" "); let mut header = main.next().unwrap().into_inner();
let num_candidates = bits.next().expect("Syntax Error").parse().expect("Syntax Error"); let num_candidates = header.next().unwrap().as_str().parse().unwrap();
let seats: usize = bits.next().expect("Syntax Error").parse().expect("Syntax Error"); let seats = header.next().unwrap().as_str().parse().unwrap();
// Initialise the Election object // Initialise the Election object
let mut election = Election { let mut election = Election {
@ -57,57 +60,47 @@ impl<N: Number> Election<N> {
constraints: None, 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 // Read ballots
for line in &mut lines { let ballot_list = main.next().unwrap().into_inner();
if line == "0" { for ballot in ballot_list {
break; let mut ballot_pairs = ballot.into_inner();
}
let mut bits = line.split(" "); let value = N::parse(ballot_pairs.next().unwrap().as_str());
if line.starts_with("-") {
// Withdrawn candidates
for bit in bits.into_iter() {
let val = bit[1..bit.len()].parse::<usize>().expect("Syntax Error");
election.withdrawn_candidates.push(val - 1);
}
continue;
}
let value = N::parse(bits.next().expect("Syntax Error"));
let mut ballot = Ballot { let mut ballot = Ballot {
orig_value: value, orig_value: value,
preferences: Vec::new(), preferences: Vec::new(),
}; };
for preference in bits { let ballot_preferences = ballot_pairs.next().unwrap().into_inner();
if preference != "0" { for preference in ballot_preferences {
let preference = preference.parse::<usize>().expect("Syntax Error"); let preference: usize = preference.as_str().parse().unwrap();
ballot.preferences.push(preference - 1); ballot.preferences.push(preference - 1);
}
} }
election.ballots.push(ballot); election.ballots.push(ballot);
} }
// Zero line
main.next();
// Read candidates // Read candidates
for line in lines.by_ref().take(num_candidates) { for _ in 0..num_candidates {
let mut line = &line[..]; let string = main.next().expect("Expected candidate name");
if line.starts_with("\"") && line.ends_with("\"") { election.candidates.push(Candidate { name: blt::unwrap_string(string).to_string() });
line = &line[1..line.len()-1];
}
election.candidates.push(Candidate { name: line.to_string() });
} }
// Read name // Read name
let line = lines.next().expect("Syntax Error"); let string = main.next().expect("Expected election title");
let mut line = &line[..]; election.name.push_str(blt::unwrap_string(string));
if line.starts_with("\"") && line.ends_with("\"") {
line = &line[1..line.len()-1];
}
election.name.push_str(line);
return election; return election;
} }
@ -321,7 +314,7 @@ impl<'a, N: Number> CountCard<'a, N> {
pub struct Parcel<'a, N> { pub struct Parcel<'a, N> {
/// [Vote]s in this parcel /// [Vote]s in this parcel
pub votes: Vec<Vote<'a, N>>, pub votes: Vec<Vote<'a, N>>,
/// Order for sorting with [opentally::stv::ExclusionMethod::BySource] /// Order for sorting with [crate::stv::ExclusionMethod::BySource]
pub source_order: usize, pub source_order: usize,
} }

View File

@ -19,6 +19,8 @@
//! Open source counting software for various preferential voting election systems //! Open source counting software for various preferential voting election systems
/// File parsers
pub mod parser;
/// Data types and logic for constraints on elections /// Data types and logic for constraints on elections
pub mod constraints; pub mod constraints;
/// Data types for representing abstract elections /// Data types for representing abstract elections
@ -36,6 +38,8 @@ pub mod ties;
use git_version::git_version; use git_version::git_version;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
#[macro_use]
extern crate pest_derive;
/// The git revision of this OpenTally build /// The git revision of this OpenTally build
pub const VERSION: &str = git_version!(args=["--always", "--dirty=-dev"], fallback="unknown"); pub const VERSION: &str = git_version!(args=["--always", "--dirty=-dev"], fallback="unknown");

View File

@ -23,7 +23,7 @@ use opentally::stv;
use clap::{AppSettings, Clap}; use clap::{AppSettings, Clap};
use std::cmp::max; use std::cmp::max;
use std::fs::File; use std::fs::{self, File};
use std::io::{self, BufRead}; use std::io::{self, BufRead};
use std::ops; use std::ops;
@ -190,29 +190,28 @@ fn main() {
let Command::STV(cmd_opts) = opts.command; let Command::STV(cmd_opts) = opts.command;
// Read BLT file // Read BLT file
let file = File::open(&cmd_opts.filename).expect("IO Error"); let file = fs::read_to_string(&cmd_opts.filename).expect("IO Error");
let lines = io::BufReader::new(file).lines();
// Create and count election according to --numbers // Create and count election according to --numbers
if cmd_opts.numbers == "rational" { if cmd_opts.numbers == "rational" {
let mut election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); let mut election: Election<Rational> = Election::from_blt(&file);
maybe_load_constraints(&mut election, &cmd_opts.constraints); maybe_load_constraints(&mut election, &cmd_opts.constraints);
// Must specify ::<N> here and in a few other places because ndarray causes E0275 otherwise // Must specify ::<N> here and in a few other places because ndarray causes E0275 otherwise
count_election::<Rational>(election, cmd_opts); count_election::<Rational>(election, cmd_opts);
} else if cmd_opts.numbers == "float64" { } else if cmd_opts.numbers == "float64" {
let mut election: Election<NativeFloat64> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); let mut election: Election<NativeFloat64> = Election::from_blt(&file);
maybe_load_constraints(&mut election, &cmd_opts.constraints); maybe_load_constraints(&mut election, &cmd_opts.constraints);
count_election::<NativeFloat64>(election, cmd_opts); count_election::<NativeFloat64>(election, cmd_opts);
} else if cmd_opts.numbers == "fixed" { } else if cmd_opts.numbers == "fixed" {
Fixed::set_dps(cmd_opts.decimals); Fixed::set_dps(cmd_opts.decimals);
let mut election: Election<Fixed> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); let mut election: Election<Fixed> = Election::from_blt(&file);
maybe_load_constraints(&mut election, &cmd_opts.constraints); maybe_load_constraints(&mut election, &cmd_opts.constraints);
count_election::<Fixed>(election, cmd_opts); count_election::<Fixed>(election, cmd_opts);
} else if cmd_opts.numbers == "gfixed" { } else if cmd_opts.numbers == "gfixed" {
GuardedFixed::set_dps(cmd_opts.decimals); GuardedFixed::set_dps(cmd_opts.decimals);
let mut election: Election<GuardedFixed> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); let mut election: Election<GuardedFixed> = Election::from_blt(&file);
maybe_load_constraints(&mut election, &cmd_opts.constraints); maybe_load_constraints(&mut election, &cmd_opts.constraints);
count_election::<GuardedFixed>(election, cmd_opts); count_election::<GuardedFixed>(election, cmd_opts);
} }

36
src/parser/blt.pest Normal file
View File

@ -0,0 +1,36 @@
main = {
SOI ~ NEWLINE*
~ header ~ NEWLINE*
~ withdrawn ~ NEWLINE*
~ ballot_list
~ zero ~ NEWLINE*
~ (string ~ NEWLINE*)+
~ EOI
}
header = {
integer ~ integer
}
withdrawn = {
(withdrawn_cand ~ NEWLINE*)*
}
withdrawn_cand = @{ "-" ~ integer }
ballot_list = { (ballot ~ NEWLINE*)* }
ballot = {
number ~ ballot_preferences ~ (zero ~ NEWLINE? | NEWLINE)
}
ballot_preferences = { integer* }
zero = { "0" }
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) }

44
src/parser/blt.rs Normal file
View File

@ -0,0 +1,44 @@
/* 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/>.
*/
#![allow(missing_docs)]
use pest::iterators::Pair;
#[derive(Parser)]
#[grammar = "parser/blt.pest"]
pub struct BLTParser;
/// 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();
}
Rule::quoted_string => {
let string = string.as_str();
return &string[1..string.len()-1];
}
_ => unreachable!()
}
}
_ => panic!("Non-string passed to unwrap_string")
}
}

19
src/parser/mod.rs Normal file
View File

@ -0,0 +1,19 @@
/* 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/>.
*/
/// BLT file parser
pub mod blt;

View File

@ -56,7 +56,7 @@ macro_rules! impl_type {
// Install panic! hook // Install panic! hook
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
let election: Election<$type> = Election::from_blt(text.lines().map(|s| s.to_string()).into_iter()); let election: Election<$type> = Election::from_blt(&text);
return [<Election$type>](election); return [<Election$type>](election);
} }

View File

@ -25,7 +25,7 @@ use csv::StringRecord;
use flate2::bufread::GzDecoder; use flate2::bufread::GzDecoder;
use std::fs::File; use std::fs::File;
use std::io::{self, BufRead}; use std::io::{self, Read};
#[test] #[test]
fn aec_tas19_rational() { fn aec_tas19_rational() {
@ -45,12 +45,12 @@ fn aec_tas19_rational() {
// Decompress BLT // Decompress BLT
let file = File::open("tests/data/aec-senate-formalpreferences-24310-TAS.blt.gz").expect("IO Error"); let file = File::open("tests/data/aec-senate-formalpreferences-24310-TAS.blt.gz").expect("IO Error");
let file_reader = io::BufReader::new(file); let file_reader = io::BufReader::new(file);
let gz_decoder = GzDecoder::new(file_reader); let mut gz_decoder = GzDecoder::new(file_reader);
let gz_reader = io::BufReader::new(gz_decoder); let mut input = String::new();
let lines = gz_reader.lines(); gz_decoder.read_to_string(&mut input).expect("IO Error");
// Read BLT // Read BLT
let election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); let election: Election<Rational> = Election::from_blt(&input);
// Validate candidate names // Validate candidate names
for (i, candidate) in candidates.iter().enumerate() { for (i, candidate) in candidates.iter().enumerate() {

View File

@ -22,7 +22,7 @@ use opentally::election::{CandidateState, CountState, Election};
use opentally::numbers::Rational; use opentally::numbers::Rational;
use opentally::stv; use opentally::stv;
use std::fs::File; use std::fs::{self, File};
use std::io::{self, BufRead}; use std::io::{self, BufRead};
#[test] #[test]
@ -30,10 +30,8 @@ fn prsa1_constr1_rational() {
// FIXME: This is unvalidated! // FIXME: This is unvalidated!
// Read BLT // Read BLT
let file = File::open("tests/data/prsa1.blt").expect("IO Error"); let file = fs::read_to_string("tests/data/prsa1.blt").expect("IO Error");
let file_reader = io::BufReader::new(file); let mut election: Election<Rational> = Election::from_blt(&file);
let lines = file_reader.lines();
let mut election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
// Read CON // Read CON
let file = File::open("tests/data/prsa1_constr1.con").expect("IO Error"); let file = File::open("tests/data/prsa1_constr1.con").expect("IO Error");
@ -94,10 +92,8 @@ fn prsa1_constr2_rational() {
// FIXME: This is unvalidated! // FIXME: This is unvalidated!
// Read BLT // Read BLT
let file = File::open("tests/data/prsa1.blt").expect("IO Error"); let file = fs::read_to_string("tests/data/prsa1.blt").expect("IO Error");
let file_reader = io::BufReader::new(file); let mut election: Election<Rational> = Election::from_blt(&file);
let lines = file_reader.lines();
let mut election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
// Read CON // Read CON
let file = File::open("tests/data/prsa1_constr2.con").expect("IO Error"); let file = File::open("tests/data/prsa1_constr2.con").expect("IO Error");
@ -158,10 +154,8 @@ fn prsa1_constr3_rational() {
// FIXME: This is unvalidated! // FIXME: This is unvalidated!
// Read BLT // Read BLT
let file = File::open("tests/data/prsa1.blt").expect("IO Error"); let file = fs::read_to_string("tests/data/prsa1.blt").expect("IO Error");
let file_reader = io::BufReader::new(file); let mut election: Election<Rational> = Election::from_blt(&file);
let lines = file_reader.lines();
let mut election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
// Read CON // Read CON
let file = File::open("tests/data/prsa1_constr3.con").expect("IO Error"); let file = File::open("tests/data/prsa1_constr3.con").expect("IO Error");

View File

@ -21,8 +21,7 @@ use opentally::election::{CandidateState, CountState, Election};
use opentally::numbers::{Fixed, NativeFloat64, Number}; use opentally::numbers::{Fixed, NativeFloat64, Number};
use opentally::stv; use opentally::stv;
use std::io::{self, BufRead}; use std::fs;
use std::fs::File;
// Compare ers97.blt count with result produced by 1987 Hill–Wichmann–Woodall reference implementation // Compare ers97.blt count with result produced by 1987 Hill–Wichmann–Woodall reference implementation
#[test] #[test]
@ -86,11 +85,8 @@ fn meek06_ers97_fixed12() {
Fixed::set_dps(12); Fixed::set_dps(12);
// Read BLT // Read BLT
let file = File::open("tests/data/ers97.blt").expect("IO Error"); let file = fs::read_to_string("tests/data/ers97.blt").expect("IO Error");
let file_reader = io::BufReader::new(file); let election: Election<Fixed> = Election::from_blt(&file);
let lines = file_reader.lines();
let election: Election<Fixed> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
// Initialise count state // Initialise count state
let mut state = CountState::new(&election); let mut state = CountState::new(&election);
@ -162,11 +158,8 @@ fn meeknz_ers97_fixed12() {
Fixed::set_dps(12); Fixed::set_dps(12);
// Read BLT // Read BLT
let file = File::open("tests/data/ers97.blt").expect("IO Error"); let file = fs::read_to_string("tests/data/ers97.blt").expect("IO Error");
let file_reader = io::BufReader::new(file); let election: Election<Fixed> = Election::from_blt(&file);
let lines = file_reader.lines();
let election: Election<Fixed> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
// Initialise count state // Initialise count state
let mut state = CountState::new(&election); let mut state = CountState::new(&election);

View File

@ -15,16 +15,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// https://web.archive.org/web/20121004213938/http://www.glasgow.gov.uk/en/YourCouncil/Elections_Voting/Election_Results/ElectionScotland2007/LGWardResults.htm?ward=1&wardname=1%20-%20Linn
use opentally::election::{CandidateState, CountState, Election}; use opentally::election::{CandidateState, CountState, Election};
use opentally::numbers::{Fixed, GuardedFixed, Number}; use opentally::numbers::{Fixed, GuardedFixed, Number};
use opentally::stv; use opentally::stv;
use xmltree::Element; use xmltree::Element;
use std::io::{self, BufRead}; use std::fs::{self, File};
use std::fs::File;
use std::ops; use std::ops;
#[test] #[test]
@ -114,11 +111,8 @@ where
let num_stages = root.get_child("headerrow").expect("Syntax Error").children.len(); let num_stages = root.get_child("headerrow").expect("Syntax Error").children.len();
// Read BLT // Read BLT
let file = File::open("tests/data/linn07.blt").expect("IO Error"); let file = fs::read_to_string("tests/data/linn07.blt").expect("IO Error");
let file_reader = io::BufReader::new(file); let mut election: Election<N> = Election::from_blt(&file);
let lines = file_reader.lines();
let mut election: Election<N> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
// !!! FOR SCOTTISH STV !!! // !!! FOR SCOTTISH STV !!!
election.normalise_ballots(); election.normalise_ballots();

View File

@ -21,8 +21,7 @@ use opentally::stv;
use csv::StringRecord; use csv::StringRecord;
use std::fs::File; use std::fs;
use std::io::{self, BufRead};
use std::ops; use std::ops;
#[allow(dead_code)] // Suppress false positive #[allow(dead_code)] // Suppress false positive
@ -48,11 +47,9 @@ where
let stages: Vec<usize> = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect(); let stages: Vec<usize> = records.first().unwrap().iter().skip(1).step_by(2).map(|s| s.parse().unwrap()).collect();
// Read BLT // Read BLT
let file = File::open(blt_file).expect("IO Error"); let file = fs::read_to_string(blt_file).expect("IO Error");
let file_reader = io::BufReader::new(file);
let lines = file_reader.lines();
let election: Election<N> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter()); let election: Election<N> = Election::from_blt(&file);
// Validate candidate names // Validate candidate names
for (i, candidate) in candidates.iter().enumerate() { for (i, candidate) in candidates.iter().enumerate() {