2021-05-28 19:58:40 +10:00
|
|
|
/* OpenTally: Open-source election vote counting
|
|
|
|
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
use crate::constraints::{Constraints, ConstraintMatrix};
|
2021-05-29 02:13:47 +10:00
|
|
|
use crate::logger::Logger;
|
2021-05-28 19:58:40 +10:00
|
|
|
use crate::numbers::Number;
|
2021-07-23 16:45:54 +10:00
|
|
|
use crate::parser::blt;
|
2021-06-13 03:15:15 +10:00
|
|
|
use crate::sharandom::SHARandom;
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-07-23 16:45:54 +10:00
|
|
|
use pest::Parser;
|
|
|
|
|
2021-05-28 19:58:40 +10:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// An election to be counted
|
2021-05-28 19:58:40 +10:00
|
|
|
pub struct Election<N> {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Name of the election
|
2021-06-03 21:35:25 +10:00
|
|
|
pub name: String,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Number of candidates to be elected
|
2021-05-28 19:58:40 +10:00
|
|
|
pub seats: usize,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [Vec] of [Candidate]s in the election
|
2021-05-28 19:58:40 +10:00
|
|
|
pub candidates: Vec<Candidate>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Indexes of withdrawn candidates
|
2021-06-12 00:50:01 +10:00
|
|
|
pub withdrawn_candidates: Vec<usize>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [Vec] of [Ballot]s cast in the election
|
2021-05-28 19:58:40 +10:00
|
|
|
pub ballots: Vec<Ballot<N>>,
|
2021-06-27 21:57:24 +10:00
|
|
|
/// Constraints on candidates
|
|
|
|
pub constraints: Option<Constraints>,
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<N: Number> Election<N> {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Parse the given BLT file and return an [Election]
|
2021-07-23 16:45:54 +10:00
|
|
|
pub fn from_blt(input: &str) -> Self {
|
|
|
|
let mut main = blt::BLTParser::parse(blt::Rule::main, input).expect("Syntax Error").next().unwrap().into_inner();
|
|
|
|
|
|
|
|
let mut header = main.next().unwrap().into_inner();
|
|
|
|
let num_candidates = header.next().unwrap().as_str().parse().unwrap();
|
|
|
|
let seats = header.next().unwrap().as_str().parse().unwrap();
|
2021-05-28 19:58:40 +10:00
|
|
|
|
|
|
|
// Initialise the Election object
|
|
|
|
let mut election = Election {
|
2021-06-03 21:35:25 +10:00
|
|
|
name: String::new(),
|
2021-05-28 19:58:40 +10:00
|
|
|
seats: seats,
|
|
|
|
candidates: Vec::with_capacity(num_candidates),
|
2021-06-12 00:50:01 +10:00
|
|
|
withdrawn_candidates: Vec::new(),
|
2021-05-28 19:58:40 +10:00
|
|
|
ballots: Vec::new(),
|
2021-06-27 21:57:24 +10:00
|
|
|
constraints: None,
|
2021-05-28 19:58:40 +10:00
|
|
|
};
|
|
|
|
|
2021-07-23 16:45:54 +10:00
|
|
|
// Read withdrawn candidates
|
|
|
|
let withdrawn_pairs = main.next().unwrap().into_inner();
|
|
|
|
for withdrawn_cand in withdrawn_pairs {
|
|
|
|
let val = withdrawn_cand.as_str();
|
|
|
|
let val: usize = val[1..val.len()].parse().unwrap();
|
|
|
|
election.withdrawn_candidates.push(val - 1);
|
|
|
|
}
|
|
|
|
|
2021-05-28 19:58:40 +10:00
|
|
|
// Read ballots
|
2021-07-23 16:45:54 +10:00
|
|
|
let ballot_list = main.next().unwrap().into_inner();
|
|
|
|
for ballot in ballot_list {
|
|
|
|
let mut ballot_pairs = ballot.into_inner();
|
2021-06-12 00:50:01 +10:00
|
|
|
|
2021-07-23 16:45:54 +10:00
|
|
|
let value = N::parse(ballot_pairs.next().unwrap().as_str());
|
2021-05-28 19:58:40 +10:00
|
|
|
|
|
|
|
let mut ballot = Ballot {
|
|
|
|
orig_value: value,
|
|
|
|
preferences: Vec::new(),
|
|
|
|
};
|
|
|
|
|
2021-07-23 16:45:54 +10:00
|
|
|
let ballot_preferences = ballot_pairs.next().unwrap().into_inner();
|
|
|
|
for preference in ballot_preferences {
|
|
|
|
let preference: usize = preference.as_str().parse().unwrap();
|
|
|
|
ballot.preferences.push(preference - 1);
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
election.ballots.push(ballot);
|
|
|
|
}
|
|
|
|
|
2021-07-23 16:45:54 +10:00
|
|
|
// Zero line
|
|
|
|
main.next();
|
|
|
|
|
2021-05-28 19:58:40 +10:00
|
|
|
// Read candidates
|
2021-07-23 16:45:54 +10:00
|
|
|
for _ in 0..num_candidates {
|
|
|
|
let string = main.next().expect("Expected candidate name");
|
|
|
|
election.candidates.push(Candidate { name: blt::unwrap_string(string).to_string() });
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-03 21:35:25 +10:00
|
|
|
// Read name
|
2021-07-23 16:45:54 +10:00
|
|
|
let string = main.next().expect("Expected election title");
|
|
|
|
election.name.push_str(blt::unwrap_string(string));
|
2021-06-03 21:35:25 +10:00
|
|
|
|
2021-05-28 19:58:40 +10:00
|
|
|
return election;
|
|
|
|
}
|
2021-06-11 21:23:08 +10:00
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Convert ballots with weight >1 to multiple ballots of weight 1
|
2021-06-11 21:23:08 +10:00
|
|
|
pub fn normalise_ballots(&mut self) {
|
|
|
|
let mut normalised_ballots = Vec::new();
|
|
|
|
for ballot in self.ballots.iter() {
|
|
|
|
let mut n = N::new();
|
|
|
|
let one = N::one();
|
|
|
|
while n < ballot.orig_value {
|
|
|
|
let new_ballot = Ballot {
|
|
|
|
orig_value: N::one(),
|
|
|
|
preferences: ballot.preferences.clone(),
|
|
|
|
};
|
|
|
|
normalised_ballots.push(new_ballot);
|
|
|
|
n += &one;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self.ballots = normalised_ballots;
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// A candidate in an [Election]
|
2021-05-28 19:58:40 +10:00
|
|
|
#[derive(PartialEq, Eq, Hash)]
|
|
|
|
pub struct Candidate {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Name of the candidate
|
2021-05-28 19:58:40 +10:00
|
|
|
pub name: String,
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// The current state of counting an [Election]
|
2021-06-16 13:00:54 +10:00
|
|
|
//#[derive(Clone)]
|
|
|
|
pub struct CountState<'a, N: Number> {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Pointer to the [Election] being counted
|
2021-05-28 19:58:40 +10:00
|
|
|
pub election: &'a Election<N>,
|
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [HashMap] of [CountCard]s for each [Candidate] in the election
|
2021-05-28 19:58:40 +10:00
|
|
|
pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [CountCard] representing the exhausted pile
|
2021-05-28 19:58:40 +10:00
|
|
|
pub exhausted: CountCard<'a, N>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [CountCard] representing loss by fraction
|
2021-05-29 00:43:58 +10:00
|
|
|
pub loss_fraction: CountCard<'a, N>,
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [crate::stv::meek::BallotTree] for Meek STV
|
2021-06-16 13:00:54 +10:00
|
|
|
pub ballot_tree: Option<crate::stv::meek::BallotTree<'a, N>>,
|
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Values used to break ties, based on forwards tie-breaking
|
2021-06-13 00:15:14 +10:00
|
|
|
pub forwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Values used to break ties, based on backwards tie-breaking
|
2021-06-13 00:15:14 +10:00
|
|
|
pub backwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [SHARandom] for random tie-breaking
|
2021-06-13 03:15:15 +10:00
|
|
|
pub random: Option<SHARandom<'a>>,
|
2021-06-13 00:15:14 +10:00
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Quota for election
|
2021-06-07 20:52:18 +10:00
|
|
|
pub quota: Option<N>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Vote required for election
|
|
|
|
///
|
2021-07-23 01:21:29 +10:00
|
|
|
/// Only used in ERS97/ERS76.
|
2021-06-07 20:52:18 +10:00
|
|
|
pub vote_required_election: Option<N>,
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Number of candidates who have been declared elected
|
2021-05-28 19:58:40 +10:00
|
|
|
pub num_elected: usize,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Number of candidates who have been declared excluded
|
2021-05-28 19:58:40 +10:00
|
|
|
pub num_excluded: usize,
|
2021-05-29 01:22:46 +10:00
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
/// [ConstraintMatrix] for constrained elections
|
|
|
|
pub constraint_matrix: Option<ConstraintMatrix>,
|
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// The type of stage being counted
|
|
|
|
///
|
|
|
|
/// For example, "Surplus of", "Exclusion of"
|
2021-05-29 01:22:46 +10:00
|
|
|
pub kind: Option<&'a str>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// The description of the stage being counted, excluding [CountState::kind]
|
2021-05-29 01:22:46 +10:00
|
|
|
pub title: String,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// [Logger] for this stage of the count
|
2021-05-29 02:13:47 +10:00
|
|
|
pub logger: Logger<'a>,
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, N: Number> CountState<'a, N> {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Construct a new blank [CountState] for the given [Election]
|
2021-05-28 19:58:40 +10:00
|
|
|
pub fn new(election: &'a Election<N>) -> Self {
|
|
|
|
let mut state = CountState {
|
|
|
|
election: &election,
|
|
|
|
candidates: HashMap::new(),
|
|
|
|
exhausted: CountCard::new(),
|
2021-05-29 00:43:58 +10:00
|
|
|
loss_fraction: CountCard::new(),
|
2021-06-16 13:00:54 +10:00
|
|
|
ballot_tree: None,
|
2021-06-13 00:15:14 +10:00
|
|
|
forwards_tiebreak: None,
|
|
|
|
backwards_tiebreak: None,
|
2021-06-13 03:15:15 +10:00
|
|
|
random: None,
|
2021-06-07 20:52:18 +10:00
|
|
|
quota: None,
|
|
|
|
vote_required_election: None,
|
2021-05-28 19:58:40 +10:00
|
|
|
num_elected: 0,
|
|
|
|
num_excluded: 0,
|
2021-06-27 21:57:24 +10:00
|
|
|
constraint_matrix: None,
|
2021-05-29 01:22:46 +10:00
|
|
|
kind: None,
|
|
|
|
title: String::new(),
|
2021-05-29 02:13:47 +10:00
|
|
|
logger: Logger { entries: Vec::new() },
|
2021-05-28 19:58:40 +10:00
|
|
|
};
|
|
|
|
|
|
|
|
for candidate in election.candidates.iter() {
|
|
|
|
state.candidates.insert(candidate, CountCard::new());
|
|
|
|
}
|
|
|
|
|
2021-06-12 00:50:01 +10:00
|
|
|
for withdrawn_idx in election.withdrawn_candidates.iter() {
|
|
|
|
state.candidates.get_mut(&election.candidates[*withdrawn_idx]).unwrap().state = CandidateState::Withdrawn;
|
|
|
|
}
|
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
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);
|
|
|
|
|
2021-06-27 22:36:50 +10:00
|
|
|
// Require correct number of candidates to be elected
|
|
|
|
let idx = vec![0; constraints.0.len()];
|
|
|
|
cm[&idx].min = election.seats;
|
|
|
|
cm[&idx].max = election.seats;
|
|
|
|
|
2021-06-27 21:57:24 +10:00
|
|
|
state.constraint_matrix = Some(cm);
|
|
|
|
}
|
|
|
|
|
2021-05-28 19:58:40 +10:00
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// [Step](CountCard::step) every [CountCard] to prepare for the next stage
|
2021-05-28 19:58:40 +10:00
|
|
|
pub fn step_all(&mut self) {
|
|
|
|
for (_, count_card) in self.candidates.iter_mut() {
|
|
|
|
count_card.step();
|
|
|
|
}
|
|
|
|
self.exhausted.step();
|
2021-05-29 00:43:58 +10:00
|
|
|
self.loss_fraction.step();
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Current state of a [Candidate] during an election count
|
2021-05-28 19:58:40 +10:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct CountCard<'a, N> {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// State of the candidate
|
2021-05-28 19:58:40 +10:00
|
|
|
pub state: CandidateState,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Order of election or exclusion
|
|
|
|
///
|
|
|
|
/// Positive integers represent order of election; negative integers represent order of exclusion
|
2021-05-28 19:58:40 +10:00
|
|
|
pub order_elected: isize,
|
|
|
|
|
2021-06-16 13:00:54 +10:00
|
|
|
//pub orig_votes: N,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Net votes transferred to this candidate in this stage
|
2021-05-28 19:58:40 +10:00
|
|
|
pub transfers: N,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Votes of the candidate at the end of this stage
|
2021-05-28 19:58:40 +10:00
|
|
|
pub votes: N,
|
|
|
|
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Parcels of ballots assigned to this candidate
|
2021-05-28 19:58:40 +10:00
|
|
|
pub parcels: Vec<Parcel<'a, N>>,
|
2021-06-16 13:00:54 +10:00
|
|
|
|
|
|
|
/// Candidate's keep value (Meek STV)
|
|
|
|
pub keep_value: Option<N>,
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, N: Number> CountCard<'a, N> {
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Returns a new blank [CountCard]
|
2021-05-28 19:58:40 +10:00
|
|
|
pub fn new() -> Self {
|
|
|
|
return CountCard {
|
2021-06-12 00:50:01 +10:00
|
|
|
state: CandidateState::Hopeful,
|
2021-05-28 19:58:40 +10:00
|
|
|
order_elected: 0,
|
2021-06-16 13:00:54 +10:00
|
|
|
//orig_votes: N::new(),
|
2021-05-28 19:58:40 +10:00
|
|
|
transfers: N::new(),
|
|
|
|
votes: N::new(),
|
|
|
|
parcels: Vec::new(),
|
2021-06-16 13:00:54 +10:00
|
|
|
keep_value: None,
|
2021-05-28 19:58:40 +10:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Transfer the given number of votes to this [CountCard], incrementing [transfers](CountCard::transfers) and [votes](CountCard::votes)
|
2021-05-28 19:58:40 +10:00
|
|
|
pub fn transfer(&mut self, transfer: &'_ N) {
|
|
|
|
self.transfers += transfer;
|
|
|
|
self.votes += transfer;
|
|
|
|
}
|
|
|
|
|
2021-06-16 13:00:54 +10:00
|
|
|
/// Set [transfers](CountCard::transfers) to 0
|
2021-05-28 19:58:40 +10:00
|
|
|
pub fn step(&mut self) {
|
2021-06-16 13:00:54 +10:00
|
|
|
//self.orig_votes = self.votes.clone();
|
2021-05-28 19:58:40 +10:00
|
|
|
self.transfers = N::new();
|
|
|
|
}
|
2021-07-19 23:15:17 +10:00
|
|
|
|
|
|
|
/// Concatenate all parcels into a single parcel, leaving [parcels](CountCard::parcels) empty
|
|
|
|
pub fn concat_parcels(&mut self) -> Vec<Vote<'a, N>> {
|
|
|
|
let mut result = Vec::new();
|
|
|
|
for parcel in self.parcels.iter_mut() {
|
|
|
|
result.append(&mut parcel.votes);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Parcel of [Vote]s during a count
|
2021-07-19 23:15:17 +10:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct Parcel<'a, N> {
|
|
|
|
/// [Vote]s in this parcel
|
|
|
|
pub votes: Vec<Vote<'a, N>>,
|
2021-07-23 16:45:54 +10:00
|
|
|
/// Order for sorting with [crate::stv::ExclusionMethod::BySource]
|
2021-07-19 23:15:17 +10:00
|
|
|
pub source_order: usize,
|
|
|
|
}
|
2021-05-28 19:58:40 +10:00
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// Represents a [Ballot] with an associated value
|
2021-05-28 19:58:40 +10:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct Vote<'a, N> {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Ballot from which the vote is derived
|
2021-05-28 19:58:40 +10:00
|
|
|
pub ballot: &'a Ballot<N>,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Current value of the ballot
|
2021-05-28 19:58:40 +10:00
|
|
|
pub value: N,
|
2021-06-16 13:00:54 +10:00
|
|
|
/// Index of the next preference to examine
|
2021-05-28 19:58:40 +10:00
|
|
|
pub up_to_pref: usize,
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// A record of a voter's preferences
|
2021-05-28 19:58:40 +10:00
|
|
|
pub struct Ballot<N> {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Original value/weight of the ballot
|
2021-05-28 19:58:40 +10:00
|
|
|
pub orig_value: N,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Indexes of candidates preferenced on the ballot
|
2021-05-28 19:58:40 +10:00
|
|
|
pub preferences: Vec<usize>,
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:43:36 +10:00
|
|
|
/// State of a [Candidate] during a count
|
2021-05-28 19:58:40 +10:00
|
|
|
#[allow(dead_code)]
|
|
|
|
#[derive(PartialEq)]
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub enum CandidateState {
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Hopeful (continuing candidate)
|
2021-06-12 00:50:01 +10:00
|
|
|
Hopeful,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Required by constraints to be guarded from exclusion
|
2021-06-12 00:50:01 +10:00
|
|
|
Guarded,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Declared elected
|
2021-06-12 00:50:01 +10:00
|
|
|
Elected,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Required by constraints to be doomed to be excluded
|
2021-06-12 00:50:01 +10:00
|
|
|
Doomed,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Withdrawn candidate
|
2021-06-12 00:50:01 +10:00
|
|
|
Withdrawn,
|
2021-06-16 17:20:29 +10:00
|
|
|
/// Declared excluded
|
2021-06-12 00:50:01 +10:00
|
|
|
Excluded,
|
2021-05-28 19:58:40 +10:00
|
|
|
}
|