Use Pest-based parser for BLT files
Support comments, optional newlines, etc.
This commit is contained in:
parent
3b8ccd097e
commit
cca097f943
136
Cargo.lock
generated
136
Cargo.lock
generated
@ -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"
|
||||||
|
@ -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"
|
||||||
|
16
docs/blt.md
16
docs/blt.md
@ -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.
|
||||||
|
@ -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 ballots
|
// Read withdrawn candidates
|
||||||
for line in &mut lines {
|
let withdrawn_pairs = main.next().unwrap().into_inner();
|
||||||
if line == "0" {
|
for withdrawn_cand in withdrawn_pairs {
|
||||||
break;
|
let val = withdrawn_cand.as_str();
|
||||||
}
|
let val: usize = val[1..val.len()].parse().unwrap();
|
||||||
|
|
||||||
let mut bits = line.split(" ");
|
|
||||||
|
|
||||||
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);
|
election.withdrawn_candidates.push(val - 1);
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = N::parse(bits.next().expect("Syntax Error"));
|
// 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 {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read candidates
|
// Zero line
|
||||||
for line in lines.by_ref().take(num_candidates) {
|
main.next();
|
||||||
let mut line = &line[..];
|
|
||||||
if line.starts_with("\"") && line.ends_with("\"") {
|
|
||||||
line = &line[1..line.len()-1];
|
|
||||||
}
|
|
||||||
|
|
||||||
election.candidates.push(Candidate { name: line.to_string() });
|
// 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
|
// 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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");
|
||||||
|
13
src/main.rs
13
src/main.rs
@ -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
36
src/parser/blt.pest
Normal 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
44
src/parser/blt.rs
Normal 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
19
src/parser/mod.rs
Normal 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;
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
tests/aec.rs
10
tests/aec.rs
@ -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() {
|
||||||
|
@ -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");
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user