Open-source election counting
https://yingtongli.me/opentally/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
399 lines
10 KiB
399 lines
10 KiB
/* OpenTally: Open-source election vote counting
|
|
* Copyright © 2021–2022 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;
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use utf8_chars::BufReadCharsExt;
|
|
|
|
use std::fmt;
|
|
use std::iter::Peekable;
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use std::fs::File;
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use std::io::BufReader;
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use std::path::Path;
|
|
|
|
/// 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
|
|
ballot_value_buf: String,
|
|
/// Current line number
|
|
line_no: u32,
|
|
/// Current column number
|
|
col_no: u32,
|
|
|
|
/// Number of candidates
|
|
num_candidates: usize,
|
|
/// Parsed [Election]
|
|
election: Election<N>,
|
|
}
|
|
|
|
/// An error when parsing a BLT file
|
|
pub enum ParseError {
|
|
/// Unexpected character
|
|
Unexpected(u32, u32, char),
|
|
/// Unexpected character, expected ...
|
|
Expected(u32, u32, char, &'static str),
|
|
}
|
|
|
|
impl fmt::Display for ParseError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
ParseError::Unexpected(line_no, col_no, char) => {
|
|
f.write_fmt(format_args!("Line {} col {}, unexpected '{}'", line_no, col_no, char))?;
|
|
}
|
|
ParseError::Expected(line_no, col_no, char, expected) => {
|
|
f.write_fmt(format_args!("Line {} col {}, unexpected '{}', expected {}", line_no, col_no, char, expected))?;
|
|
}
|
|
}
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for ParseError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
return fmt::Display::fmt(self, f);
|
|
}
|
|
}
|
|
|
|
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.ballot_value_buf.clear();
|
|
self.ballot_value_buf.push('0');
|
|
self.ballot()?;
|
|
} else {
|
|
// End of ballots
|
|
self.delimiter();
|
|
break;
|
|
}
|
|
} else {
|
|
self.ballot_value_buf.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<Vec<usize>> = Vec::new();
|
|
loop {
|
|
if self.lookahead() == '0' || self.lookahead() == '\n' {
|
|
// End of preferences
|
|
self.accept();
|
|
break;
|
|
} else if self.lookahead() == '=' {
|
|
// Equal preference
|
|
self.accept();
|
|
preferences.last_mut().unwrap().push(self.usize()? - 1);
|
|
self.delimiter_not_nl();
|
|
} else {
|
|
// No equal preference
|
|
preferences.push(vec![self.usize()? - 1]);
|
|
self.delimiter_not_nl();
|
|
}
|
|
}
|
|
|
|
self.delimiter();
|
|
|
|
let ballot = Ballot {
|
|
orig_value: N::parse(&self.ballot_value_buf),
|
|
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,
|
|
is_dummy: false,
|
|
});
|
|
}
|
|
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.ballot_value_buf.push(d); }
|
|
}
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
/// Parse a quoted or raw string
|
|
fn string(&mut self) -> Result<String, ParseError> {
|
|
if let Ok(s) = self.quoted_string() {
|
|
return Ok(s);
|
|
}
|
|
if let Ok(s) = self.raw_string() {
|
|
return Ok(s);
|
|
}
|
|
return Err(ParseError::Expected(self.line_no, self.col_no, self.lookahead(), "string"));
|
|
}
|
|
|
|
/// 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::Expected(self.line_no, self.col_no, self.lookahead(), "'\"'"));
|
|
}
|
|
}
|
|
|
|
/// 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::Expected(self.line_no, self.col_no, self.lookahead(), "nonzero digit"));
|
|
}
|
|
}
|
|
|
|
/// 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::Expected(self.line_no, self.col_no, self.lookahead(), "digit"));
|
|
}
|
|
}
|
|
|
|
/// 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::Expected(self.line_no, self.col_no, self.lookahead(), "number"));
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
return *self.chars.peek().expect("Unexpected EOF");
|
|
}
|
|
|
|
/// Read and return one character from the stream
|
|
fn accept(&mut self) -> char {
|
|
let result = self.chars.next().expect("Unexpected EOF");
|
|
if result == '\n' {
|
|
self.line_no += 1;
|
|
self.col_no = 1;
|
|
} else {
|
|
self.col_no += 1;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// PUBLIC API
|
|
|
|
/// Return a new [BLTParser]
|
|
pub fn new(chars: Peekable<I>) -> Self {
|
|
Self {
|
|
chars,
|
|
ballot_value_buf: String::new(),
|
|
line_no: 1,
|
|
col_no: 1,
|
|
num_candidates: 0,
|
|
election: Election {
|
|
name: String::new(),
|
|
seats: 0,
|
|
candidates: Vec::new(),
|
|
withdrawn_candidates: Vec::new(),
|
|
ballots: Vec::new(),
|
|
total_votes: None,
|
|
constraints: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Return the parsed [Election]
|
|
pub fn as_election(self) -> Election<N> {
|
|
return self.election;
|
|
}
|
|
}
|
|
|
|
/// Parse the given BLT file and return an [Election]
|
|
pub fn parse_iterator<I: Iterator<Item=char>, N: Number>(input: Peekable<I>) -> Result<Election<N>, ParseError> {
|
|
let mut parser = BLTParser::new(input);
|
|
parser.parse_blt()?;
|
|
return Ok(parser.as_election());
|
|
}
|
|
|
|
/// Parse the BLT file at the given path and return an [Election]
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn parse_path<P: AsRef<Path>, N: Number>(path: P) -> Result<Election<N>, ParseError> {
|
|
let mut reader = BufReader::new(File::open(path).expect("IO Error"));
|
|
let chars = reader.chars().map(|r| r.expect("IO Error")).peekable();
|
|
return parse_iterator(chars);
|
|
}
|
|
|