diff --git a/Cargo.lock b/Cargo.lock index fb53a9c..fb197fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index ca5fae2..6dae0f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/README.md b/README.md index 40522e0..e9212d8 100644 --- a/README.md +++ b/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. Each Record contains a Vec, where each Vote has a name and a value. - -The get_unique_names function is used to get a Vec of the unique names in the records. - -The count_votes function is used to count the votes for each name. It returns a Vec where each Vote has a name and a value that is the total of the votes for that name. The Vec is sorted in descending order by vote value. - +cargo +nightly -Z unstable-options build --out-dir ./build +``` \ No newline at end of file diff --git a/example/sample.blt b/example/sample.blt new file mode 100644 index 0000000..d08b581 --- /dev/null +++ b/example/sample.blt @@ -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 \ No newline at end of file diff --git a/example/sample.csv b/example/sample.csv new file mode 100644 index 0000000..07cfc28 --- /dev/null +++ b/example/sample.csv @@ -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 \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 48a3a41..7320682 100644 --- a/src/main.rs +++ b/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, } -#[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, +struct ElectionData { + num_candidates: i32, + num_seats: i32, + ballots: Vec, + candidates: Vec, + title: String, } -fn read_to_records(file: &PathBuf) -> Result, Box> { - 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> = serde_json::from_reader(reader)?; - let mut records: Vec = Vec::new(); - for names in name_vec { - let mut votes: Vec = 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) -> Vec { - let mut names: HashMap = 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) -> Vec { - let mut counts: HashMap = 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 = 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::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, amount: usize) -> Vec { - let mut records: Vec<_> = recs.iter().map(|x| (*x).clone()).collect(); - let mut winners: Vec = 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 = 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 = 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 = 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"); + } +} \ No newline at end of file