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"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itertools",
|
"itertools",
|
||||||
|
"quick-csv",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -23,3 +24,18 @@ checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"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]
|
[dependencies]
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
|
quick-csv = "0.1.6"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use crate::{csv, header::Header, stats::Stats};
|
use crate::{header::Header, stats::Stats};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -12,10 +12,10 @@ pub struct Ballot {
|
||||||
const CANDIDATE_MIN: usize = 6;
|
const CANDIDATE_MIN: usize = 6;
|
||||||
|
|
||||||
impl Ballot {
|
impl Ballot {
|
||||||
pub fn parse(mut row: csv::reader::RowReader, header: &Header, stats: &mut Stats) -> Result<Ballot, Box<dyn std::error::Error>> {
|
pub fn parse(row: quick_csv::Row, header: &Header, stats: &mut Stats) -> Result<Ballot, Box<dyn std::error::Error>> {
|
||||||
let mut cols = row.by_ref().skip(6);
|
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)),
|
Ok(place) => Some((place, index)),
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
};
|
};
|
||||||
|
|
@ -45,9 +45,6 @@ impl Ballot {
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
println!("abl votes: {votes_party:?}");
|
|
||||||
println!("btl votes: {votes_candidate:?}");
|
|
||||||
println!("at: {}", row.get_line_number());
|
|
||||||
return Err("ballot is informal".into());
|
return Err("ballot is informal".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,9 +57,11 @@ impl Ballot {
|
||||||
pub fn apply_weight(&mut self, weight: f64) {
|
pub fn apply_weight(&mut self, weight: f64) {
|
||||||
self.weight *= weight;
|
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() {
|
for &id in self.votes.iter() {
|
||||||
if enabled[id] {
|
if enabled(id) {
|
||||||
return Some(id);
|
return Some(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
103
src/counter.rs
103
src/counter.rs
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{sync::{Arc, Mutex}, thread};
|
use std::rc::Rc;
|
||||||
use itertools::Itertools;
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
|
|
@ -11,7 +11,7 @@ pub enum Event {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Counter {
|
pub struct Counter {
|
||||||
pub header: Arc<Header>,
|
pub header: Rc<Header>,
|
||||||
pub ballots: Vec<Ballot>,
|
pub ballots: Vec<Ballot>,
|
||||||
pub stats: Stats,
|
pub stats: Stats,
|
||||||
winners_left: usize,
|
winners_left: usize,
|
||||||
|
|
@ -20,37 +20,18 @@ pub struct Counter {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Counter {
|
impl Counter {
|
||||||
pub fn new<'a,I>(mut csv: I, winners: usize) -> Result<Self, Box<dyn std::error::Error>>
|
pub fn new<T: std::io::BufRead>(mut csv: quick_csv::Csv<T>, winners: usize) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
where I: Iterator<Item=csv::reader::RowReader<'a>> + Send + Sync
|
let header = Rc::new(Header::parse(csv.next().ok_or("csv header missing")??, winners)?);
|
||||||
{
|
let mut ballots = Vec::new();
|
||||||
let header = Arc::new(Header::parse(csv.next().ok_or("csv header missing")?, winners)?);
|
let mut stats = Stats::new();
|
||||||
let ballots = Mutex::new(Vec::new());
|
|
||||||
let stats = Mutex::new(Stats::new());
|
|
||||||
let csv = Mutex::new(csv);
|
|
||||||
|
|
||||||
if winners > header.candidates.len() {
|
if winners > header.candidates.len() {
|
||||||
return Err("winners can't be smaller than the candidates list".into());
|
return Err("winners can't be smaller than the candidates list".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
thread::scope(|s| {
|
for row in csv {
|
||||||
let threads = std::thread::available_parallelism().unwrap().into();
|
ballots.push(Ballot::parse(row?, &header, &mut stats)?);
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{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 enabled = vec![true; header.candidates.len()];
|
||||||
let quota = (ballots.len() as f64) / (header.winners as f64 + 1.0) + 1.0;
|
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> {
|
fn count_primaries(&self) -> Vec<ScoreItem> {
|
||||||
let mut scores = Mutex::new(vec![0.0; self.enabled.len()]);
|
let mut scores = vec![0.0; self.enabled.len()];
|
||||||
let ballot_chunks = Mutex::new(self.ballots.chunks(1024));
|
for ballot in self.ballots.iter() {
|
||||||
|
if let Some(index) = ballot.get_primary_index(|id| self.enabled[id]) {
|
||||||
thread::scope(|s| {
|
scores[index] += ballot.get_weight();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (dst, src) in scores.lock().unwrap().iter_mut().zip(l_scores.into_iter()) {
|
|
||||||
*dst += src;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
scores.into_iter().enumerate().filter_map(|(index, value)| match self.enabled[index] {
|
||||||
scores.get_mut().unwrap().iter().copied().enumerate().filter_map(|(index, value)| match self.enabled[index] {
|
|
||||||
true => Some(ScoreItem::new(index, value)),
|
true => Some(ScoreItem::new(index, value)),
|
||||||
false => None,
|
false => None,
|
||||||
}).collect_vec()
|
}).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 {
|
pub fn get_quota(&self) -> f64 {
|
||||||
self.quota
|
self.quota
|
||||||
}
|
}
|
||||||
|
|
@ -146,9 +89,19 @@ impl Iterator for Counter {
|
||||||
}
|
}
|
||||||
|
|
||||||
winners.sort_by(|a,b| b.cmp(a));
|
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() {
|
for winner in winners.iter() {
|
||||||
self.enabled[winner.index] = false;
|
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 std::collections::HashMap;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use crate::{candidate::{Candidate, CandidateName}, csv, party::Party};
|
use crate::{candidate::{Candidate, CandidateName}, party::Party};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -11,12 +11,12 @@ pub struct Header {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 parties = Vec::<Party>::new();
|
||||||
let mut candidates = Vec::<Candidate>::new();
|
let mut candidates = Vec::<Candidate>::new();
|
||||||
let mut parties_lookup = HashMap::<&str, usize>::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 ':'")?;
|
let [party, name] = col.split(':').next_array().ok_or("Missing ':'")?;
|
||||||
|
|
||||||
if party == "UG" { // independents
|
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 counter::Counter;
|
||||||
use util::Percent;
|
use util::Percent;
|
||||||
|
|
||||||
|
|
@ -9,7 +8,6 @@ pub mod ballot;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
pub mod party;
|
pub mod party;
|
||||||
pub mod stats;
|
pub mod stats;
|
||||||
pub mod csv;
|
|
||||||
mod counter;
|
mod counter;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -21,11 +19,8 @@ fn main() {
|
||||||
}
|
}
|
||||||
(args[1].clone(), args[2].parse::<usize>().unwrap())
|
(args[1].clone(), args[2].parse::<usize>().unwrap())
|
||||||
};
|
};
|
||||||
|
let csv = quick_csv::Csv::from_file(csv_path).unwrap().flexible(true);
|
||||||
let csv = String::from_utf8(fs::read(&csv_path).unwrap()).unwrap();
|
let counter = Counter::new(csv, winner_count).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 mut winners = Vec::with_capacity(winner_count);
|
let mut winners = Vec::with_capacity(winner_count);
|
||||||
let total = counter.ballots.len() as f64;
|
let total = counter.ballots.len() as f64;
|
||||||
let header = counter.header.clone();
|
let header = counter.header.clone();
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,5 @@ impl Stats {
|
||||||
both: 0,
|
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 escape;
|
||||||
pub mod percent;
|
pub mod percent;
|
||||||
pub mod score_item;
|
pub mod score_item;
|
||||||
|
pub mod csv;
|
||||||
|
|
||||||
pub use score_item::ScoreItem;
|
pub use score_item::ScoreItem;
|
||||||
pub use escape::{EscapeWriter, EscapeWriterOpts};
|
pub use escape::{EscapeWriter, EscapeWriterOpts};
|
||||||
pub use percent::Percent;
|
pub use percent::Percent;
|
||||||
|
pub use csv::CsvWriter;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue