Compare commits
No commits in common. "17a6c88dbc963840e3404db4261916b147ad6c02" and "9d8d601c6a1a9129caeeb40859fc7f0bb83e5f0e" have entirely different histories.
17a6c88dbc
...
9d8d601c6a
|
|
@ -7,6 +7,7 @@ name = "ballot-counter"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"quick-csv",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -23,3 +24,18 @@ checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
|||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-csv"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edf4b7701db7d2e4c9c010f21eebd8676a50f79223a0cf858162d24bff47338c"
|
||||
dependencies = [
|
||||
"rustc-serialize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-serialize"
|
||||
version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401"
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ edition = "2024"
|
|||
|
||||
[dependencies]
|
||||
itertools = "0.14.0"
|
||||
quick-csv = "0.1.6"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use itertools::Itertools;
|
||||
|
||||
use crate::{csv, header::Header, stats::Stats};
|
||||
use crate::{header::Header, stats::Stats};
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -12,10 +12,10 @@ pub struct Ballot {
|
|||
const CANDIDATE_MIN: usize = 6;
|
||||
|
||||
impl Ballot {
|
||||
pub fn parse(mut row: csv::reader::RowReader, header: &Header, stats: &mut Stats) -> Result<Ballot, Box<dyn std::error::Error>> {
|
||||
let mut cols = row.by_ref().skip(6);
|
||||
pub fn parse(row: quick_csv::Row, header: &Header, stats: &mut Stats) -> Result<Ballot, Box<dyn std::error::Error>> {
|
||||
let mut cols = row.columns()?.skip(6);
|
||||
|
||||
let place_filter = |(index, place): (usize, String)| match place.parse::<i32>() {
|
||||
let place_filter = |(index, place): (usize, &str)| match place.parse::<i32>() {
|
||||
Ok(place) => Some((place, index)),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
|
@ -45,9 +45,6 @@ impl Ballot {
|
|||
}
|
||||
|
||||
else {
|
||||
println!("abl votes: {votes_party:?}");
|
||||
println!("btl votes: {votes_candidate:?}");
|
||||
println!("at: {}", row.get_line_number());
|
||||
return Err("ballot is informal".into());
|
||||
}
|
||||
|
||||
|
|
@ -60,9 +57,11 @@ impl Ballot {
|
|||
pub fn apply_weight(&mut self, weight: f64) {
|
||||
self.weight *= weight;
|
||||
}
|
||||
pub fn get_primary_index(&self, enabled: &[bool]) -> Option<usize> {
|
||||
pub fn get_primary_index<F>(&self, enabled: F) -> Option<usize>
|
||||
where F: Fn(usize) -> bool
|
||||
{
|
||||
for &id in self.votes.iter() {
|
||||
if enabled[id] {
|
||||
if enabled(id) {
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{sync::{Arc, Mutex}, thread};
|
||||
use std::rc::Rc;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{ballot::Ballot, csv, header::Header, stats::Stats, util::ScoreItem};
|
||||
use crate::{ballot::Ballot, header::Header, stats::Stats, util::ScoreItem};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
|
|
@ -11,7 +11,7 @@ pub enum Event {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct Counter {
|
||||
pub header: Arc<Header>,
|
||||
pub header: Rc<Header>,
|
||||
pub ballots: Vec<Ballot>,
|
||||
pub stats: Stats,
|
||||
winners_left: usize,
|
||||
|
|
@ -20,37 +20,18 @@ pub struct Counter {
|
|||
}
|
||||
|
||||
impl Counter {
|
||||
pub fn new<'a,I>(mut csv: I, winners: usize) -> Result<Self, Box<dyn std::error::Error>>
|
||||
where I: Iterator<Item=csv::reader::RowReader<'a>> + Send + Sync
|
||||
{
|
||||
let header = Arc::new(Header::parse(csv.next().ok_or("csv header missing")?, winners)?);
|
||||
let ballots = Mutex::new(Vec::new());
|
||||
let stats = Mutex::new(Stats::new());
|
||||
let csv = Mutex::new(csv);
|
||||
pub fn new<T: std::io::BufRead>(mut csv: quick_csv::Csv<T>, winners: usize) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let header = Rc::new(Header::parse(csv.next().ok_or("csv header missing")??, winners)?);
|
||||
let mut ballots = Vec::new();
|
||||
let mut stats = Stats::new();
|
||||
|
||||
if winners > header.candidates.len() {
|
||||
return Err("winners can't be smaller than the candidates list".into());
|
||||
}
|
||||
|
||||
thread::scope(|s| {
|
||||
let threads = std::thread::available_parallelism().unwrap().into();
|
||||
for _ in 0..threads {
|
||||
s.spawn(|| {
|
||||
let mut l_stats = Stats::new();
|
||||
let mut l_ballots = Vec::new();
|
||||
while let Some(mut row) = { csv.lock().unwrap().next() } {
|
||||
if !row.check_empty() {
|
||||
l_ballots.push(Ballot::parse(row, &header, &mut l_stats).unwrap());
|
||||
for row in csv {
|
||||
ballots.push(Ballot::parse(row?, &header, &mut stats)?);
|
||||
}
|
||||
}
|
||||
{ballots.lock().unwrap().append(&mut l_ballots)};
|
||||
{stats.lock().unwrap().add(&l_stats)};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let ballots = ballots.into_inner().unwrap();
|
||||
let stats = stats.into_inner().unwrap();
|
||||
|
||||
let enabled = vec![true; header.candidates.len()];
|
||||
let quota = (ballots.len() as f64) / (header.winners as f64 + 1.0) + 1.0;
|
||||
|
|
@ -66,55 +47,17 @@ impl Counter {
|
|||
})
|
||||
}
|
||||
fn count_primaries(&self) -> Vec<ScoreItem> {
|
||||
let mut scores = Mutex::new(vec![0.0; self.enabled.len()]);
|
||||
let ballot_chunks = Mutex::new(self.ballots.chunks(1024));
|
||||
|
||||
thread::scope(|s| {
|
||||
let threads = std::thread::available_parallelism().unwrap().into();
|
||||
for _ in 0..threads {
|
||||
s.spawn(|| {
|
||||
let mut l_scores = vec![0.0; self.enabled.len()];
|
||||
while let Some(ballots) = { ballot_chunks.lock().unwrap().next() } {
|
||||
for ballot in ballots {
|
||||
if let Some(index) = ballot.get_primary_index(&self.enabled) {
|
||||
l_scores[index] += ballot.get_weight();
|
||||
let mut scores = vec![0.0; self.enabled.len()];
|
||||
for ballot in self.ballots.iter() {
|
||||
if let Some(index) = ballot.get_primary_index(|id| self.enabled[id]) {
|
||||
scores[index] += ballot.get_weight();
|
||||
}
|
||||
}
|
||||
}
|
||||
for (dst, src) in scores.lock().unwrap().iter_mut().zip(l_scores.into_iter()) {
|
||||
*dst += src;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
scores.get_mut().unwrap().iter().copied().enumerate().filter_map(|(index, value)| match self.enabled[index] {
|
||||
scores.into_iter().enumerate().filter_map(|(index, value)| match self.enabled[index] {
|
||||
true => Some(ScoreItem::new(index, value)),
|
||||
false => None,
|
||||
}).collect_vec()
|
||||
}
|
||||
fn apply_weights(&mut self, winners: &[ScoreItem]) {
|
||||
let ballot_chunks = Mutex::new(self.ballots.chunks_mut(1024));
|
||||
thread::scope(|s| {
|
||||
let threads = std::thread::available_parallelism().unwrap().into();
|
||||
for _ in 0..threads {
|
||||
s.spawn(|| {
|
||||
while let Some(ballots) = { ballot_chunks.lock().unwrap().next() } {
|
||||
for ballot in ballots {
|
||||
let index = match ballot.get_primary_index(&self.enabled) {
|
||||
Some(v) => v,
|
||||
None => continue,
|
||||
};
|
||||
if let Some(winner) = winners.iter().filter(|&v| v.index == index).next() {
|
||||
let surplus = winner.value - self.quota;
|
||||
ballot.apply_weight(surplus / winner.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
pub fn get_quota(&self) -> f64 {
|
||||
self.quota
|
||||
}
|
||||
|
|
@ -146,9 +89,19 @@ impl Iterator for Counter {
|
|||
}
|
||||
|
||||
winners.sort_by(|a,b| b.cmp(a));
|
||||
self.apply_weights(&winners);
|
||||
self.winners_left -= winners.len();
|
||||
|
||||
for ballot in self.ballots.iter_mut() {
|
||||
let index = match ballot.get_primary_index(|id| self.enabled[id]) {
|
||||
Some(v) => v,
|
||||
None => continue,
|
||||
};
|
||||
if let Some(winner) = winners.iter().filter(|&v| v.index == index).next() {
|
||||
let surplus = winner.value - self.quota;
|
||||
ballot.apply_weight(surplus / winner.value);
|
||||
}
|
||||
}
|
||||
|
||||
self.winners_left -= winners.len();
|
||||
for winner in winners.iter() {
|
||||
self.enabled[winner.index] = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
|
||||
pub mod writer;
|
||||
pub mod reader;
|
||||
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
use std::{iter::Peekable, str::Chars};
|
||||
|
||||
|
||||
pub struct RowReader<'a> {
|
||||
it: Peekable<Chars<'a>>,
|
||||
delimiter: char,
|
||||
ended: bool,
|
||||
at: usize,
|
||||
}
|
||||
|
||||
impl<'a> RowReader<'a> {
|
||||
pub fn new(line: &'a str, at: usize, delimiter: char) -> Self {
|
||||
Self { it: line.chars().peekable(), at, delimiter, ended: false }
|
||||
}
|
||||
pub fn get_line_number(&self) -> usize {
|
||||
self.at
|
||||
}
|
||||
pub fn check_empty(&mut self) -> bool {
|
||||
self.it.peek().is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RowReader<'a> {
|
||||
type Item = String;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.ended {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut value = String::new();
|
||||
let mut escaped = false;
|
||||
let mut end_quote = false;
|
||||
let can_escape = self.it.peek().copied() == Some('"');
|
||||
|
||||
if can_escape {
|
||||
self.it.next();
|
||||
}
|
||||
|
||||
for ch in self.it.by_ref() {
|
||||
if escaped {
|
||||
value.push(ch);
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if can_escape {
|
||||
if ch == '\\' {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if ch == '"' {
|
||||
if end_quote {
|
||||
value.push(ch);
|
||||
}
|
||||
end_quote = !end_quote;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if !can_escape || end_quote {
|
||||
if ch == '\r' {
|
||||
continue;
|
||||
}
|
||||
if ch == self.delimiter {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
value.push(ch);
|
||||
}
|
||||
|
||||
self.ended = true;
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use std::collections::HashMap;
|
||||
use itertools::Itertools;
|
||||
use crate::{candidate::{Candidate, CandidateName}, csv, party::Party};
|
||||
use crate::{candidate::{Candidate, CandidateName}, party::Party};
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -11,12 +11,12 @@ pub struct Header {
|
|||
}
|
||||
|
||||
impl Header {
|
||||
pub fn parse(row: csv::reader::RowReader, winners: usize) -> Result<Header, Box<dyn std::error::Error>> {
|
||||
pub fn parse(row: quick_csv::Row, winners: usize) -> Result<Header, Box<dyn std::error::Error>> {
|
||||
let mut parties = Vec::<Party>::new();
|
||||
let mut candidates = Vec::<Candidate>::new();
|
||||
let mut parties_lookup = HashMap::<&str, usize>::new();
|
||||
|
||||
for col in row.skip(6).collect_vec().iter() {
|
||||
for col in row.columns()?.skip(6) {
|
||||
let [party, name] = col.split(':').next_array().ok_or("Missing ':'")?;
|
||||
|
||||
if party == "UG" { // independents
|
||||
|
|
|
|||
11
src/main.rs
11
src/main.rs
|
|
@ -1,5 +1,4 @@
|
|||
use std::{env, fs};
|
||||
|
||||
use std::env;
|
||||
use counter::Counter;
|
||||
use util::Percent;
|
||||
|
||||
|
|
@ -9,7 +8,6 @@ pub mod ballot;
|
|||
pub mod header;
|
||||
pub mod party;
|
||||
pub mod stats;
|
||||
pub mod csv;
|
||||
mod counter;
|
||||
|
||||
fn main() {
|
||||
|
|
@ -21,11 +19,8 @@ fn main() {
|
|||
}
|
||||
(args[1].clone(), args[2].parse::<usize>().unwrap())
|
||||
};
|
||||
|
||||
let csv = String::from_utf8(fs::read(&csv_path).unwrap()).unwrap();
|
||||
let rows = csv.split('\n').enumerate().map(|(i,v)| csv::reader::RowReader::new(v, i, ','));
|
||||
|
||||
let counter = Counter::new(rows, winner_count).unwrap();
|
||||
let csv = quick_csv::Csv::from_file(csv_path).unwrap().flexible(true);
|
||||
let counter = Counter::new(csv, winner_count).unwrap();
|
||||
let mut winners = Vec::with_capacity(winner_count);
|
||||
let total = counter.ballots.len() as f64;
|
||||
let header = counter.header.clone();
|
||||
|
|
|
|||
|
|
@ -20,12 +20,5 @@ impl Stats {
|
|||
both: 0,
|
||||
}
|
||||
}
|
||||
pub fn add(&mut self, rhs: &Self) {
|
||||
self.total += rhs.total;
|
||||
self.party += rhs.party;
|
||||
self.party_single += rhs.party_single;
|
||||
self.candidate += rhs.candidate;
|
||||
self.both += rhs.both;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
pub mod escape;
|
||||
pub mod percent;
|
||||
pub mod score_item;
|
||||
pub mod csv;
|
||||
|
||||
pub use score_item::ScoreItem;
|
||||
pub use escape::{EscapeWriter, EscapeWriterOpts};
|
||||
pub use percent::Percent;
|
||||
pub use csv::CsvWriter;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue