added AEC formality checking rules

This commit is contained in:
Jay Robson 2025-05-07 17:35:58 +10:00
parent e403ab3b82
commit cb02be7d8e
5 changed files with 73 additions and 39 deletions

View File

@ -1,11 +1,6 @@
use itertools::Itertools;
use crate::header::Header;
#[derive(Copy,Clone,Debug,PartialEq)]
enum BallotType {
Party,
Candidate,
}
use crate::{header::Header, stats::Stats};
#[derive(Debug)]
@ -14,44 +9,43 @@ pub struct Ballot {
weight: f64,
}
impl Ballot {
pub fn parse(row: quick_csv::Row, header: &Header) -> Result<Ballot, Box<dyn std::error::Error>> {
let cols = row.columns()?.skip(6).collect_vec();
let mut votes: Vec<Option<usize>>;
let ty: BallotType;
const CANDIDATE_MIN: usize = 6;
let col_filter = |(index, place): (usize, &str)| match place.parse::<usize>() {
Ok(v) => Some((index, v - 1)),
impl Ballot {
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, &str)| match place.parse::<i32>() {
Ok(place) => Some((place, index)),
Err(_) => None,
};
// 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);
}
let mut votes_party = cols.by_ref().take(header.parties.len()).enumerate().filter_map(place_filter).collect_vec();
let mut votes_candidate = cols.take(header.candidates.len()).enumerate().filter_map(place_filter).collect_vec();
let votes;
if votes_candidate.len() > 0 && votes_party.len() > 0 {
stats.both += 1;
}
if votes_candidate.len() >= CANDIDATE_MIN.min(header.candidates.len()) {
votes_candidate.sort_by(|a, b| i32::cmp(&a.0, &b.0));
votes = votes_candidate.into_iter().map(|(_,id)| id).collect_vec();
stats.candidate += 1;
}
else if votes_party.len() > 0 {
votes_party.sort_by(|a, b| i32::cmp(&a.0, &b.0));
votes = votes_party.into_iter().map(|(_,id)| header.parties[id].member_ids.iter().copied()).flatten().collect_vec();
stats.party += 1;
}
// 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);
}
return Err("ballot is informal".into());
}
let votes = match ty {
BallotType::Candidate => votes.iter().filter_map(|&v| v).collect(),
BallotType::Party => votes.iter().filter_map(|&v| v).map(|id| header.parties[id].member_ids.iter().copied()).flatten().collect(),
};
Ok(Ballot {
weight: 1.0,
votes,
})
stats.total += 1;
Ok(Ballot { weight: 1.0, votes })
}
pub fn get_weight(&self) -> f64 {
self.weight

View File

@ -1,7 +1,7 @@
use std::rc::Rc;
use itertools::Itertools;
use crate::{ballot::Ballot, header::Header, util::ScoreItem};
use crate::{ballot::Ballot, header::Header, stats::Stats, util::ScoreItem};
#[derive(Debug)]
pub enum Event {
@ -13,6 +13,7 @@ pub enum Event {
pub struct Counter {
pub header: Rc<Header>,
pub ballots: Vec<Ballot>,
pub stats: Stats,
winners_left: usize,
enabled: Vec<bool>,
quota: f64,
@ -22,13 +23,14 @@ impl Counter {
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());
}
for row in csv {
ballots.push(Ballot::parse(row?, &header)?);
ballots.push(Ballot::parse(row?, &header, &mut stats)?);
}
let enabled = vec![true; header.candidates.len()];
@ -38,6 +40,7 @@ impl Counter {
Ok(Counter {
header,
ballots,
stats,
winners_left,
enabled,
quota,

View File

@ -7,6 +7,7 @@ pub mod candidate;
pub mod ballot;
pub mod header;
pub mod party;
pub mod stats;
mod counter;
fn main() {
@ -27,7 +28,11 @@ fn main() {
eprintln!("Parties: {}", header.parties.len());
eprintln!("Candidates: {}", header.candidates.len());
eprintln!("Vacancies: {}", winner_count);
eprintln!("Ballots: {}", counter.ballots.len());
eprintln!("Ballots:");
eprintln!(" Total: {}", counter.stats.total);
eprintln!(" Above: {}, {}", counter.stats.party, Percent(counter.stats.party.into(), counter.stats.total.into()));
eprintln!(" Below: {}, {}", counter.stats.candidate, Percent(counter.stats.candidate.into(), counter.stats.total.into()));
eprintln!(" Both: {}, {}", counter.stats.both, Percent(counter.stats.both.into(), counter.stats.total.into()));
eprintln!("Quota: {}, {}", counter.get_quota(), Percent(counter.get_quota(), total));
eprintln!();

22
src/stats.rs Normal file
View File

@ -0,0 +1,22 @@
use std::fmt::Debug;
#[derive(Debug)]
pub struct Stats {
pub total: u32,
pub candidate: u32,
pub party: u32,
pub both: u32,
}
impl Stats {
pub fn new() -> Self {
Self {
total: 0,
party: 0,
candidate: 0,
both: 0,
}
}
}

View File

@ -1,4 +1,4 @@
use std::fmt::Display;
use std::fmt::{Debug, Display};
pub struct Percent(pub f64, pub f64);
@ -9,6 +9,11 @@ impl Display for Percent {
write!(f, "{:.3}%", self.0 / self.1 * 100.0)
}
}
impl Debug for Percent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Percent({}, {:.3}%)", self.0, self.0 / self.1 * 100.0)
}
}
impl Display for PercentDiff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let v = self.0 / self.1 * 100.0;
@ -18,4 +23,9 @@ impl Display for PercentDiff {
}, v.abs())
}
}
impl Debug for PercentDiff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Percent({}, {:.3}%)", self.0, self.0 / self.1 * 100.0)
}
}