OpenTally/src/cli/convert.rs

156 lines
4.4 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::Election;
use crate::numbers::Rational;
use crate::parser;
use crate::writer;
use clap::{AppSettings, Clap};
use std::fs::File;
/// Convert between different ballot data formats
#[derive(Clap)]
#[clap(setting=AppSettings::DeriveDisplayOrder)]
pub struct SubcmdOptions {
/// Path to the input data file
#[clap(help_heading=Some("INPUT"))]
infile: String,
/// Format of input file
#[clap(help_heading=Some("INPUT"), short, long, possible_values=&["bin", "blt", "csp"], value_name="format")]
r#in: Option<String>,
/// Path to the output data file
#[clap(help_heading=Some("OUTPUT"))]
outfile: String,
/// Format of output file
#[clap(help_heading=Some("OUTPUT"), short, long, possible_values=&["bin", "blt", "csp"], value_name="format")]
out: Option<String>,
/// Number of seats
#[clap(help_heading=Some("ELECTION SPECIFICATION"), long)]
seats: Option<usize>,
/// Require 1st preference
#[clap(help_heading=Some("PREFERENCE VALIDATION"), long)]
require_1: bool,
/// Require sequential preferences
#[clap(help_heading=Some("PREFERENCE VALIDATION"), long)]
require_sequential: bool,
/// Require strict ordering of preferences (disallow equal rankings)
#[clap(help_heading=Some("PREFERENCE VALIDATION"), long)]
require_strict_order: bool,
/// Do not output wholly informal ballots
#[clap(help_heading=Some("PREFERENCE VALIDATION"), long)]
omit_informal: bool,
}
/// Entrypoint for subcommand
pub fn main(mut cmd_opts: SubcmdOptions) -> Result<(), i32> {
// Auto-detect input/output formats
if cmd_opts.r#in == None {
if cmd_opts.infile.ends_with(".bin") {
cmd_opts.r#in = Some("bin".to_string());
} else if cmd_opts.infile.ends_with(".blt") {
cmd_opts.r#in = Some("blt".to_string());
} else if cmd_opts.infile.ends_with(".csp") {
cmd_opts.r#in = Some("csp".to_string());
} else {
println!("Error: --in not specified and format cannot be determined from input filename");
return Err(1);
}
}
if cmd_opts.out == None {
if cmd_opts.outfile.ends_with(".bin") {
cmd_opts.out = Some("bin".to_string());
} else if cmd_opts.outfile.ends_with(".blt") {
cmd_opts.out = Some("blt".to_string());
} else if cmd_opts.outfile.ends_with(".csp") {
cmd_opts.out = Some("csp".to_string());
} else {
println!("Error: --out not specified and format cannot be determined from output filename");
return Err(1);
}
}
// Read input file
let mut election: Election<Rational>;
match cmd_opts.r#in.as_deref().unwrap() {
"bin" => {
election = parser::bin::parse_path(cmd_opts.infile);
}
"blt" => {
match parser::blt::parse_path(cmd_opts.infile) {
Ok(e) => {
election = e;
}
Err(err) => {
println!("Syntax Error: {}", err);
return Err(1);
}
}
}
"csp" => {
let file = File::open(cmd_opts.infile).expect("IO Error");
election = parser::csp::parse_reader(file, cmd_opts.require_1, cmd_opts.require_sequential, cmd_opts.require_strict_order).expect("Syntax Error");
}
_ => unreachable!()
};
match cmd_opts.seats {
Some(seats) => {
election.seats = seats;
}
None => {
if election.seats == 0 {
println!("Error: --seats must be specified with CSP input");
return Err(1);
}
}
}
if cmd_opts.omit_informal {
// Remove wholly informal ballots from output
election.ballots.retain(|b| !b.preferences.is_empty());
}
// Write output file
let output = File::create(cmd_opts.outfile).expect("IO Error");
match cmd_opts.out.as_deref().unwrap() {
"bin" => {
writer::bin::write(election, output);
}
"blt" => {
writer::blt::write(election, output);
}
"csp" => {
writer::csp::write(election, output);
}
_ => unreachable!()
}
return Ok(());
}