Implement constraints (guard-doom method) for CLI
This commit is contained in:
parent
c563654ace
commit
38eef74e77
|
@ -119,6 +119,7 @@ async function clickCount() {
|
|||
document.getElementById('chkBulkExclusion').checked,
|
||||
document.getElementById('chkDeferSurpluses').checked,
|
||||
document.getElementById('chkMeekImmediateElect').checked,
|
||||
"guard_doom",
|
||||
parseInt(document.getElementById('txtPPDP').value),
|
||||
];
|
||||
|
||||
|
|
|
@ -15,27 +15,155 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::election::{Candidate, CandidateState, CountCard, Election};
|
||||
use crate::numbers::Number;
|
||||
|
||||
use ndarray::{Array, Dimension, IxDyn};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::ops;
|
||||
|
||||
/// Constraints for an [crate::election::Election]
|
||||
#[derive(Debug)]
|
||||
enum ConstraintError {
|
||||
pub struct Constraints(pub Vec<Constraint>);
|
||||
|
||||
impl Constraints {
|
||||
/// Parse the given CON file and return a [Constraints]
|
||||
pub fn from_con<I: Iterator<Item=String>>(lines: I) -> Self {
|
||||
let mut constraints = Constraints(Vec::new());
|
||||
|
||||
for line in lines {
|
||||
let mut bits = line.split(" ").peekable();
|
||||
|
||||
// Read constraint category
|
||||
let mut constraint_name = String::new();
|
||||
let x = bits.next().expect("Syntax Error");
|
||||
if x.starts_with('"') {
|
||||
if x.ends_with('"') {
|
||||
constraint_name.push_str(&x[1..x.len()-1]);
|
||||
} else {
|
||||
constraint_name.push_str(&x[1..]);
|
||||
while !bits.peek().expect("Syntax Error").ends_with('"') {
|
||||
constraint_name.push_str(" ");
|
||||
constraint_name.push_str(bits.next().unwrap());
|
||||
}
|
||||
let x = bits.next().unwrap();
|
||||
constraint_name.push_str(" ");
|
||||
constraint_name.push_str(&x[..x.len()-1]);
|
||||
}
|
||||
} else {
|
||||
constraint_name.push_str(x);
|
||||
}
|
||||
|
||||
// Read constraint group
|
||||
let mut group_name = String::new();
|
||||
let x = bits.next().expect("Syntax Error");
|
||||
if x.starts_with('"') {
|
||||
if x.ends_with('"') {
|
||||
group_name.push_str(&x[1..x.len()-1]);
|
||||
} else {
|
||||
group_name.push_str(&x[1..]);
|
||||
while !bits.peek().expect("Syntax Error").ends_with('"') {
|
||||
group_name.push_str(" ");
|
||||
group_name.push_str(bits.next().unwrap());
|
||||
}
|
||||
let x = bits.next().unwrap();
|
||||
group_name.push_str(" ");
|
||||
group_name.push_str(&x[..x.len()-1]);
|
||||
}
|
||||
} else {
|
||||
group_name.push_str(x);
|
||||
}
|
||||
|
||||
// Read min, max
|
||||
let min: usize = bits.next().expect("Syntax Error").parse().expect("Syntax Error");
|
||||
let max: usize = bits.next().expect("Syntax Error").parse().expect("Syntax Error");
|
||||
|
||||
// Read candidates
|
||||
let mut candidates: Vec<usize> = Vec::new();
|
||||
for x in bits {
|
||||
candidates.push(x.parse::<usize>().expect("Syntax Error") - 1);
|
||||
}
|
||||
|
||||
// Insert constraint/group
|
||||
let constraint = match constraints.0.iter_mut().filter(|c| c.name == constraint_name).next() {
|
||||
Some(c) => { c }
|
||||
None => {
|
||||
let c = Constraint {
|
||||
name: constraint_name,
|
||||
groups: Vec::new(),
|
||||
};
|
||||
constraints.0.push(c);
|
||||
constraints.0.last_mut().unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
if constraint.groups.iter().any(|g| g.name == group_name) {
|
||||
panic!("Duplicate group \"{}\" in constraint \"{}\"", group_name, constraint.name);
|
||||
}
|
||||
|
||||
constraint.groups.push(ConstrainedGroup {
|
||||
name: group_name,
|
||||
candidates: candidates,
|
||||
min: min,
|
||||
max: max,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Validate constraints
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
/// A single dimension of constraint
|
||||
#[derive(Debug)]
|
||||
pub struct Constraint {
|
||||
/// Name of this constraint
|
||||
pub name: String,
|
||||
/// Groups of candidates within this constraint
|
||||
pub groups: Vec<ConstrainedGroup>,
|
||||
}
|
||||
|
||||
/// A group of candidates, of which a certain minimum and maximum must be elected
|
||||
#[derive(Debug)]
|
||||
pub struct ConstrainedGroup {
|
||||
/// Name of this group
|
||||
pub name: String,
|
||||
/// Indexes of [crate::election::Candidate]s to constrain
|
||||
pub candidates: Vec<usize>,
|
||||
/// Minimum number to elect
|
||||
pub min: usize,
|
||||
/// Maximum number to elect
|
||||
pub max: usize,
|
||||
}
|
||||
|
||||
/// Error reaching a stable state when processing constraints
|
||||
#[derive(Debug)]
|
||||
pub enum ConstraintError {
|
||||
/// No conformant result is possible
|
||||
NoConformantResult,
|
||||
}
|
||||
|
||||
/// Cell in a [ConstraintMatrix]
|
||||
#[derive(Clone)]
|
||||
struct ConstraintMatrixCell {
|
||||
elected: usize,
|
||||
min: usize,
|
||||
max: usize,
|
||||
cands: usize,
|
||||
pub struct ConstraintMatrixCell {
|
||||
/// Number of elected candidates in this cell
|
||||
pub elected: usize,
|
||||
/// Minimum number of candidates which must be elected from this cell for a conformant result
|
||||
pub min: usize,
|
||||
/// Maximum number of candidates which may be elected from this cell for a conformant result
|
||||
pub max: usize,
|
||||
/// Total number of elected or hopeful candidates in this cell
|
||||
pub cands: usize,
|
||||
}
|
||||
|
||||
struct ConstraintMatrix(Array<ConstraintMatrixCell, IxDyn>);
|
||||
/// Hypercube/tensor of [ConstraintMatrixCell]s representing the conformant combinations of elected candidates
|
||||
pub struct ConstraintMatrix(pub Array<ConstraintMatrixCell, IxDyn>);
|
||||
|
||||
impl ConstraintMatrix {
|
||||
/// Return a new [ConstraintMatrix], with the specified number of groups for each constraint dimension
|
||||
pub fn new(constraints: &mut [usize]) -> Self {
|
||||
// Add 1 to dimensions for totals cells
|
||||
for c in constraints.iter_mut() {
|
||||
|
@ -53,9 +181,8 @@ impl ConstraintMatrix {
|
|||
));
|
||||
}
|
||||
|
||||
/// Initialise the matrix once the number of candidates in each innermost cell, and min/max for each constraint group, have been specified
|
||||
pub fn init(&mut self) {
|
||||
let indices: Vec<IxDyn> = ndarray::indices(self.0.shape()).into_iter().collect();
|
||||
|
||||
// Compute candidate totals
|
||||
self.recount_cands();
|
||||
|
||||
|
@ -64,30 +191,68 @@ impl ConstraintMatrix {
|
|||
self.0[&idx].max = self.0[&idx].cands;
|
||||
|
||||
// Initialise max for inner cells (>=2 zeroes)
|
||||
for idx in indices.iter() {
|
||||
for idx in ndarray::indices(self.0.shape()) {
|
||||
if (0..idx.ndim()).fold(0, |acc, d| if idx[d] != 0 { acc + 1 } else { acc }) < 2 {
|
||||
continue;
|
||||
}
|
||||
self.0[idx].max = self.0[idx].cands;
|
||||
self.0[&idx].max = self.0[&idx].cands;
|
||||
}
|
||||
|
||||
// NB: Bounds on min, max, etc. will be further refined in initial step() calls
|
||||
}
|
||||
|
||||
/// Update cands/elected in innermost cells based on the provided [CountState::candidates]
|
||||
pub fn update_from_state<N: Number>(&mut self, election: &Election<N>, candidates: &HashMap<&Candidate, CountCard<N>>) {
|
||||
let constraints = election.constraints.as_ref().unwrap();
|
||||
|
||||
// Reset innermost cells
|
||||
for idx in ndarray::indices(self.0.shape()) {
|
||||
if (0..idx.ndim()).fold(0, |acc, d| if idx[d] == 0 { acc + 1 } else { acc }) != 0 {
|
||||
continue;
|
||||
}
|
||||
self.0[&idx].cands = 0;
|
||||
self.0[&idx].elected = 0;
|
||||
}
|
||||
|
||||
for (i, candidate) in election.candidates.iter().enumerate() {
|
||||
let idx: Vec<usize> = constraints.0.iter().map(|c| {
|
||||
for (j, group) in c.groups.iter().enumerate() {
|
||||
if group.candidates.contains(&i) {
|
||||
return j + 1;
|
||||
}
|
||||
}
|
||||
panic!("Candidate \"{}\" not represented in constraint \"{}\"", candidate.name, c.name);
|
||||
}).collect();
|
||||
let cell = &mut self[&idx[..]];
|
||||
|
||||
let count_card = candidates.get(candidate).unwrap();
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful | CandidateState::Guarded => {
|
||||
cell.cands += 1;
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
cell.cands += 1;
|
||||
cell.elected += 1;
|
||||
}
|
||||
CandidateState::Withdrawn | CandidateState::Doomed | CandidateState::Excluded => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recompute [self::cands] and [self::elected] for totals cells based on the innermost cells
|
||||
pub fn recount_cands(&mut self) {
|
||||
let shape = Vec::from(self.0.shape());
|
||||
let indices: Vec<IxDyn> = ndarray::indices(self.0.shape()).into_iter().collect();
|
||||
|
||||
// Compute cands/elected totals
|
||||
for nzeroes in 1..self.0.ndim()+1 {
|
||||
for idx in indices.iter() {
|
||||
for idx in ndarray::indices(self.0.shape()) {
|
||||
// First compute totals cells with 1 zero, then 2 zeroes, ... then grand total (all zeroes)
|
||||
if (0..idx.ndim()).fold(0, |acc, d| if idx[d] == 0 { acc + 1 } else { acc }) != nzeroes {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.0[idx].cands = 0;
|
||||
self.0[idx].elected = 0;
|
||||
self.0[&idx].cands = 0;
|
||||
self.0[&idx].elected = 0;
|
||||
|
||||
// The axis along which to sum - if multiple, just pick the first, as these should agree
|
||||
let zero_axis = (0..idx.ndim()).filter(|d| idx[*d] == 0).next().unwrap();
|
||||
|
@ -96,19 +261,22 @@ impl ConstraintMatrix {
|
|||
let mut idx2 = idx.clone();
|
||||
for coord in 1..shape[zero_axis] {
|
||||
idx2[zero_axis] = coord;
|
||||
self.0[idx].cands += self.0[&idx2].cands;
|
||||
self.0[idx].elected += self.0[&idx2].elected;
|
||||
self.0[&idx].cands += self.0[&idx2].cands;
|
||||
self.0[&idx].elected += self.0[&idx2].elected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to advance the matrix one step towards a stable state
|
||||
///
|
||||
/// Returns `Ok(true)` if in a stable state, `Ok(false)` if not, and `ConstraintError` on an error.
|
||||
///
|
||||
pub fn step(&mut self) -> Result<bool, ConstraintError> {
|
||||
let shape = Vec::from(self.0.shape());
|
||||
let indices: Vec<IxDyn> = ndarray::indices(self.0.shape()).into_iter().collect();
|
||||
|
||||
for idx in indices.iter() {
|
||||
let cell = &mut self.0[idx];
|
||||
for idx in ndarray::indices(self.0.shape()) {
|
||||
let cell = &mut self.0[&idx];
|
||||
|
||||
// Rule 1: Ensure elected < min < max < cands
|
||||
if cell.min < cell.elected {
|
||||
|
@ -125,7 +293,7 @@ impl ConstraintMatrix {
|
|||
|
||||
let nzeroes = (0..idx.ndim()).fold(0, |acc, d| if idx[d] == 0 { acc + 1 } else { acc });
|
||||
|
||||
// Rule 2/3: Ensure min/max is possible in inner cells
|
||||
// Rule 2/3: Ensure min/max is possible in innermost cells
|
||||
if nzeroes == 0 {
|
||||
for axis in 0..self.0.ndim() {
|
||||
let mut idx2 = idx.clone();
|
||||
|
@ -148,12 +316,12 @@ impl ConstraintMatrix {
|
|||
let this_max = (axis_max as i32) - (other_min as i32);
|
||||
let this_min = (axis_min as i32) - (other_max as i32);
|
||||
|
||||
if this_max < (self.0[idx].max as i32) {
|
||||
self.0[idx].max = this_max as usize;
|
||||
if this_max < (self.0[&idx].max as i32) {
|
||||
self.0[&idx].max = this_max as usize;
|
||||
return Ok(false);
|
||||
}
|
||||
if this_min > (self.0[idx].min as i32) {
|
||||
self.0[idx].min = this_min as usize;
|
||||
if this_min > (self.0[&idx].min as i32) {
|
||||
self.0[&idx].min = this_min as usize;
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
@ -173,12 +341,12 @@ impl ConstraintMatrix {
|
|||
return (acc_max + self.0[&idx2].max, acc_min + self.0[&idx2].min);
|
||||
});
|
||||
|
||||
if axis_max < self.0[idx].max {
|
||||
self.0[idx].max = axis_max;
|
||||
if axis_max < self.0[&idx].max {
|
||||
self.0[&idx].max = axis_max;
|
||||
return Ok(false);
|
||||
}
|
||||
if axis_min > self.0[idx].min {
|
||||
self.0[idx].min = axis_min;
|
||||
if axis_min > self.0[&idx].min {
|
||||
self.0[&idx].min = axis_min;
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
@ -195,7 +363,48 @@ impl fmt::Display for ConstraintMatrix {
|
|||
|
||||
let mut result = String::new();
|
||||
|
||||
// TODO: ≠2 dimensions
|
||||
// TODO: >2 dimensions
|
||||
if shape.len() == 1 {
|
||||
result.push_str("+");
|
||||
for _ in 0..shape[0] {
|
||||
result.push_str("-------------+");
|
||||
}
|
||||
result.push_str("\n");
|
||||
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Elected: {:2}", self[&[x]].elected));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push_str("\n");
|
||||
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Min: {:2}", self[&[x]].min));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push_str("\n");
|
||||
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Max: {:2}", self[&[x]].max));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push_str("\n");
|
||||
|
||||
result.push_str("|");
|
||||
for x in 0..shape[0] {
|
||||
result.push_str(&format!(" Cands: {:2}", self[&[x]].cands));
|
||||
result.push_str(if x == 0 { " ‖" } else { " |" });
|
||||
}
|
||||
result.push_str("\n");
|
||||
|
||||
result.push_str("+");
|
||||
for _ in 0..shape[0] {
|
||||
result.push_str("-------------+");
|
||||
}
|
||||
result.push_str("\n");
|
||||
} else if shape.len() == 2 {
|
||||
for y in 0..shape[1] {
|
||||
result.push_str("+");
|
||||
for _ in 0..shape[0] {
|
||||
|
@ -237,6 +446,9 @@ impl fmt::Display for ConstraintMatrix {
|
|||
result.push_str("-------------+");
|
||||
}
|
||||
result.push_str("\n");
|
||||
} else {
|
||||
todo!();
|
||||
}
|
||||
|
||||
return f.write_str(&result);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::constraints::{Constraints, ConstraintMatrix};
|
||||
use crate::logger::Logger;
|
||||
use crate::numbers::Number;
|
||||
use crate::sharandom::SHARandom;
|
||||
|
@ -33,6 +34,8 @@ pub struct Election<N> {
|
|||
pub withdrawn_candidates: Vec<usize>,
|
||||
/// [Vec] of [Ballot]s cast in the election
|
||||
pub ballots: Vec<Ballot<N>>,
|
||||
/// Constraints on candidates
|
||||
pub constraints: Option<Constraints>,
|
||||
}
|
||||
|
||||
impl<N: Number> Election<N> {
|
||||
|
@ -51,6 +54,7 @@ impl<N: Number> Election<N> {
|
|||
candidates: Vec::with_capacity(num_candidates),
|
||||
withdrawn_candidates: Vec::new(),
|
||||
ballots: Vec::new(),
|
||||
constraints: None,
|
||||
};
|
||||
|
||||
// Read ballots
|
||||
|
@ -169,6 +173,9 @@ pub struct CountState<'a, N: Number> {
|
|||
/// Number of candidates who have been declared excluded
|
||||
pub num_excluded: usize,
|
||||
|
||||
/// [ConstraintMatrix] for constrained elections
|
||||
pub constraint_matrix: Option<ConstraintMatrix>,
|
||||
|
||||
/// The type of stage being counted
|
||||
///
|
||||
/// For example, "Surplus of", "Exclusion of"
|
||||
|
@ -195,6 +202,7 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||
vote_required_election: None,
|
||||
num_elected: 0,
|
||||
num_excluded: 0,
|
||||
constraint_matrix: None,
|
||||
kind: None,
|
||||
title: String::new(),
|
||||
logger: Logger { entries: Vec::new() },
|
||||
|
@ -208,6 +216,29 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||
state.candidates.get_mut(&election.candidates[*withdrawn_idx]).unwrap().state = CandidateState::Withdrawn;
|
||||
}
|
||||
|
||||
if let Some(constraints) = &election.constraints {
|
||||
let mut num_groups: Vec<usize> = constraints.0.iter().map(|c| c.groups.len()).collect();
|
||||
let mut cm = ConstraintMatrix::new(&mut num_groups[..]);
|
||||
|
||||
// Init constraint matrix total cells min/max
|
||||
for (i, constraint) in constraints.0.iter().enumerate() {
|
||||
for (j, group) in constraint.groups.iter().enumerate() {
|
||||
let mut idx = vec![0; constraints.0.len()];
|
||||
idx[i] = j + 1;
|
||||
let mut cell = &mut cm[&idx];
|
||||
cell.min = group.min;
|
||||
cell.max = group.max;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in grand total, etc.
|
||||
cm.update_from_state(&state.election, &state.candidates);
|
||||
cm.init();
|
||||
//println!("{}", cm);
|
||||
|
||||
state.constraint_matrix = Some(cm);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
|
|
62
src/main.rs
62
src/main.rs
|
@ -15,9 +15,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use opentally::stv;
|
||||
use opentally::constraints::Constraints;
|
||||
use opentally::election::{Candidate, CandidateState, CountCard, CountState, CountStateOrRef, Election, StageResult};
|
||||
use opentally::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
|
||||
use opentally::stv;
|
||||
|
||||
use clap::{AppSettings, Clap};
|
||||
|
||||
|
@ -156,6 +157,17 @@ struct STV {
|
|||
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
|
||||
meek_immediate_elect: bool,
|
||||
|
||||
// -----------------
|
||||
// -- Constraints --
|
||||
|
||||
/// Path to a CON file specifying constraints
|
||||
#[clap(help_heading=Some("CONSTRAINTS"), long)]
|
||||
constraints: Option<String>,
|
||||
|
||||
/// Mode of handling constraints
|
||||
#[clap(help_heading=Some("CONSTRAINTS"), long, possible_values=&["guard_doom"], default_value="guard_doom")]
|
||||
constraint_mode: String,
|
||||
|
||||
// ----------------------
|
||||
// -- Display settings --
|
||||
|
||||
|
@ -183,23 +195,37 @@ fn main() {
|
|||
|
||||
// Create and count election according to --numbers
|
||||
if cmd_opts.numbers == "rational" {
|
||||
let election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
let mut election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||
|
||||
// Must specify ::<N> here and in a few other places because ndarray causes E0275 otherwise
|
||||
count_election::<Rational>(election, cmd_opts);
|
||||
} else if cmd_opts.numbers == "float64" {
|
||||
let election: Election<NativeFloat64> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
let mut election: Election<NativeFloat64> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||
count_election::<NativeFloat64>(election, cmd_opts);
|
||||
} else if cmd_opts.numbers == "fixed" {
|
||||
Fixed::set_dps(cmd_opts.decimals);
|
||||
let election: Election<Fixed> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
let mut election: Election<Fixed> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||
count_election::<Fixed>(election, cmd_opts);
|
||||
} else if cmd_opts.numbers == "gfixed" {
|
||||
GuardedFixed::set_dps(cmd_opts.decimals);
|
||||
let election: Election<GuardedFixed> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
|
||||
let mut election: Election<GuardedFixed> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
|
||||
maybe_load_constraints(&mut election, &cmd_opts.constraints);
|
||||
count_election::<GuardedFixed>(election, cmd_opts);
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_load_constraints<N: Number>(election: &mut Election<N>, constraints: &Option<String>) {
|
||||
if let Some(c) = constraints {
|
||||
let file = File::open(c).expect("IO Error");
|
||||
let lines = io::BufReader::new(file).lines();
|
||||
election.constraints = Some(Constraints::from_con(lines.map(|r| r.expect("IO Error").to_string()).into_iter()));
|
||||
}
|
||||
}
|
||||
|
||||
fn count_election<N: Number>(mut election: Election<N>, cmd_opts: STV)
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
|
@ -230,6 +256,7 @@ where
|
|||
cmd_opts.bulk_exclude,
|
||||
cmd_opts.defer_surpluses,
|
||||
cmd_opts.meek_immediate_elect,
|
||||
&cmd_opts.constraint_mode,
|
||||
cmd_opts.pp_decimals,
|
||||
);
|
||||
|
||||
|
@ -290,23 +317,34 @@ where
|
|||
|
||||
fn print_candidates<'a, N: 'a + Number, I: Iterator<Item=(&'a Candidate, &'a CountCard<'a, N>)>>(candidates: I, cmd_opts: &STV) {
|
||||
for (candidate, count_card) in candidates {
|
||||
if count_card.state == CandidateState::Elected {
|
||||
match count_card.state {
|
||||
CandidateState::Hopeful => {
|
||||
println!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
CandidateState::Guarded => {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - Guarded", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
CandidateState::Elected => {
|
||||
if let Some(kv) = &count_card.keep_value {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {} (kv = {:.dps2$})", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, kv, dps=cmd_opts.pp_decimals, dps2=max(cmd_opts.pp_decimals, 2));
|
||||
} else {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
} else if count_card.state == CandidateState::Excluded {
|
||||
}
|
||||
CandidateState::Doomed => {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - Doomed", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
CandidateState::Withdrawn => {
|
||||
if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - Withdrawn", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
}
|
||||
CandidateState::Excluded => {
|
||||
// If --hide-excluded, hide unless nonzero votes or nonzero transfers
|
||||
if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - Excluded {}", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
} else if count_card.state == CandidateState::Withdrawn {
|
||||
if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() {
|
||||
println!("- {}: {:.dps$} ({:.dps$}) - Withdrawn", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
} else {
|
||||
println!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=cmd_opts.pp_decimals);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -360,6 +360,8 @@ where
|
|||
count_card.state = CandidateState::Excluded;
|
||||
state.num_excluded += 1;
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
|
||||
super::update_constraints(state, opts);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -518,6 +520,8 @@ where
|
|||
state.num_excluded += 1;
|
||||
count_card.order_elected = -(order_excluded as isize);
|
||||
}
|
||||
|
||||
super::update_constraints(state, opts);
|
||||
}
|
||||
|
||||
// Reset count
|
||||
|
|
210
src/stv/mod.rs
210
src/stv/mod.rs
|
@ -27,11 +27,12 @@ pub mod meek;
|
|||
pub mod wasm;
|
||||
|
||||
use crate::numbers::Number;
|
||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Vote};
|
||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, Vote};
|
||||
use crate::sharandom::SHARandom;
|
||||
use crate::ties::TieStrategy;
|
||||
|
||||
use itertools::Itertools;
|
||||
use ndarray::Dimension;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
@ -79,6 +80,8 @@ pub struct STVOptions {
|
|||
pub defer_surpluses: bool,
|
||||
/// (Meek STV) Immediately elect candidates even if keep values have not converged
|
||||
pub meek_immediate_elect: bool,
|
||||
/// Mode of handling constraints
|
||||
pub constraint_mode: ConstraintMode,
|
||||
/// Print votes to specified decimal places in results report
|
||||
pub pp_decimals: usize,
|
||||
}
|
||||
|
@ -107,6 +110,7 @@ impl STVOptions {
|
|||
bulk_exclude: bool,
|
||||
defer_surpluses: bool,
|
||||
meek_immediate_elect: bool,
|
||||
constraint_mode: &str,
|
||||
pp_decimals: usize,
|
||||
) -> Self {
|
||||
return STVOptions {
|
||||
|
@ -171,6 +175,11 @@ impl STVOptions {
|
|||
bulk_exclude,
|
||||
defer_surpluses,
|
||||
meek_immediate_elect,
|
||||
constraint_mode: match constraint_mode {
|
||||
"guard_doom" => ConstraintMode::GuardDoom,
|
||||
"rollback" => ConstraintMode::Rollback,
|
||||
_ => panic!("Invalid --constraint-mode"),
|
||||
},
|
||||
pp_decimals,
|
||||
};
|
||||
}
|
||||
|
@ -382,6 +391,14 @@ impl ExclusionMethod {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enum of options for [STVOptions::constraint_mode]
|
||||
pub enum ConstraintMode {
|
||||
/// Guard or doom candidates as soon as required to secure a conformant result
|
||||
GuardDoom,
|
||||
/// TODO: NYI
|
||||
Rollback,
|
||||
}
|
||||
|
||||
/// An error during the STV count
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
|
@ -442,6 +459,14 @@ where
|
|||
return Ok(false);
|
||||
}
|
||||
|
||||
// Exclude doomed candidates
|
||||
if exclude_doomed(&mut state, &opts)? {
|
||||
calculate_quota(&mut state, opts);
|
||||
elect_meeting_quota(&mut state, opts);
|
||||
update_tiebreaks(&mut state, opts);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Distribute surpluses
|
||||
if distribute_surpluses(&mut state, &opts)? {
|
||||
calculate_quota(&mut state, opts);
|
||||
|
@ -681,18 +706,24 @@ fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOption
|
|||
|
||||
/// Declare elected all candidates meeting the quota
|
||||
fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
|
||||
let vote_req = state.vote_required_election.as_ref().unwrap(); // Have to do this or else the borrow checker gets confused
|
||||
let vote_req = state.vote_required_election.as_ref().unwrap().clone(); // Have to do this or else the borrow checker gets confused
|
||||
|
||||
let mut cands_meeting_quota: Vec<&Candidate> = state.election.candidates.iter()
|
||||
.filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::Hopeful && meets_quota(vote_req, cc, opts) })
|
||||
.filter(|c| {
|
||||
let cc = state.candidates.get(c).unwrap();
|
||||
return (cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded) && meets_quota(&vote_req, cc, opts);
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !cands_meeting_quota.is_empty() {
|
||||
// Sort by votes
|
||||
cands_meeting_quota.sort_unstable_by(|a, b| state.candidates.get(a).unwrap().votes.cmp(&state.candidates.get(b).unwrap().votes));
|
||||
|
||||
let elected = !cands_meeting_quota.is_empty();
|
||||
|
||||
while !cands_meeting_quota.is_empty() {
|
||||
// Declare elected in descending order of votes
|
||||
for candidate in cands_meeting_quota.into_iter().rev() {
|
||||
let candidate = cands_meeting_quota.pop().unwrap();
|
||||
|
||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||
count_card.state = CandidateState::Elected;
|
||||
state.num_elected += 1;
|
||||
|
@ -703,21 +734,125 @@ fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
|
|||
vec![&candidate.name]
|
||||
);
|
||||
|
||||
if update_constraints(state, opts) {
|
||||
// Recheck as some candidates may have been doomed
|
||||
cands_meeting_quota = state.election.candidates.iter()
|
||||
.filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::Hopeful && meets_quota(&vote_req, cc, opts) })
|
||||
.collect();
|
||||
cands_meeting_quota.sort_unstable_by(|a, b| state.candidates.get(a).unwrap().votes.cmp(&state.candidates.get(b).unwrap().votes));
|
||||
}
|
||||
|
||||
if opts.quota_mode == QuotaMode::ERS97 {
|
||||
// Vote required for election may have changed
|
||||
calculate_quota(state, opts);
|
||||
}
|
||||
}
|
||||
|
||||
if opts.quota_mode == QuotaMode::ERS97 {
|
||||
// Repeat in case vote required for election has changed
|
||||
//calculate_quota(state, opts);
|
||||
elect_meeting_quota(state, opts);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return elected;
|
||||
}
|
||||
|
||||
fn candidates_in_constraint_cell<'a, N: Number>(election: &'a Election<N>, candidates: &HashMap<&Candidate, CountCard<N>>, idx: &[usize]) -> Vec<&'a Candidate> {
|
||||
let mut result: Vec<&Candidate> = Vec::new();
|
||||
for (i, candidate) in election.candidates.iter().enumerate() {
|
||||
let cc = candidates.get(candidate).unwrap();
|
||||
if cc.state != CandidateState::Hopeful {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Is this candidate within this constraint cell?
|
||||
let mut matches = true;
|
||||
for (coord, constraint) in idx.iter().zip(election.constraints.as_ref().unwrap().0.iter()) {
|
||||
let group = &constraint.groups[coord - 1]; // The group referred to by this constraint cell
|
||||
if !group.candidates.contains(&i) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if matches {
|
||||
result.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn update_constraints<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool {
|
||||
if state.constraint_matrix.is_none() {
|
||||
return false;
|
||||
}
|
||||
let cm = state.constraint_matrix.as_mut().unwrap();
|
||||
|
||||
// Update cands/elected
|
||||
cm.update_from_state(&state.election, &state.candidates);
|
||||
cm.recount_cands();
|
||||
|
||||
// Iterate for stable state
|
||||
//println!("{}", cm);
|
||||
while !cm.step().expect("No conformant result") {
|
||||
//println!("{}", cm);
|
||||
}
|
||||
//println!("{}", cm);
|
||||
|
||||
// TODO: Refactor and move this to constraints module?
|
||||
match opts.constraint_mode {
|
||||
ConstraintMode::GuardDoom => {
|
||||
// Check for guarded or doomed candidates
|
||||
let mut guarded_or_doomed = false;
|
||||
|
||||
for idx in ndarray::indices(cm.0.shape()) {
|
||||
if (0..idx.ndim()).fold(0, |acc, d| if idx[d] == 0 { acc + 1 } else { acc }) != 0 {
|
||||
continue;
|
||||
}
|
||||
let cell = &cm.0[&idx];
|
||||
|
||||
if cell.elected == cell.max {
|
||||
// Doom remaining candidates in this cell
|
||||
let doomed = candidates_in_constraint_cell(state.election, &state.candidates, idx.slice());
|
||||
if !doomed.is_empty() {
|
||||
for candidate in doomed.iter() {
|
||||
state.candidates.get_mut(candidate).unwrap().state = CandidateState::Doomed;
|
||||
}
|
||||
|
||||
state.logger.log_smart(
|
||||
"{} must be doomed to comply with constraints.",
|
||||
"{} must be doomed to comply with constraints.",
|
||||
doomed.iter().map(|c| c.name.as_str()).collect()
|
||||
);
|
||||
|
||||
guarded_or_doomed = true;
|
||||
}
|
||||
}
|
||||
if cell.cands == cell.min {
|
||||
// Guard remaining candidates in this cell
|
||||
let guarded = candidates_in_constraint_cell(state.election, &state.candidates, idx.slice());
|
||||
if !guarded.is_empty() {
|
||||
for candidate in guarded.iter() {
|
||||
state.candidates.get_mut(candidate).unwrap().state = CandidateState::Guarded;
|
||||
}
|
||||
|
||||
state.logger.log_smart(
|
||||
"{} must be guarded to comply with constraints.",
|
||||
"{} must be guarded to comply with constraints.",
|
||||
guarded.iter().map(|c| c.name.as_str()).collect()
|
||||
);
|
||||
|
||||
guarded_or_doomed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return guarded_or_doomed;
|
||||
}
|
||||
_ => { todo!() }
|
||||
}
|
||||
|
||||
//return false;
|
||||
}
|
||||
|
||||
/// Determine whether the transfer of all surpluses can be deferred
|
||||
|
@ -777,7 +912,10 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result
|
|||
|
||||
// Bulk elect all remaining candidates
|
||||
let mut hopefuls: Vec<&Candidate> = state.election.candidates.iter()
|
||||
.filter(|c| state.candidates.get(c).unwrap().state == CandidateState::Hopeful)
|
||||
.filter(|c| {
|
||||
let cc = state.candidates.get(c).unwrap();
|
||||
return cc.state == CandidateState::Hopeful || cc.state == CandidateState::Guarded;
|
||||
})
|
||||
.collect();
|
||||
|
||||
while !hopefuls.is_empty() {
|
||||
|
@ -810,6 +948,8 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result
|
|||
);
|
||||
|
||||
hopefuls.remove(hopefuls.iter().position(|c| *c == candidate).unwrap());
|
||||
|
||||
update_constraints(state, opts);
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
|
@ -817,6 +957,53 @@ fn bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result
|
|||
return Ok(false);
|
||||
}
|
||||
|
||||
fn exclude_doomed<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||
where
|
||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||
{
|
||||
let mut doomed: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
|
||||
.map(|c| (c, state.candidates.get(c).unwrap()))
|
||||
.filter(|(_, cc)| cc.state == CandidateState::Doomed)
|
||||
.collect();
|
||||
|
||||
if !doomed.is_empty() {
|
||||
let excluded_candidates;
|
||||
|
||||
if opts.bulk_exclude {
|
||||
excluded_candidates = doomed.into_iter().map(|(c, _)| c).collect();
|
||||
} else {
|
||||
// Exclude only the lowest-ranked doomed candidate
|
||||
// Sort by votes
|
||||
doomed.sort_by(|a, b| a.1.votes.cmp(&b.1.votes));
|
||||
|
||||
// Handle ties
|
||||
if doomed.len() > 1 && doomed[0].1.votes == doomed[1].1.votes {
|
||||
let min_votes = &doomed[0].1.votes;
|
||||
let doomed = doomed.into_iter().filter_map(|(c, cc)| if &cc.votes == min_votes { Some(c) } else { None }).collect();
|
||||
excluded_candidates = vec![choose_lowest(state, opts, doomed)?];
|
||||
} else {
|
||||
excluded_candidates = vec![&doomed[0].0];
|
||||
}
|
||||
}
|
||||
|
||||
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect();
|
||||
state.kind = Some("Exclusion of");
|
||||
state.title = names.join(", ");
|
||||
state.logger.log_smart(
|
||||
"Doomed candidate, {}, is excluded.",
|
||||
"Doomed candidates, {}, are excluded.",
|
||||
names
|
||||
);
|
||||
|
||||
exclude_candidates(state, opts, excluded_candidates);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
/// Determine which continuing candidates could be excluded in a bulk exclusion
|
||||
///
|
||||
/// The value of [STVOptions::bulk_exclude] is not taken into account and must be handled by the caller
|
||||
|
@ -893,8 +1080,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let mut names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect();
|
||||
names.sort();
|
||||
let names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect();
|
||||
state.kind = Some("Exclusion of");
|
||||
state.title = names.join(", ");
|
||||
state.logger.log_smart(
|
||||
|
|
|
@ -188,7 +188,7 @@ impl STVOptions {
|
|||
round_votes: Option<usize>,
|
||||
round_quota: Option<usize>,
|
||||
sum_surplus_transfers: &str,
|
||||
meek_surplus_limit: &str,
|
||||
meek_surplus_tolerance: &str,
|
||||
normalise_ballots: bool,
|
||||
quota: &str,
|
||||
quota_criterion: &str,
|
||||
|
@ -204,6 +204,7 @@ impl STVOptions {
|
|||
bulk_exclude: bool,
|
||||
defer_surpluses: bool,
|
||||
meek_immediate_elect: bool,
|
||||
constraint_mode: &str,
|
||||
pp_decimals: usize,
|
||||
) -> Self {
|
||||
Self(stv::STVOptions::new(
|
||||
|
@ -212,7 +213,7 @@ impl STVOptions {
|
|||
round_votes,
|
||||
round_quota,
|
||||
sum_surplus_transfers,
|
||||
meek_surplus_limit,
|
||||
meek_surplus_tolerance,
|
||||
normalise_ballots,
|
||||
quota,
|
||||
quota_criterion,
|
||||
|
@ -228,6 +229,7 @@ impl STVOptions {
|
|||
bulk_exclude,
|
||||
defer_surpluses,
|
||||
meek_immediate_elect,
|
||||
constraint_mode,
|
||||
pp_decimals,
|
||||
))
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ fn aec_tas19_rational() {
|
|||
bulk_exclude: true,
|
||||
defer_surpluses: false,
|
||||
meek_immediate_elect: false,
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
utils::validate_election::<Rational>(stages, records, election, stv_opts, None, &["exhausted", "lbf"]);
|
||||
|
|
|
@ -43,6 +43,7 @@ fn csm15_float64() {
|
|||
bulk_exclude: true,
|
||||
defer_surpluses: false,
|
||||
meek_immediate_elect: false,
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
utils::read_validate_election::<NativeFloat64>("tests/data/CSM15.csv", "tests/data/CSM15.blt", stv_opts, Some(6), &["quota"]);
|
||||
|
|
|
@ -43,6 +43,7 @@ fn ers97_rational() {
|
|||
bulk_exclude: true,
|
||||
defer_surpluses: true,
|
||||
meek_immediate_elect: false,
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
utils::read_validate_election::<Rational>("tests/data/ers97.csv", "tests/data/ers97.blt", stv_opts, None, &["nt", "vre"]);
|
||||
|
|
|
@ -48,6 +48,7 @@ fn meek87_ers97_float64() {
|
|||
bulk_exclude: false,
|
||||
defer_surpluses: false,
|
||||
meek_immediate_elect: false,
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
utils::read_validate_election::<NativeFloat64>("tests/data/ers97_meek.csv", "tests/data/ers97.blt", stv_opts, Some(2), &["exhausted", "quota"]);
|
||||
|
@ -77,6 +78,7 @@ fn meek06_ers97_fixed12() {
|
|||
bulk_exclude: false,
|
||||
defer_surpluses: true,
|
||||
meek_immediate_elect: true,
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
Fixed::set_dps(12);
|
||||
|
@ -151,6 +153,7 @@ fn meeknz_ers97_fixed12() {
|
|||
bulk_exclude: false,
|
||||
defer_surpluses: true,
|
||||
meek_immediate_elect: true,
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
Fixed::set_dps(12);
|
||||
|
|
|
@ -43,6 +43,7 @@ fn prsa1_rational() {
|
|||
bulk_exclude: false,
|
||||
defer_surpluses: false,
|
||||
meek_immediate_elect: false,
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 2,
|
||||
};
|
||||
utils::read_validate_election::<Rational>("tests/data/prsa1.csv", "tests/data/prsa1.blt", stv_opts, None, &["exhausted", "lbf"]);
|
||||
|
|
|
@ -50,6 +50,7 @@ fn scotland_linn07_fixed5() {
|
|||
bulk_exclude: false,
|
||||
defer_surpluses: false,
|
||||
meek_immediate_elect: false,
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 5,
|
||||
};
|
||||
Fixed::set_dps(5);
|
||||
|
@ -79,6 +80,7 @@ fn scotland_linn07_gfixed5() {
|
|||
bulk_exclude: false,
|
||||
defer_surpluses: false,
|
||||
meek_immediate_elect: false,
|
||||
constraint_mode: stv::ConstraintMode::GuardDoom,
|
||||
pp_decimals: 5,
|
||||
};
|
||||
GuardedFixed::set_dps(5);
|
||||
|
|
|
@ -142,11 +142,11 @@ where
|
|||
if candidate_state == "" {
|
||||
}
|
||||
else if candidate_state == "H" {
|
||||
assert!(count_card.state == CandidateState::Hopeful);
|
||||
assert!(count_card.state == CandidateState::Hopeful, "Unexpected state for \"{}\" at index {}", candidate.name, idx);
|
||||
} else if candidate_state == "EL" || candidate_state == "PEL" {
|
||||
assert!(count_card.state == CandidateState::Elected);
|
||||
assert!(count_card.state == CandidateState::Elected, "Unexpected state for \"{}\" at index {}", candidate.name, idx);
|
||||
} else if candidate_state == "EX" || candidate_state == "EXCLUDING" {
|
||||
assert!(count_card.state == CandidateState::Excluded);
|
||||
assert!(count_card.state == CandidateState::Excluded, "Unexpected state for \"{}\" at index {}", candidate.name, idx);
|
||||
} else {
|
||||
panic!("Unknown state descriptor {}", candidate_state);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue