created csv to blt converter

This commit is contained in:
Adrian Gunnar Lauterer 2024-03-17 00:19:16 +01:00
parent add30f3515
commit a88045226b
Signed by: adriangl
GPG Key ID: D33368A59745C2F0
6 changed files with 311 additions and 156 deletions

119
Cargo.lock generated
View File

@ -35,6 +35,54 @@ dependencies = [
"winapi",
]
[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "async-stream"
version = "0.3.5"
@ -166,12 +214,58 @@ dependencies = [
"ansi_term",
"atty",
"bitflags 1.3.2",
"strsim",
"strsim 0.8.0",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "clap"
version = "4.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.0",
]
[[package]]
name = "clap_derive"
version = "4.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.52",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "cookie"
version = "0.18.0"
@ -439,6 +533,12 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -1177,13 +1277,19 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "strsim"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]]
name = "structopt"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
dependencies = [
"clap",
"clap 2.34.0",
"lazy_static",
"structopt-derive",
]
@ -1194,7 +1300,7 @@ version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
dependencies = [
"heck",
"heck 0.3.3",
"proc-macro-error",
"proc-macro2",
"quote",
@ -1489,6 +1595,12 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.7.0"
@ -1521,6 +1633,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
name = "vote-rs"
version = "0.1.0"
dependencies = [
"clap 4.5.3",
"csv",
"rand",
"rocket",

View File

@ -20,3 +20,4 @@ uuid = { version = "1.7.0", features = ["serde", "v4"] }
structopt = "0.3.23"
# for csv reading
csv = "1.1.6"
clap = { version = "4.5.3", features = ["derive"] }

View File

@ -1,29 +1,12 @@
# Vote-rs
This is a simple Rust program that reads a file of voting records and counts the votes for each nominee.
## goael
Implement a stv vote counting system in rust.
Could be based on rules and information found here: https://www.opavote.com/methods/single-transferable-vote
probably implement the scottish method https://www.legislation.gov.uk/ssi/2007/42/contents/made
because it is simpler and seems to be ok middle ground between complexity and fairness.
currently in development
## Usage
creates a .blt file from a csv file
To run the program, use the following command:
to build
```bash
cargo run -- sample.csv --amount 3
cargo +nightly -Z unstable-options build --out-dir ./build
```
Replace sample.csv with the path to your file of voting records.
amount is the number of nominees to select from the records.
## Functionality
The program reads the voting records from the specified file into a Vec<Record>. Each Record contains a Vec<Vote>, where each Vote has a name and a value.
The get_unique_names function is used to get a Vec<String> of the unique names in the records.
The count_votes function is used to count the votes for each name. It returns a Vec<Vote> where each Vote has a name and a value that is the total of the votes for that name. The Vec<Vote> is sorted in descending order by vote value.

18
example/sample.blt Normal file
View File

@ -0,0 +1,18 @@
4 2 # four candidates are competing for two seats
-2 # Bob has withdrawn (optional)
1 4 1 3 2 0 # first ballot
1 2 4 1 3 0
1 1 4 2 3 0 # The first number is the ballot weight (>= 1). Weigth is to deduplicate ballots.
1 1 2 4 3 0 # The last 0 is an end of ballot marker.
1 1 4 3 0 # Numbers in between correspond to the candidates
1 3 2 4 1 0 # on the ballot.
1 3 4 1 2 0
1 3 4 1 2 0 # Chuck, Diane, Amy, Bob
1 4 3 2 0
1 2 3 4 1 0 # last ballot
0 # end of ballots marker
"Amy" # candidate 1
"Bob" # candidate 2
"Chuck" # candidate 3
"Diane" # candidate 4
"Gardening Club Election" # title

10
example/sample.csv Normal file
View File

@ -0,0 +1,10 @@
Diane,Amy,Chuck,Bob
Bob,Diane,Amy,Chuck
Amy,Diane,Bob,Chuck
Amy,Bob,Diane,Chuck
Amy,Diane,Chuck
Chuck,Bob,Diane,Amy
Chuck,Diane,Amy,Bob
Chuck,Diane,Amy,Bob
Diane,Chuck,Bob
Bob,Chuck,Diane,Amy
1 Diane,Amy,Chuck,Bob
2 Bob,Diane,Amy,Chuck
3 Amy,Diane,Bob,Chuck
4 Amy,Bob,Diane,Chuck
5 Amy,Diane,Chuck
6 Chuck,Bob,Diane,Amy
7 Chuck,Diane,Amy,Bob
8 Chuck,Diane,Amy,Bob
9 Diane,Chuck,Bob
10 Bob,Chuck,Diane,Amy

View File

@ -1,158 +1,188 @@
use std::path::PathBuf;
use std::process;
use structopt::StructOpt;
use std::error::Error;
use std::io;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use std::fs::OpenOptions;
use std::path::Path;
//import for taking application flags
#[derive(Debug, StructOpt)]
#[structopt(name = "example", about = "An example of StructOpt usage.")]
struct Opt {//options
/// File to process
#[structopt(parse(from_os_str))]
file: PathBuf,
/// Amount to select
#[structopt(short = "a", long = "amount")]
amount: usize,
#[derive(Clone)]
struct Ballot {
weight: i32,
candidates: Vec<i32>,
}
#[derive(Deserialize, Debug, Clone)]
struct Vote {
struct Candidate {
id: i32,
name: String,
value: f64,
}
#[derive(Deserialize, Debug, Clone)]
struct Score {
name: String,
value: f64,
}
#[derive(Deserialize, Debug, Clone)]
struct Record {
votes: Vec<Vote>,
struct ElectionData {
num_candidates: i32,
num_seats: i32,
ballots: Vec<Ballot>,
candidates: Vec<Candidate>,
title: String,
}
fn read_to_records(file: &PathBuf) -> Result<Vec<Record>, Box<dyn Error>> {
let file = std::fs::File::open(file)?;
let reader = io::BufReader::new(file);
//read the json file with a list of lists with names into a vector of records, where each record is a list of votes containing a name and a value. the first name in each list has value 1, the following names have value 0
let name_vec: Vec<Vec<String>> = serde_json::from_reader(reader)?;
let mut records: Vec<Record> = Vec::new();
for names in name_vec {
let mut votes: Vec<Vote> = Vec::new();
for (i, name) in names.iter().enumerate() {
let vote = Vote {
name: name.clone().to_string().to_lowercase().replace(" ", "_").replace("-","_").replace("__", "_"),
value: if i == 0 { 1.0 } else { 0.0 },
};
votes.push(vote);
fn election_data_to_blt(election_data: ElectionData) -> String {
//convert the election data to a blt file
//format
// 7 4 # 7 candidates, 4 seats
//1 1 3 0 # weigth, candidate ids, 0
// ...
//0
//"a" # candidate name
//...
//"title" # election title
let mut blt = String::new();
//push the number of candidates and seats
blt.push_str(&format!("{} {}\n", election_data.num_candidates, election_data.num_seats));
for ballot in &election_data.ballots {
blt.push_str(&format!("{} ", ballot.weight));
for candidate in &ballot.candidates {
blt.push_str(&format!("{} ", candidate)); // Use candidate directly
}
let record = Record { votes };
records.push(record);
blt.push_str("0\n");
}
blt.push_str("0\n");
for candidate in &election_data.candidates {
blt.push_str(&format!("\"{}\"\n", candidate.name));
}
blt.push_str(&format!("\"{}\"\n", election_data.title));
return blt;
}
fn csv_to_election_data(csv: &str, num_seats: i32, title: String) -> ElectionData {
//read the csv file and convert it to a ballot
let delimiter = ",";
//read the csv file
let mut file = File::open(csv).expect("file not found");
//convert the csv file to a list of lists [ ['a', 'c', 'b'], ['b', 'a', 'd'] ] where each line is a array like ['a', 'c', 'b']
let mut contents = String::new();
file.read_to_string(&mut contents).expect("something went wrong reading the file");
let lines: Vec<&str> = contents.split("\n").collect();
let mut votes: Vec<Vec<&str>> = Vec::new();
for line in lines {
let vote: Vec<&str> = line.split(delimiter).collect();
votes.push(vote);
}
Ok(records)
}
//get all unique names from the records
fn get_unique_names(records: &Vec<Record>) -> Vec<String> {
let mut names: HashMap<String, bool> = HashMap::new();
for record in records {
for vote in &record.votes {
names.entry(vote.name.clone()).or_insert(true);
}
}
names.keys().cloned().collect()
}
fn count_votes(records: &Vec<Record>) -> Vec<Score> {
let mut counts: HashMap<String, f64> = HashMap::new();
for record in records {
for vote in &record.votes {
*counts.entry(vote.name.clone()).or_insert(0.0) += vote.value;
}
}
let mut scores: Vec<Score> = counts.into_iter()
.map(|(name, value)| Score { name, value })
.collect();
scores.sort_by(|a, b| {
let value_cmp = b.value.partial_cmp(&a.value).unwrap();
if value_cmp == std::cmp::Ordering::Equal {
a.name.cmp(&b.name)
} else {
value_cmp
//get all the unique candidates from all the votes and create a list of candidates
let mut names: Vec<&str> = Vec::new();
for vote in &votes {
for name in vote {
if !names.contains(name) {
names.push(name);
}
});
scores
}
// (3) The vote on each ballot paper transferred under paragraph (2) shall have a value ("the transfer value") calculated as follows—
// A divided by B
// Where
// A = the value which is calculated by multiplying the surplus of the transferring candidate by the value of the ballot paper when received by that candidate; and
// B = the total number of votes credited to that candidate,
// the calculation being made to five decimal places (any remainder being ignored).
//TODO: implement the quota calculation
fn exceeds_quota(score: &Score, quota: f64) -> bool {
score.value >= quota
}
//TODO: implement the stv algorithm
fn stv(recs: &Vec<Record>, amount: usize) -> Vec<Score> {
let mut records: Vec<_> = recs.iter().map(|x| (*x).clone()).collect();
let mut winners: Vec<Score> = Vec::new();
let total_votes = records.len() as f64;
let mut round = 0;
while winners.len() < amount {
let quota = total_votes / (amount as f64 + 1.0);
round += 1;
let scores = count_votes(&records);
println!("Round {}: {:?}", round, scores);
winners.append(&mut scores.iter().filter(|x| exceeds_quota(x, quota)).cloned().collect());
if winners.len() >= amount {
break;
}
//do soemthing random for now until i actually implement the stv algorithm.
//remove the last candidate from the records
let names = get_unique_names(&records);
//remove the last name from the records
records.retain(|x| x.votes.iter().any(|y| y.name != names[names.len() - 1]));
//add the first candidate to the winners
winners.push(scores[0].clone());
}
winners
let num_candidates = names.len() as i32;
let mut candidates: Vec<Candidate> = Vec::new();
for (i, name) in names.iter().enumerate() {
let candidate = Candidate {
id: (i + 1) as i32, // Add 1 to make it 1-indexed
name: name.to_string(),
};
candidates.push(candidate);
}
//generate the ballots
let mut ballots: Vec<Ballot> = Vec::new();
for vote in &votes {
let mut ballot = Ballot {
weight: 1,
candidates: Vec::new(),
};
for name in vote {
for candidate in &candidates {
if candidate.name == *name {
ballot.candidates.push(candidate.id);
}
}
}
ballots.push(ballot);
}
//deduplicate the ballots
let mut deduped_ballots: Vec<Ballot> = Vec::new();
for ballot in ballots {
let mut found = false;
for deduped_ballot in &deduped_ballots {
if deduped_ballot.candidates == ballot.candidates {
found = true;
break;
}
}
if !found {
deduped_ballots.push(ballot);
}
}
ballots = deduped_ballots;
let election_data = ElectionData {
num_candidates: num_candidates,
num_seats: num_seats,
ballots: ballots,
candidates: candidates,
title: title,
};
return election_data;
}
use clap::Parser;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
//number of seats to be elected
#[clap(short, long, default_value = "1")]
seats: i32,
//title of the election
#[clap(short, long)]
title: String,
//csv file containing the votes
#[clap(short, long)]
csv: String,
//output file
#[clap(short, long, default_value = "")]
output: String,
}
fn main() {
let opt = Opt::from_args();
let records = read_to_records(&opt.file).unwrap_or_else(|err| {
println!("Error reading file: {}", err);
process::exit(1);
});
println!("Records: {:?}", records);
let nominees = get_unique_names(&records);
println!("Nominees: {:?}", nominees);
let counts = count_votes(&records);
println!("Counts: {:?}", counts);
let args: Args = Args::parse();
let winners = stv(&records, opt.amount);
println!("Winners: {:?}", winners);
let election_data = csv_to_election_data(&args.csv, args.seats, args.title);
let blt = election_data_to_blt(election_data);
//if output is not specified, print to stdout
if args.output == "" {
println!("{}", blt);
return;
}
let file_path = &args.output;
if Path::new(file_path).exists() {
panic!("File already exists");
} else {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(file_path)
.expect("Failed to create file");
file.write_all(blt.as_bytes()).expect("write failed");
}
}