created csv to blt converter
This commit is contained in:
parent
add30f3515
commit
a88045226b
119
Cargo.lock
generated
119
Cargo.lock
generated
@ -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",
|
||||
|
@ -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"] }
|
||||
|
27
README.md
27
README.md
@ -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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
cargo +nightly -Z unstable-options build --out-dir ./build
|
||||
```
|
18
example/sample.blt
Normal file
18
example/sample.blt
Normal 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
10
example/sample.csv
Normal 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
|
|
292
src/main.rs
292
src/main.rs
@ -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));
|
||||
|
||||
Ok(records)
|
||||
return blt;
|
||||
}
|
||||
|
||||
|
||||
//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();
|
||||
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 = ",";
|
||||
|
||||
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
|
||||
//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);
|
||||
}
|
||||
|
||||
//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 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 winners = stv(&records, opt.amount);
|
||||
println!("Winners: {:?}", winners);
|
||||
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user