335 lines
7.9 KiB
Rust
335 lines
7.9 KiB
Rust
/* 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/>.
|
|
*/
|
|
|
|
use crate::election::{Ballot, Candidate, Election};
|
|
use crate::numbers::Number;
|
|
|
|
use std::iter::Peekable;
|
|
|
|
/// 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>,
|
|
|
|
/// Temporary buffer for parsing ballot values
|
|
buf_ballot_value: String,
|
|
|
|
/// Number of candidates
|
|
num_candidates: usize,
|
|
/// Parsed [Election]
|
|
election: Election<N>,
|
|
}
|
|
|
|
/// An error when parsing a BLT file
|
|
#[derive(Debug)]
|
|
pub enum ParseError {
|
|
/// Invalid syntax
|
|
Invalid
|
|
}
|
|
|
|
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()?;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|