From 29ffe1190f357a80237706474dc9162f08dba240 Mon Sep 17 00:00:00 2001 From: Jay Robson Date: Sat, 3 May 2025 21:22:32 +1000 Subject: [PATCH] loader working --- Cargo.lock | 101 +++++++++-------------------------------------- Cargo.toml | 3 +- src/ballot.rs | 75 +++++++++++++++++++++++++++++++++++ src/candidate.rs | 7 ++++ src/header.rs | 46 +++++++++++++++++++++ src/main.rs | 40 ++++++++++++++++++- 6 files changed, 188 insertions(+), 84 deletions(-) create mode 100644 src/ballot.rs create mode 100644 src/candidate.rs create mode 100644 src/header.rs diff --git a/Cargo.lock b/Cargo.lock index 742b7f4..0a7180b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,99 +6,36 @@ version = 4 name = "ballot-counter" version = "0.1.0" dependencies = [ - "csv", + "itertools", + "quick-csv", ] [[package]] -name = "csv" -version = "1.3.1" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde", + "either", ] [[package]] -name = "csv-core" -version = "0.1.12" +name = "quick-csv" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "edf4b7701db7d2e4c9c010f21eebd8676a50f79223a0cf858162d24bff47338c" dependencies = [ - "memchr", + "rustc-serialize", ] [[package]] -name = "itoa" -version = "1.0.15" +name = "rustc-serialize" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "syn" -version = "2.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" diff --git a/Cargo.toml b/Cargo.toml index b3a9228..1b6162a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] -csv = "1.3.1" +itertools = "0.14.0" +quick-csv = "0.1.6" diff --git a/src/ballot.rs b/src/ballot.rs new file mode 100644 index 0000000..3910835 --- /dev/null +++ b/src/ballot.rs @@ -0,0 +1,75 @@ +use itertools::Itertools; + +use crate::header::Header; + +#[derive(Copy,Clone,Debug,PartialEq)] +pub enum BallotType { + Party, + Candidate, +} + + +#[derive(Debug)] +pub struct Ballot { + pub state: String, + pub division: String, + pub collection_point: String, + pub collection_point_id: usize, + pub batch_id: usize, + pub paper_id: usize, + pub votes: Vec, + pub ty: BallotType, +} + +impl Ballot { + pub fn parse_rows(csv: quick_csv::Csv, header: &Header) -> Result, Box> { + let mut ballots = Vec::::new(); + + let col_filter = |(index, place): (usize, &str)| match place.parse::() { + Ok(v) => Some((index, v - 1)), + Err(_) => None, + }; + + for result in csv { + let row = result?; + let mut cols = row.columns()?; + + let (state, division, collection_point, collection_point_id, batch_id, paper_id) = cols.next_tuple().ok_or("Missing columns")?; + let cols = cols.collect_vec(); + let mut votes: Vec>; + let ty: BallotType; + + // below the line votes + if cols.len() > header.parties.len() { + ty = BallotType::Candidate; + votes = vec![None; header.candidates.len()]; + for (col_id, place) in cols.iter().copied().skip(header.parties.len()).take(header.candidates.len()).enumerate().filter_map(col_filter) { + votes[place] = Some(col_id); + } + } + + // above the line votes + else { + ty = BallotType::Party; + votes = vec![None; header.parties.len()]; + for (col_id, place) in cols.iter().copied().take(header.parties.len()).enumerate().filter_map(col_filter) { + votes[place] = Some(col_id); + } + } + + ballots.push(Ballot { + state: state.to_owned(), + division: division.to_owned(), + collection_point: collection_point.to_owned(), + collection_point_id: collection_point_id.parse()?, + batch_id: batch_id.parse()?, + paper_id: paper_id.parse()?, + votes: votes.iter().copied().filter_map(|v| v).collect(), + ty, + }); + } + + Ok(ballots) + } +} + diff --git a/src/candidate.rs b/src/candidate.rs new file mode 100644 index 0000000..d34b0de --- /dev/null +++ b/src/candidate.rs @@ -0,0 +1,7 @@ + +#[derive(Debug)] +pub struct Candidate { + pub name: String, + pub party_id: Option, +} + diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000..a3d8605 --- /dev/null +++ b/src/header.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; + +use itertools::Itertools; + +use crate::candidate::Candidate; + + +#[derive(Debug)] +pub struct Header { + pub parties: Vec, + pub candidates: Vec, +} + +impl Header { + pub fn parse(row: quick_csv::Row) -> Result> { + let mut header = Header { + parties: Vec::new(), + candidates: Vec::new(), + }; + let mut parties_lookup = HashMap::<&str, usize>::new(); + + for col in row.columns()?.skip(6) { + let [party, name] = col.split(':').next_array().ok_or("Missing ':'")?; + + if party == "UG" { // independents + header.candidates.push(Candidate { + name: name.to_owned(), + party_id: None, + }); + } + else if let Some(party_id) = parties_lookup.get(party).copied() { + header.candidates.push(Candidate { + name: name.to_owned(), + party_id: Some(party_id), + }); + } + else { + parties_lookup.insert(party, header.parties.len()); + header.parties.push(name.to_owned()); + } + } + + Ok(header) + } +} + diff --git a/src/main.rs b/src/main.rs index a30eb95..5ee8940 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,41 @@ +use std::env; + +use ballot::{Ballot, BallotType}; +use header::Header; + +pub mod candidate; +mod ballot; +mod header; + fn main() { - println!("Hello, world!"); + let csv_path = { + let args: Vec = env::args().collect(); + if args.len() != 2 { + eprintln!("Usage: {} ", args[0]); + return; + } + args[1].clone() + }; + let mut csv = quick_csv::Csv::from_file(csv_path).unwrap().flexible(true); + let header = Header::parse(csv.next().unwrap().unwrap()).unwrap(); + let ballots = Ballot::parse_rows(csv, &header).unwrap(); + + println!("Parties: {}", header.parties.len()); + println!("Candidates: {}", header.candidates.len()); + + let total = ballots.len() as u32; + let mut total_atl = 0u32; + let mut total_btl = 0u32; + + for ballot in ballots.iter() { + *match ballot.ty { + BallotType::Party => &mut total_atl, + BallotType::Candidate => &mut total_btl, + } += 1; + } + + println!("Above the line: {total_atl} {}%", f64::from(total_atl) / f64::from(total) * 100.0); + println!("Below the line: {total_btl} {}%", f64::from(total_btl) / f64::from(total) * 100.0); + println!("Total: {total}"); } +