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", "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]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.5" version = "0.3.5"
@ -166,12 +214,58 @@ dependencies = [
"ansi_term", "ansi_term",
"atty", "atty",
"bitflags 1.3.2", "bitflags 1.3.2",
"strsim", "strsim 0.8.0",
"textwrap", "textwrap",
"unicode-width", "unicode-width",
"vec_map", "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]] [[package]]
name = "cookie" name = "cookie"
version = "0.18.0" version = "0.18.0"
@ -439,6 +533,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.19" version = "0.1.19"
@ -1177,13 +1277,19 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "strsim"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]] [[package]]
name = "structopt" name = "structopt"
version = "0.3.26" version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
dependencies = [ dependencies = [
"clap", "clap 2.34.0",
"lazy_static", "lazy_static",
"structopt-derive", "structopt-derive",
] ]
@ -1194,7 +1300,7 @@ version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
dependencies = [ dependencies = [
"heck", "heck 0.3.3",
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1489,6 +1595,12 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.7.0" version = "1.7.0"
@ -1521,6 +1633,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
name = "vote-rs" name = "vote-rs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap 4.5.3",
"csv", "csv",
"rand", "rand",
"rocket", "rocket",

View File

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

View File

@ -1,29 +1,12 @@
# Vote-rs # Vote-rs
This is a simple Rust program that reads a file of voting records and counts the votes for each nominee. currently in development
## 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.
## Usage creates a .blt file from a csv file
To run the program, use the following command: to build
```bash ```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::fs::File;
use std::process; use std::io::prelude::*;
use structopt::StructOpt; use std::fs::OpenOptions;
use std::error::Error; use std::path::Path;
use std::io; //import for taking application flags
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, StructOpt)] #[derive(Clone)]
#[structopt(name = "example", about = "An example of StructOpt usage.")] struct Ballot {
struct Opt {//options weight: i32,
/// File to process candidates: Vec<i32>,
#[structopt(parse(from_os_str))]
file: PathBuf,
/// Amount to select
#[structopt(short = "a", long = "amount")]
amount: usize,
} }
#[derive(Deserialize, Debug, Clone)] struct Candidate {
struct Vote { id: i32,
name: String, name: String,
value: f64,
} }
#[derive(Deserialize, Debug, Clone)] struct ElectionData {
struct Score { num_candidates: i32,
name: String, num_seats: i32,
value: f64, ballots: Vec<Ballot>,
} candidates: Vec<Candidate>,
title: String,
#[derive(Deserialize, Debug, Clone)]
struct Record {
votes: Vec<Vote>,
} }
fn read_to_records(file: &PathBuf) -> Result<Vec<Record>, Box<dyn Error>> { fn election_data_to_blt(election_data: ElectionData) -> String {
let file = std::fs::File::open(file)?; //convert the election data to a blt file
let reader = io::BufReader::new(file); //format
//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 // 7 4 # 7 candidates, 4 seats
let name_vec: Vec<Vec<String>> = serde_json::from_reader(reader)?; //1 1 3 0 # weigth, candidate ids, 0
let mut records: Vec<Record> = Vec::new(); // ...
for names in name_vec { //0
let mut votes: Vec<Vote> = Vec::new(); //"a" # candidate name
for (i, name) in names.iter().enumerate() { //...
let vote = Vote { //"title" # election title
name: name.clone().to_string().to_lowercase().replace(" ", "_").replace("-","_").replace("__", "_"),
value: if i == 0 { 1.0 } else { 0.0 }, let mut blt = String::new();
}; //push the number of candidates and seats
votes.push(vote); 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 }; blt.push_str("0\n");
records.push(record);
} }
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));
Ok(records) return blt;
} }
//get all unique names from the records fn csv_to_election_data(csv: &str, num_seats: i32, title: String) -> ElectionData {
fn get_unique_names(records: &Vec<Record>) -> Vec<String> { //read the csv file and convert it to a ballot
let mut names: HashMap<String, bool> = HashMap::new(); let delimiter = ",";
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| { //read the csv file
let value_cmp = b.value.partial_cmp(&a.value).unwrap(); let mut file = File::open(csv).expect("file not found");
if value_cmp == std::cmp::Ordering::Equal {
a.name.cmp(&b.name) //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']
} else { let mut contents = String::new();
value_cmp 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);
}
//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() { fn main() {
let opt = Opt::from_args(); let args: Args = Args::parse();
let records = read_to_records(&opt.file).unwrap_or_else(|err| {
println!("Error reading file: {}", err); let election_data = csv_to_election_data(&args.csv, args.seats, args.title);
process::exit(1); let blt = election_data_to_blt(election_data);
}); //if output is not specified, print to stdout
println!("Records: {:?}", records); if args.output == "" {
let nominees = get_unique_names(&records); println!("{}", blt);
println!("Nominees: {:?}", nominees); return;
let counts = count_votes(&records); }
println!("Counts: {:?}", counts);
let winners = stv(&records, opt.amount); let file_path = &args.output;
println!("Winners: {:?}", winners); 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");
}
}