now produces identical results
This commit is contained in:
parent
e615e286dd
commit
57fa414862
|
|
@ -17,6 +17,7 @@ pub struct Ballot {
|
||||||
batch_id: usize,
|
batch_id: usize,
|
||||||
paper_id: usize,
|
paper_id: usize,
|
||||||
votes: Vec<usize>,
|
votes: Vec<usize>,
|
||||||
|
weight: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ballot {
|
impl Ballot {
|
||||||
|
|
@ -61,12 +62,21 @@ impl Ballot {
|
||||||
collection_point_id: collection_point_id.parse()?,
|
collection_point_id: collection_point_id.parse()?,
|
||||||
batch_id: batch_id.parse()?,
|
batch_id: batch_id.parse()?,
|
||||||
paper_id: paper_id.parse()?,
|
paper_id: paper_id.parse()?,
|
||||||
|
weight: 1.0,
|
||||||
votes,
|
votes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn get_primary_index(&self, enabled: &[bool]) -> Option<usize> {
|
pub fn get_weight(&self) -> f64 {
|
||||||
|
self.weight
|
||||||
|
}
|
||||||
|
pub fn apply_weight(&mut self, weight: f64) {
|
||||||
|
self.weight *= weight;
|
||||||
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,102 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
use crate::{ballot::Ballot, header::Header};
|
use crate::{ballot::Ballot, header::Header, score_item::{ScoreItem, ScoreItems}};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
Lose(ScoreItem),
|
||||||
|
Win(Vec<ScoreItem>),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Counter {
|
pub struct Counter {
|
||||||
pub header: Rc<Header>,
|
pub header: Rc<Header>,
|
||||||
pub ballots: Vec<Ballot>,
|
pub ballots: Vec<Ballot>,
|
||||||
|
winners_left: usize,
|
||||||
|
enabled: Vec<bool>,
|
||||||
|
quota: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Counter {
|
impl Counter {
|
||||||
pub fn new<T: std::io::BufRead>(mut csv: quick_csv::Csv<T>) -> 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>> {
|
||||||
let header = Header::parse(csv.next().ok_or("csv header missing")??)?;
|
let header = Header::parse(csv.next().ok_or("csv header missing")??, winners)?;
|
||||||
let mut ballots = Vec::new();
|
let mut ballots = Vec::new();
|
||||||
|
|
||||||
|
if winners > header.candidates.len() {
|
||||||
|
return Err("winners can't be smaller than the candidates list".into());
|
||||||
|
}
|
||||||
|
|
||||||
for row in csv {
|
for row in csv {
|
||||||
ballots.push(Ballot::parse(row?, header.clone())?);
|
ballots.push(Ballot::parse(row?, header.clone())?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let enabled = vec![true; header.candidates.len()];
|
||||||
|
let quota = (ballots.len() as f64) / (header.winners as f64 + 1.0) + 1.0;
|
||||||
|
let winners_left = header.winners;
|
||||||
|
|
||||||
Ok(Counter {
|
Ok(Counter {
|
||||||
header,
|
header,
|
||||||
ballots,
|
ballots,
|
||||||
|
winners_left,
|
||||||
|
enabled,
|
||||||
|
quota,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
fn count_primaries(&self) -> ScoreItems {
|
||||||
pub fn count_primaries(&self, enabled: &[bool]) -> Vec<f64> {
|
let mut scores = vec![0.0; self.enabled.len()];
|
||||||
assert!(enabled.len() == self.header.candidates.len());
|
|
||||||
|
|
||||||
let mut scores = vec![0.0; enabled.len()];
|
|
||||||
|
|
||||||
for ballot in self.ballots.iter() {
|
for ballot in self.ballots.iter() {
|
||||||
if let Some(index) = ballot.get_primary_index(enabled) {
|
if let Some(index) = ballot.get_primary_index(|id| self.enabled[id]) {
|
||||||
scores[index] += 1.0;
|
scores[index] += ballot.get_weight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScoreItems::new(scores, &self.enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scores
|
impl Iterator for Counter {
|
||||||
}
|
type Item = Event;
|
||||||
|
|
||||||
pub fn count_all_with_decaying_weights(&self, enabled: &[bool]) -> Vec<f64> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
assert!(enabled.len() == self.header.candidates.len());
|
let mut winners = Vec::<ScoreItem>::with_capacity(self.enabled.len());
|
||||||
|
let primaries = self.count_primaries().collect_vec();
|
||||||
|
let mut lowest = *primaries.first()?;
|
||||||
|
|
||||||
let mut scores = vec![0.0; enabled.len()];
|
for &score in primaries.iter() {
|
||||||
|
lowest = lowest.min(score);
|
||||||
|
|
||||||
for ballot in self.ballots.iter() {
|
if score.value >= self.quota {
|
||||||
ballot.count_all_with_decaying_weights(&mut scores, enabled);
|
winners.push(score);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
scores
|
|
||||||
|
if primaries.len() == 1 && self.winners_left == 1 {
|
||||||
|
self.enabled[lowest.index] = false;
|
||||||
|
return Some(Event::Win(vec![lowest]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if winners.len() == 0 {
|
||||||
|
self.enabled[lowest.index] = false;
|
||||||
|
return Some(Event::Lose(lowest));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Event::Win(winners))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,15 @@ use crate::{candidate::{Candidate, CandidateName}, party::Party};
|
||||||
pub struct Header {
|
pub struct Header {
|
||||||
pub parties: Vec<Party>,
|
pub parties: Vec<Party>,
|
||||||
pub candidates: Vec<Candidate>,
|
pub candidates: Vec<Candidate>,
|
||||||
|
pub winners: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Header {
|
impl Header {
|
||||||
pub fn parse(row: quick_csv::Row) -> Result<Rc<Header>, Box<dyn std::error::Error>> {
|
pub fn parse(row: quick_csv::Row, winners: usize) -> Result<Rc<Header>, Box<dyn std::error::Error>> {
|
||||||
let mut header = Header {
|
let mut header = Header {
|
||||||
parties: Vec::new(),
|
parties: Vec::new(),
|
||||||
candidates: Vec::new(),
|
candidates: Vec::new(),
|
||||||
|
winners,
|
||||||
};
|
};
|
||||||
let mut parties_lookup = HashMap::<&str, usize>::new();
|
let mut parties_lookup = HashMap::<&str, usize>::new();
|
||||||
|
|
||||||
|
|
|
||||||
92
src/main.rs
92
src/main.rs
|
|
@ -1,13 +1,13 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
use counter::Counter;
|
use counter::Counter;
|
||||||
use itertools::Itertools;
|
use util::Percent;
|
||||||
use util::{percent::PercentDiff, CsvWriter, Percent};
|
|
||||||
|
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod candidate;
|
pub mod candidate;
|
||||||
pub mod ballot;
|
pub mod ballot;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
pub mod party;
|
pub mod party;
|
||||||
|
pub mod score_item;
|
||||||
mod counter;
|
mod counter;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -20,91 +20,37 @@ 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 = quick_csv::Csv::from_file(csv_path).unwrap().flexible(true);
|
||||||
let counter = Counter::new(csv).unwrap();
|
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();
|
let header = counter.header.clone();
|
||||||
|
|
||||||
eprintln!("Header: {:#?}", header);
|
eprintln!("Header: {:#?}", header);
|
||||||
eprintln!("Parties: {}", header.parties.len());
|
eprintln!("Parties: {}", header.parties.len());
|
||||||
eprintln!("Candidates: {}", header.candidates.len());
|
eprintln!("Candidates: {}", header.candidates.len());
|
||||||
|
|
||||||
let total = counter.ballots.len() as u32;
|
|
||||||
let mut running = vec![true; header.candidates.len()];
|
|
||||||
let mut csv_out = CsvWriter::new(std::io::stdout().lock());
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut row = csv_out.row();
|
|
||||||
row.skip(4).unwrap();
|
|
||||||
for name in header.iter_candidate_name() {
|
|
||||||
row.write(name).unwrap();
|
|
||||||
row.skip(1).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
eprintln!("Running election rounds:");
|
eprintln!("Running election rounds:");
|
||||||
|
|
||||||
let mut scores_last: Option<Vec<f64>> = None;
|
for (index, event) in (1..).zip(counter) {
|
||||||
let mut id_worst_last: Option<usize> = None;
|
match event {
|
||||||
|
counter::Event::Win(mut v) => {
|
||||||
for index in 0..(running.len() - winner_count + 1) {
|
eprintln!(" {index}: Win:");
|
||||||
let scores = counter.count_all_with_decaying_weights(&running);
|
v.sort_by(|a,b| b.cmp(a));
|
||||||
let scores_ordered = scores.iter().copied().enumerate().filter(|&(id,_)| running[id]).sorted_by(|a, b| f64::total_cmp(&b.1, &a.1)).collect_vec();
|
for winner in v.iter() {
|
||||||
|
eprintln!(" - {}, {}, {}", header.get_candidate_name(winner.index), winner.value, Percent(winner.value, total));
|
||||||
let (id_worst, score_worst) = match scores_ordered.last() {
|
|
||||||
Some(&v) => v,
|
|
||||||
None => break,
|
|
||||||
};
|
|
||||||
|
|
||||||
csv_out.row();
|
|
||||||
let mut row = csv_out.row();
|
|
||||||
|
|
||||||
if let Some(scores_last) = scores_last {
|
|
||||||
let id_worst_last = id_worst_last.unwrap();
|
|
||||||
let score_worst_last = scores_last[id_worst_last];
|
|
||||||
|
|
||||||
row.skip(1).unwrap();
|
|
||||||
row.write(header.get_candidate_name(id_worst_last)).unwrap();
|
|
||||||
row.write(score_worst_last).unwrap();
|
|
||||||
row.write(Percent(score_worst_last, total.into())).unwrap();
|
|
||||||
|
|
||||||
for ((&score, &score_last), &running) in scores.iter().zip(scores_last.iter()).zip(running.iter()) {
|
|
||||||
if running {
|
|
||||||
let score_diff = score - score_last;
|
|
||||||
row.write(score_diff).unwrap();
|
|
||||||
row.write(PercentDiff(score_diff, score_worst_last)).unwrap();
|
|
||||||
} else {
|
|
||||||
row.skip(2).unwrap();
|
|
||||||
}
|
}
|
||||||
|
winners.extend_from_slice(&v);
|
||||||
}
|
}
|
||||||
drop(row);
|
counter::Event::Lose(v) => {
|
||||||
row = csv_out.row();
|
eprintln!(" {index}: Lose: {}. {}, {}", header.get_candidate_name(v.index), v.value, Percent(v.value, total));
|
||||||
}
|
}
|
||||||
|
|
||||||
row.write(index + 1).unwrap();
|
|
||||||
row.skip(3).unwrap();
|
|
||||||
|
|
||||||
for (&score, &running) in scores.iter().zip(running.iter()) {
|
|
||||||
if running {
|
|
||||||
row.write(score).unwrap();
|
|
||||||
row.write(Percent(score, total.into())).unwrap();
|
|
||||||
} else {
|
|
||||||
row.skip(2).unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
running[id_worst] = false;
|
eprintln!();
|
||||||
scores_last = Some(scores);
|
eprintln!("Final winners:");
|
||||||
id_worst_last = Some(id_worst);
|
for (index, winner) in (1..).zip(winners.iter()) {
|
||||||
|
eprintln!(" {index}: {}", header.get_candidate_name(winner.index));
|
||||||
let mut total_votes = 0.0;
|
|
||||||
|
|
||||||
eprintln!("Round {}:", index+1);
|
|
||||||
for (place, &(id, score)) in scores_ordered.iter().take(winner_count).enumerate() {
|
|
||||||
eprintln!(" {}. {}: {score}, {}", place + 1, header.get_candidate_name(id), Percent(score, total.into()));
|
|
||||||
total_votes += score;
|
|
||||||
}
|
|
||||||
if scores_ordered.len() > winner_count {
|
|
||||||
eprintln!(" {}. {}: {score_worst}, {}", scores_ordered.len(), header.get_candidate_name(id_worst), Percent(score_worst, total.into()));
|
|
||||||
}
|
|
||||||
eprintln!(" Total: {total_votes}, {}", Percent(total_votes.into(), total.into()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
|
||||||
|
#[derive(Clone,Copy,Debug)]
|
||||||
|
pub struct ScoreItem {
|
||||||
|
pub index: usize,
|
||||||
|
pub value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScoreItems<'a> {
|
||||||
|
enabled: &'a [bool],
|
||||||
|
data: Vec<f64>,
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScoreItem {
|
||||||
|
pub fn new(index: usize, value: f64) -> Self {
|
||||||
|
Self { index, value }
|
||||||
|
}
|
||||||
|
pub fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.value.total_cmp(&other.value)
|
||||||
|
}
|
||||||
|
pub fn max(self, other: Self) -> Self {
|
||||||
|
if self.value > other.value {
|
||||||
|
self
|
||||||
|
} else {
|
||||||
|
other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn min(self, other: Self) -> Self {
|
||||||
|
if self.value > other.value {
|
||||||
|
other
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ScoreItems<'a> {
|
||||||
|
pub fn new(data: Vec<f64>, enabled: &'a [bool]) -> Self {
|
||||||
|
Self { data, enabled, index: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for ScoreItems<'a> {
|
||||||
|
type Item = ScoreItem;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
while self.index < self.data.len() {
|
||||||
|
if self.enabled[self.index] {
|
||||||
|
let v = ScoreItem::new(self.index, self.data[self.index]);
|
||||||
|
self.index += 1;
|
||||||
|
return Some(v);
|
||||||
|
}
|
||||||
|
self.index += 1;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let left = self.data.len() - self.index;
|
||||||
|
(0, Some(left))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue