Initial implementation of --constraint-mode repeat_count

This commit is contained in:
RunasSudo 2022-04-16 02:27:59 +10:00
parent df9223ebe6
commit 03af86733e
No known key found for this signature in database
GPG Key ID: 7234E476BF21C61A
18 changed files with 635 additions and 73 deletions

View File

@ -167,10 +167,33 @@ The algorithm used by the random number generator is specified at [rng.md](https
This file selector allows you to load a [CON file](https://yingtongli.me/git/OpenTally/about/docs/con-fmt.md) specifying constraints on the election. For example, if a certain minimum or maximum number of candidates can be elected from a particular category. This file selector allows you to load a [CON file](https://yingtongli.me/git/OpenTally/about/docs/con-fmt.md) specifying constraints on the election. For example, if a certain minimum or maximum number of candidates can be elected from a particular category.
OpenTally applies constraints using the GreyFitzgerald method. Whenever a candidate is declared elected or excluded, any candidate who must be elected to secure a conformant result is deemed *guarded*, and any candidate who must not be elected to secure a conformant result is deemed *doomed*. Any candidate who is doomed is excluded at the next opportunity. Any candidate who is guarded is prevented from being excluded. ### Constraint method (--constraint-method)
This dropdown allows you to select how constraints are applied. The options are:
*Guard/doom* (default):
When this option is selected, OpenTally applies constraints using the GreyFitzgerald method. Whenever a candidate is declared elected or excluded, any candidate who must be elected to secure a conformant result is deemed *guarded*, and any candidate who must not be elected to secure a conformant result is deemed *doomed*. Any candidate who is doomed is excluded at the next opportunity. Any candidate who is guarded is prevented from being excluded.
Multiple constraints are supported using the method described by Hill ([*Voting Matters* 1998;(9):24](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;(13):47](http://www.votingmatters.org.uk/ISSUE13/P3.HTM)). Multiple constraints are supported using the method described by Hill ([*Voting Matters* 1998;(9):24](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;(13):47](http://www.votingmatters.org.uk/ISSUE13/P3.HTM)).
*Repeat count*:
When this option is selected, only constraints specifying a maximum number of candidates to be elected from a particular group are supported. Other constraint groups will be **silently ignored**. Note that each candidate must still be assigned to exactly one group within each constraint.
The count proceeds as normal, until the point that a candidate would be elected who would violate the constraint. At this point, that candidate and all other candidates from the constrained group are excluded, and all previously excluded candidates from the non-constrained group are reintroduced.
All ballot papers are removed from the count, and redistributed among the candidates in the following order:
* Any undistributed surpluses, each surplus comprising one stage
* Any exhausted ballot papers, in one or more stages (according to *Exclusion method*)
* The ballot papers of each continuing candidate from the non-constrained group, in one or more stages (according to *Exclusion method*), candidate-by-candidate in random order or an order specified by the user (according to *Ties*, with options other than *Random* and *Prompt* ignored)
* The ballot papers of each continuing candidate from the constrained group, in like manner
Once all ballot papers have been so redistributed, the count resumes as usual.
This method is specified, for example, in Schedule 1.1 of the [Monash Student Association *Election Regulations* (2021)](https://msa.monash.edu/app/uploads/2021/07/MSA-Election-Regulations-2021.pdf).
## Report options ## Report options
### Report style ### Report style

View File

@ -183,7 +183,16 @@
Constraints: Constraints:
</div> </div>
<div> <div>
<label>
<input type="file" id="conFile"> <input type="file" id="conFile">
</label>
<label>
Method:
<select id="selConstraintMethod">
<option value="guard_doom" selected>Guard/doom</option>
<option value="repeat_count">Repeat count</option>
</select>
</label>
</div> </div>
<div class="subheading"> <div class="subheading">
Report options: Report options:

View File

@ -165,7 +165,7 @@ async function clickCount() {
document.getElementById('chkImmediateElect').checked, document.getElementById('chkImmediateElect').checked,
document.getElementById('txtMinThreshold').value, document.getElementById('txtMinThreshold').value,
conPath, conPath,
"guard_doom", document.getElementById('selConstraintMethod').value,
parseInt(document.getElementById('txtPPDP').value), parseInt(document.getElementById('txtPPDP').value),
]; ];

View File

@ -1,3 +1,20 @@
/* OpenTally: Open-source election vote counting
* Copyright © 20212022 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/>.
*/
importScripts('opentally.js?v=GITVERSION'); importScripts('opentally.js?v=GITVERSION');
var wasm = wasm_bindgen; var wasm = wasm_bindgen;
@ -53,7 +70,7 @@ onmessage = function(evt) {
// Init constraints if applicable // Init constraints if applicable
if (evt.data.conData) { if (evt.data.conData) {
wasm['election_load_constraints_' + numbers](election, evt.data.conData); wasm['election_load_constraints_' + numbers](election, evt.data.conData, opts);
} }
// Describe count // Describe count

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::constraints::Constraints; use crate::constraints::{self, Constraints};
use crate::election::{CandidateState, CountState, Election, StageKind}; use crate::election::{CandidateState, CountState, Election, StageKind};
use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational}; use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
use crate::parser::{bin, blt}; use crate::parser::{bin, blt};
@ -176,7 +176,7 @@ pub struct SubcmdOptions {
constraints: Option<String>, constraints: Option<String>,
/// Mode of handling constraints /// Mode of handling constraints
#[clap(help_heading=Some("CONSTRAINTS"), long, possible_values=&["guard_doom"], default_value="guard_doom")] #[clap(help_heading=Some("CONSTRAINTS"), long, possible_values=&["guard_doom", "repeat_count"], default_value="guard_doom")]
constraint_mode: String, constraint_mode: String,
// --------------------- // ---------------------
@ -207,25 +207,25 @@ pub fn main(cmd_opts: SubcmdOptions) -> Result<(), i32> {
// Read and count election according to --numbers // Read and count election according to --numbers
if cmd_opts.numbers == "rational" { if cmd_opts.numbers == "rational" {
let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?; let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?;
maybe_load_constraints(&mut election, &cmd_opts.constraints)?; maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
// Must specify ::<N> here and in a few other places because ndarray causes E0275 otherwise // Must specify ::<N> here and in a few other places because ndarray causes E0275 otherwise
count_election::<Rational>(election, cmd_opts)?; count_election::<Rational>(election, cmd_opts)?;
} else if cmd_opts.numbers == "float64" { } else if cmd_opts.numbers == "float64" {
let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?; let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?;
maybe_load_constraints(&mut election, &cmd_opts.constraints)?; maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
count_election::<NativeFloat64>(election, cmd_opts)?; count_election::<NativeFloat64>(election, cmd_opts)?;
} else if cmd_opts.numbers == "fixed" { } else if cmd_opts.numbers == "fixed" {
Fixed::set_dps(cmd_opts.decimals); Fixed::set_dps(cmd_opts.decimals);
let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?; let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?;
maybe_load_constraints(&mut election, &cmd_opts.constraints)?; maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
count_election::<Fixed>(election, cmd_opts)?; count_election::<Fixed>(election, cmd_opts)?;
} else if cmd_opts.numbers == "gfixed" { } else if cmd_opts.numbers == "gfixed" {
GuardedFixed::set_dps(cmd_opts.decimals); GuardedFixed::set_dps(cmd_opts.decimals);
let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?; let mut election = election_from_file(&cmd_opts.filename, cmd_opts.bin)?;
maybe_load_constraints(&mut election, &cmd_opts.constraints)?; maybe_load_constraints(&mut election, &cmd_opts.constraints, &cmd_opts.constraint_mode)?;
count_election::<GuardedFixed>(election, cmd_opts)?; count_election::<GuardedFixed>(election, cmd_opts)?;
} }
@ -248,7 +248,7 @@ fn election_from_file<N: Number>(path: &str, bin: bool) -> Result<Election<N>, i
} }
} }
fn maybe_load_constraints<N: Number>(election: &mut Election<N>, constraints: &Option<String>) -> Result<(), i32> { fn maybe_load_constraints<N: Number>(election: &mut Election<N>, constraints: &Option<String>, constraint_mode: &str) -> Result<(), i32> {
if let Some(c) = constraints { if let Some(c) = constraints {
let file = File::open(c).expect("IO Error"); let file = File::open(c).expect("IO Error");
let lines = io::BufReader::new(file).lines(); let lines = io::BufReader::new(file).lines();
@ -265,10 +265,14 @@ fn maybe_load_constraints<N: Number>(election: &mut Election<N>, constraints: &O
} }
// Validate constraints // Validate constraints
if let Err(err) = election.constraints.as_ref().unwrap().validate_constraints(election.candidates.len()) { if let Err(err) = election.constraints.as_ref().unwrap().validate_constraints(election.candidates.len(), constraint_mode.into()) {
println!("Constraint Validation Error: {}", err); println!("Constraint Validation Error: {}", err);
return Err(1); return Err(1);
} }
if constraint_mode == "repeat_count" {
constraints::init_repeat_count(election);
}
} }
Ok(()) Ok(())
@ -345,7 +349,7 @@ where
{ {
// Describe count // Describe count
let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value }); let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value });
print!("Count computed by OpenTally (revision {}). Read {:.0} ballots from \"{}\" for election \"{}\". There are {} candidates for {} vacancies. ", crate::VERSION, total_ballots, filename, election.name, election.candidates.len(), election.seats); print!("Count computed by OpenTally (revision {}). Read {:.0} ballots from \"{}\" for election \"{}\". There are {} candidates for {} vacancies. ", crate::VERSION, total_ballots, filename, election.name, election.candidates.iter().filter(|c| !c.is_dummy).count(), election.seats);
let opts_str = opts.describe::<N>(); let opts_str = opts.describe::<N>();
if !opts_str.is_empty() { if !opts_str.is_empty() {
println!("Counting using options \"{}\".", opts_str); println!("Counting using options \"{}\".", opts_str);
@ -538,6 +542,11 @@ where
StageKind::ExclusionOf(candidates) => { StageKind::ExclusionOf(candidates) => {
stage_results[2].push(format!(r#""{}""#, candidates.iter().map(|c| &c.name).sorted().join("+"))); stage_results[2].push(format!(r#""{}""#, candidates.iter().map(|c| &c.name).sorted().join("+")));
} }
StageKind::Rollback => todo!(),
StageKind::RollbackExhausted => todo!(),
StageKind::BallotsOf(candidate) => {
stage_results[2].push(format!(r#""{}""#, candidate.name));
}
StageKind::SurplusesDistributed => todo!(), StageKind::SurplusesDistributed => todo!(),
StageKind::BulkElection => { StageKind::BulkElection => {
//let mut elected_candidates = Vec::new(); //let mut elected_candidates = Vec::new();

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting /* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo) * Copyright © 20212022 Lee Yingtong Li (RunasSudo)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -15,9 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::election::{Candidate, CandidateState, CountCard, CountState, Election}; use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, StageKind, RollbackState};
use crate::numbers::Number; use crate::numbers::Number;
use crate::stv::{ConstraintMode, STVOptions}; use crate::stv::{self, gregory, sample, ConstraintMode, STVError, STVOptions, SurplusMethod, SurplusOrder};
use crate::ties::{self, TieStrategy};
use itertools::Itertools; use itertools::Itertools;
use ndarray::{Array, Dimension, IxDyn}; use ndarray::{Array, Dimension, IxDyn};
@ -89,8 +90,8 @@ impl Constraints {
return Ok(constraints); return Ok(constraints);
} }
/// Validate that each candidate is specified exactly once in each constraint /// Validate that each candidate is specified exactly once in each constraint, and (if applicable) limitations of the constraint mode are applied
pub fn validate_constraints(&self, num_candidates: usize) -> Result<(), ValidationError> { pub fn validate_constraints(&self, num_candidates: usize, constraint_mode: ConstraintMode) -> Result<(), ValidationError> {
for constraint in &self.0 { for constraint in &self.0 {
let mut remaining_candidates: Vec<usize> = (0..num_candidates).collect(); let mut remaining_candidates: Vec<usize> = (0..num_candidates).collect();
@ -105,6 +106,19 @@ impl Constraints {
} }
} }
} }
if constraint_mode == ConstraintMode::RepeatCount {
// Each group must be either a maximum constraint, or the remaining group
if group.min == 0 {
// Maximum constraint: OK
} else if group.max >= group.candidates.len() {
// Remaining group: OK
} else {
return Err(ValidationError::InvalidTwoStage(constraint.name.clone(), group.name.clone()));
}
// FIXME: Is other validation required?
}
} }
if !remaining_candidates.is_empty() { if !remaining_candidates.is_empty() {
@ -114,6 +128,24 @@ impl Constraints {
Ok(()) Ok(())
} }
/// Check if any elected candidates exceed constrained maximums
pub fn exceeds_maximum<'a, N: Number>(&self, election: &Election<N>, candidates: HashMap<&'a Candidate, CountCard<'a, N>>) -> Option<(&Constraint, &ConstrainedGroup)> {
for constraint in &self.0 {
for group in &constraint.groups {
let mut num_elected = 0;
for candidate in &group.candidates {
if candidates[&election.candidates[*candidate]].state == CandidateState::Elected {
num_elected += 1;
}
}
if num_elected > group.max {
return Some((&constraint, &group));
}
}
}
return None;
}
} }
/// Error parsing constraints /// Error parsing constraints
@ -154,6 +186,8 @@ pub enum ValidationError {
DuplicateCandidate(usize, String), DuplicateCandidate(usize, String),
/// Unassigned candidate in a constraint /// Unassigned candidate in a constraint
UnassignedCandidate(usize, String), UnassignedCandidate(usize, String),
/// Constraint is incompatible with ConstraintMode::TwoStage
InvalidTwoStage(String, String),
} }
impl fmt::Display for ValidationError { impl fmt::Display for ValidationError {
@ -165,6 +199,9 @@ impl fmt::Display for ValidationError {
ValidationError::UnassignedCandidate(candidate, constraint_name) => { ValidationError::UnassignedCandidate(candidate, constraint_name) => {
f.write_fmt(format_args!(r#"Unassigned candidate {} in constraint "{}""#, candidate + 1, constraint_name)) f.write_fmt(format_args!(r#"Unassigned candidate {} in constraint "{}""#, candidate + 1, constraint_name))
} }
ValidationError::InvalidTwoStage(constraint_name, group_name) => {
f.write_fmt(format_args!(r#"Constraint "{}" group "{}" is incompatible with --constraint-mode repeat_count"#, constraint_name, group_name))
}
} }
} }
} }
@ -187,7 +224,7 @@ fn duplicate_candidate() {
let input = r#""Constraint 1" "Group 1" 0 3 1 2 3 4 let input = r#""Constraint 1" "Group 1" 0 3 1 2 3 4
"Constraint 1" "Group 2" 0 3 4 5 6"#; "Constraint 1" "Group 2" 0 3 4 5 6"#;
let constraints = Constraints::from_con(input.lines()).unwrap(); let constraints = Constraints::from_con(input.lines()).unwrap();
constraints.validate_constraints(6).unwrap_err(); constraints.validate_constraints(6, ConstraintMode::GuardDoom).unwrap_err();
} }
#[test] #[test]
@ -195,7 +232,7 @@ fn unassigned_candidate() {
let input = r#""Constraint 1" "Group 1" 0 3 1 2 3 let input = r#""Constraint 1" "Group 1" 0 3 1 2 3
"Constraint 1" "Group 2" 0 3 4 5 6"#; "Constraint 1" "Group 2" 0 3 4 5 6"#;
let constraints = Constraints::from_con(input.lines()).unwrap(); let constraints = Constraints::from_con(input.lines()).unwrap();
constraints.validate_constraints(7).unwrap_err(); constraints.validate_constraints(7, ConstraintMode::GuardDoom).unwrap_err();
} }
/// Read an optionally quoted string, returning the string without quotes /// Read an optionally quoted string, returning the string without quotes
@ -262,6 +299,10 @@ pub enum ConstraintError {
NoConformantResult, NoConformantResult,
} }
// ----------------------
// GUARD/DOOM CONSTRAINTS
// ----------------------
/// Cell in a [ConstraintMatrix] /// Cell in a [ConstraintMatrix]
#[derive(Clone)] #[derive(Clone)]
pub struct ConstraintMatrixCell { pub struct ConstraintMatrixCell {
@ -332,6 +373,10 @@ impl ConstraintMatrix {
} }
for (i, candidate) in election.candidates.iter().enumerate() { for (i, candidate) in election.candidates.iter().enumerate() {
if candidate.is_dummy {
continue;
}
let idx: Vec<usize> = constraints.0.iter().map(|c| { let idx: Vec<usize> = constraints.0.iter().map(|c| {
for (j, group) in c.groups.iter().enumerate() { for (j, group) in c.groups.iter().enumerate() {
if group.candidates.contains(&i) { if group.candidates.contains(&i) {
@ -607,8 +652,8 @@ fn candidates_in_constraint_cell<'a, N: Number>(election: &'a Election<N>, candi
return result; return result;
} }
/// Clone and update the constraints matrix, with the state of the given candidates set to candidate_state /// Clone and update the constraints matrix, with the state of the given candidates set to candidate_state check if a conformant result is possible
pub fn try_constraints<N: Number>(state: &CountState<N>, candidates: &[&Candidate], candidate_state: CandidateState) -> Result<(), ConstraintError> { pub fn test_constraints_any_time<N: Number>(state: &CountState<N>, candidates: &[&Candidate], candidate_state: CandidateState) -> Result<(), ConstraintError> {
if state.constraint_matrix.is_none() { if state.constraint_matrix.is_none() {
return Ok(()); return Ok(());
} }
@ -697,10 +742,312 @@ pub fn update_constraints<N: Number>(state: &mut CountState<N>, opts: &STVOption
return guarded_or_doomed; return guarded_or_doomed;
} }
_ => { todo!() } ConstraintMode::RepeatCount => { return false; } // No action needed here: elect_hopefuls checks test_constraints_immediate
}
}
// ----------------------------------
// FOR --constraint-mode repeat_count
// ----------------------------------
/// Check constraints, with the state of the given candidates set to candidate_state check if this immediately violates constraints
pub fn test_constraints_immediate<'a, N: Number>(state: &CountState<'a, N>, candidates: &[&Candidate], candidate_state: CandidateState) -> Result<(), (&'a Constraint, &'a ConstrainedGroup)> {
if state.election.constraints.is_none() {
return Ok(());
} }
//return false; let mut trial_candidates = state.candidates.clone(); // TODO: Can probably be optimised by not cloning CountCard::parcels
for candidate in candidates {
trial_candidates.get_mut(candidate).unwrap().state = candidate_state;
}
if let Some((a, b)) = state.election.constraints.as_ref().unwrap().exceeds_maximum(state.election, trial_candidates) {
return Err((a, b));
}
return Ok(());
}
/// Initialise the [Election] as required for --constraint-mode repeat_count
pub fn init_repeat_count<N: Number>(election: &mut Election<N>) {
// Add dummy candidates
let mut new_candidates = Vec::new();
for candidate in &election.candidates {
let mut new_candidate = candidate.clone();
new_candidate.is_dummy = true;
new_candidates.push(new_candidate);
}
election.candidates.append(&mut new_candidates);
}
/// Initialise the rollback for [ConstraintMode::TwoStage]
pub fn init_repeat_count_rollback<'a, N: Number>(state: &mut CountState<'a, N>, constraint: &'a Constraint, group: &'a ConstrainedGroup) {
let mut rollback_candidates = HashMap::new();
let rollback_exhausted = state.exhausted.clone();
// Copy ballot papers to rollback state
for (candidate, count_card) in state.candidates.iter_mut() {
rollback_candidates.insert(*candidate, count_card.clone());
}
state.rollback_state = RollbackState::NeedsRollback { candidates: Some(rollback_candidates), exhausted: Some(rollback_exhausted), constraint, group };
}
/// Process one stage of rollback for [ConstraintMode::TwoStage]
pub fn rollback_one_stage<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
where
for<'r> &'r N: ops::Add<&'r N, Output=N>,
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>,
for<'r> &'r N: ops::Neg<Output=N>
{
if let RollbackState::NeedsRollback { candidates, exhausted, constraint, group } = &mut state.rollback_state {
let mut candidates = candidates.take().unwrap();
// Exclude candidates who cannot be elected due to constraint violations
let order_excluded = state.num_excluded + 1;
let mut excluded_candidates = Vec::new();
for candidate_idx in &group.candidates {
let count_card = state.candidates.get_mut(&state.election.candidates[*candidate_idx]).unwrap();
if count_card.state == CandidateState::Hopeful {
count_card.state = CandidateState::Excluded;
count_card.finalised = true;
state.num_excluded += 1;
count_card.order_elected = -(order_excluded as isize);
excluded_candidates.push(state.election.candidates[*candidate_idx].name.as_str());
}
}
// Prepare dummy candidates, etc.
for candidate in &state.election.candidates {
if candidate.is_dummy {
continue;
}
// Move ballot papers to dummy candidate
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == candidate.name && c.is_dummy).unwrap();
let dummy_count_card = state.candidates.get_mut(dummy_candidate).unwrap();
dummy_count_card.parcels.append(&mut candidates.get_mut(candidate).unwrap().parcels);
dummy_count_card.votes = candidates[candidate].votes.clone();
// Reset count
let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.parcels.clear();
count_card.votes = N::new();
count_card.transfers = N::new();
if candidates[candidate].state == CandidateState::Elected {
if &candidates[candidate].votes > state.quota.as_ref().unwrap() {
count_card.votes = state.quota.as_ref().unwrap().clone();
} else {
count_card.votes = candidates[candidate].votes.clone();
}
}
}
state.title = StageKind::Rollback;
state.logger.log_smart(
"Rolled back to apply constraints. {} is excluded.",
"Rolled back to apply constraints. {} are excluded.",
excluded_candidates.into_iter().sorted().collect()
);
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: exhausted.take(), candidate_distributing: None, constraint: Some(constraint), group: Some(group) };
return Ok(true);
}
if let RollbackState::RollingBack { candidates, exhausted, candidate_distributing, constraint, group } = &mut state.rollback_state {
let candidates = candidates.take().unwrap();
let mut exhausted = exhausted.take().unwrap();
let mut candidate_distributing = candidate_distributing.take();
let constraint = constraint.take().unwrap();
let group = group.take().unwrap();
// --------------------
// Distribute surpluses
let has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
.filter(|c| {
let cc = &candidates[c];
if !c.is_dummy && cc.state == CandidateState::Elected && !cc.finalised {
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
!state.candidates[dummy_candidate].finalised
} else {
false
}
})
.collect();
if !has_surplus.is_empty() {
// Distribute top candidate's surplus
let max_cands = match opts.surplus_order {
SurplusOrder::BySize => {
ties::multiple_max_by(&has_surplus, |c| &candidates[c].votes)
}
SurplusOrder::ByOrder => {
ties::multiple_min_by(&has_surplus, |c| candidates[c].order_elected)
}
};
let elected_candidate = if max_cands.len() > 1 {
stv::choose_highest(state, opts, &max_cands, "Which candidate's surplus to distribute?")?
} else {
max_cands[0]
};
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == elected_candidate.name && c.is_dummy).unwrap();
match opts.surplus {
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => { gregory::distribute_surplus(state, opts, dummy_candidate); }
SurplusMethod::IHare | SurplusMethod::Hare => { sample::distribute_surplus(state, opts, dummy_candidate)?; }
_ => unreachable!()
}
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing, constraint: Some(constraint), group: Some(group) };
return Ok(true);
}
// ----------------------------------
// Distribute exhausted ballot papers
// FIXME: Untested!
if exhausted.parcels.iter().any(|p| !p.votes.is_empty()) {
// Use arbitrary dummy candidate
let dummy_candidate = state.election.candidates.iter().find(|c| c.is_dummy).unwrap();
let dummy_count_card = state.candidates.get_mut(dummy_candidate).unwrap();
dummy_count_card.parcels.append(&mut exhausted.parcels);
state.title = StageKind::RollbackExhausted;
state.logger.log_literal(String::from("Distributing exhausted ballots."));
stv::exclude_candidates(state, opts, vec![dummy_candidate])?;
let dummy_count_card = state.candidates.get_mut(dummy_candidate).unwrap();
exhausted.parcels.append(&mut dummy_count_card.parcels);
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing: None, constraint: Some(constraint), group: Some(group) };
return Ok(true);
}
// ------------------------------------------------
// Distribute ballot papers of electable candidates
let electable_candidates_old: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of multiple
.filter(|c| {
let cc = &candidates[c];
let cand_idx = state.election.candidates.iter().position(|x| x == *c).unwrap();
if !c.is_dummy && !group.candidates.contains(&cand_idx) && !cc.finalised {
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
!state.candidates[dummy_candidate].finalised
} else {
false
}
})
.collect();
if !electable_candidates_old.is_empty() {
if candidate_distributing.is_none() || !electable_candidates_old.contains(candidate_distributing.as_ref().unwrap()) {
if electable_candidates_old.len() > 1 {
// Determine or prompt for which candidate to distribute
for strategy in opts.ties.iter() {
match strategy {
TieStrategy::Random(_) | TieStrategy::Prompt => {
candidate_distributing = Some(strategy.choose_lowest(state, opts, &electable_candidates_old, "Which candidate's ballots to distribute?").unwrap());
break;
}
TieStrategy::Forwards | TieStrategy::Backwards => {}
}
}
} else {
candidate_distributing = Some(electable_candidates_old[0]);
}
}
let candidate_distributing = candidate_distributing.unwrap();
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == candidate_distributing.name && c.is_dummy).unwrap();
state.title = StageKind::BallotsOf(candidate_distributing);
state.logger.log_smart(
"Distributing ballot papers of {}.",
"Distributing ballot papers of {}.",
vec![candidate_distributing.name.as_str()]
);
stv::exclude_candidates(state, opts, vec![dummy_candidate])?;
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing: Some(candidate_distributing), constraint: Some(constraint), group: Some(group) };
return Ok(true);
}
// --------------------------------------------------
// Distribute ballot papers of unelectable candidates
let unelectable_candidates_old: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of multiple
.filter(|c| {
let cc = &candidates[c];
let cand_idx = state.election.candidates.iter().position(|x| x == *c).unwrap();
if !c.is_dummy && group.candidates.contains(&cand_idx) && !cc.finalised {
let dummy_candidate = state.election.candidates.iter().find(|x| x.name == c.name && x.is_dummy).unwrap();
!state.candidates[dummy_candidate].finalised
} else {
false
}
})
.collect();
if !unelectable_candidates_old.is_empty() {
if candidate_distributing.is_none() || !unelectable_candidates_old.contains(candidate_distributing.as_ref().unwrap()) {
if unelectable_candidates_old.len() > 1 {
// Determine or prompt for which candidate to distribute
for strategy in opts.ties.iter() {
match strategy {
TieStrategy::Random(_) | TieStrategy::Prompt => {
candidate_distributing = Some(strategy.choose_lowest(state, opts, &unelectable_candidates_old, "Which candidate's ballots to distribute?").unwrap());
break;
}
TieStrategy::Forwards | TieStrategy::Backwards => {}
}
}
} else {
candidate_distributing = Some(unelectable_candidates_old[0]);
}
}
let candidate_distributing = candidate_distributing.unwrap();
let dummy_candidate = state.election.candidates.iter().find(|c| c.name == candidate_distributing.name && c.is_dummy).unwrap();
state.title = StageKind::BallotsOf(candidate_distributing);
state.logger.log_smart(
"Distributing ballot papers of {}.",
"Distributing ballot papers of {}.",
vec![candidate_distributing.name.as_str()]
);
stv::exclude_candidates(state, opts, vec![dummy_candidate])?;
state.rollback_state = RollbackState::RollingBack { candidates: Some(candidates), exhausted: Some(exhausted), candidate_distributing: Some(candidate_distributing), constraint: Some(constraint), group: Some(group) };
return Ok(true);
}
// ---------------------------
// Rollback complete: finalise
// Delete dummy candidates
for (candidate, count_card) in state.candidates.iter_mut() {
if candidate.is_dummy {
count_card.state = CandidateState::Withdrawn;
count_card.parcels.clear();
count_card.votes = N::new();
count_card.transfers = N::new();
}
}
state.logger.log_literal(String::from("Rollback complete."));
state.rollback_state = RollbackState::Normal;
state.num_excluded = state.candidates.values().filter(|cc| cc.state == CandidateState::Excluded).count();
return Ok(false);
}
unreachable!();
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting /* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo) * Copyright © 20212022 Lee Yingtong Li (RunasSudo)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::constraints::{Constraints, ConstraintMatrix}; use crate::constraints::{Constraint, Constraints, ConstrainedGroup, ConstraintMatrix};
use crate::logger::Logger; use crate::logger::Logger;
use crate::numbers::Number; use crate::numbers::Number;
use crate::sharandom::SHARandom; use crate::sharandom::SHARandom;
@ -36,6 +36,7 @@ use std::fmt;
/// An election to be counted /// An election to be counted
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
#[derive(Clone)]
pub struct Election<N> { pub struct Election<N> {
/// Name of the election /// Name of the election
pub name: String, pub name: String,
@ -92,14 +93,17 @@ impl<N: Number> Election<N> {
} }
/// A candidate in an [Election] /// A candidate in an [Election]
#[derive(Eq, Hash, PartialEq)] #[derive(Clone, Eq, Hash, PartialEq)]
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
pub struct Candidate { pub struct Candidate {
/// Name of the candidate /// Name of the candidate
pub name: String, pub name: String,
/// If this candidate is a dummy candidate (e.g. for --constraint-mode repeat_count)
pub is_dummy: bool,
} }
/// The current state of counting an [Election] /// The current state of counting an [Election]
#[derive(Clone)]
pub struct CountState<'a, N: Number> { pub struct CountState<'a, N: Number> {
/// Pointer to the [Election] being counted /// Pointer to the [Election] being counted
pub election: &'a Election<N>, pub election: &'a Election<N>,
@ -135,6 +139,8 @@ pub struct CountState<'a, N: Number> {
/// [ConstraintMatrix] for constrained elections /// [ConstraintMatrix] for constrained elections
pub constraint_matrix: Option<ConstraintMatrix>, pub constraint_matrix: Option<ConstraintMatrix>,
/// [RollbackState] when using [ConstraintMode::Rollback]
pub rollback_state: RollbackState<'a, N>,
/// Transfer table for this surplus/exclusion /// Transfer table for this surplus/exclusion
pub transfer_table: Option<TransferTable<'a, N>>, pub transfer_table: Option<TransferTable<'a, N>>,
@ -162,6 +168,7 @@ impl<'a, N: Number> CountState<'a, N> {
num_elected: 0, num_elected: 0,
num_excluded: 0, num_excluded: 0,
constraint_matrix: None, constraint_matrix: None,
rollback_state: RollbackState::Normal,
transfer_table: None, transfer_table: None,
title: StageKind::FirstPreferences, title: StageKind::FirstPreferences,
logger: Logger { entries: Vec::new() }, logger: Logger { entries: Vec::new() },
@ -169,7 +176,11 @@ impl<'a, N: Number> CountState<'a, N> {
// Init candidate count cards // Init candidate count cards
for candidate in election.candidates.iter() { for candidate in election.candidates.iter() {
state.candidates.insert(candidate, CountCard::new()); let mut count_card = CountCard::new();
if candidate.is_dummy {
count_card.state = CandidateState::Withdrawn;
}
state.candidates.insert(candidate, count_card);
} }
// Set withdrawn candidates state // Set withdrawn candidates state
@ -241,6 +252,10 @@ impl<'a, N: Number> CountState<'a, N> {
let mut result = String::new(); let mut result = String::new();
for (candidate, count_card) in candidates { for (candidate, count_card) in candidates {
if candidate.is_dummy {
continue;
}
match count_card.state { match count_card.state {
CandidateState::Hopeful => { CandidateState::Hopeful => {
result.push_str(&format!("- {}: {:.dps$} ({:.dps$})\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals)); result.push_str(&format!("- {}: {:.dps$} ({:.dps$})\n", candidate.name, count_card.votes, count_card.transfers, dps=opts.pp_decimals));
@ -281,7 +296,7 @@ impl<'a, N: Number> CountState<'a, N> {
result.push_str(&format!("Exhausted: {:.dps$} ({:.dps$})\n", self.exhausted.votes, self.exhausted.transfers, dps=opts.pp_decimals)); result.push_str(&format!("Exhausted: {:.dps$} ({:.dps$})\n", self.exhausted.votes, self.exhausted.transfers, dps=opts.pp_decimals));
result.push_str(&format!("Loss by fraction: {:.dps$} ({:.dps$})\n", self.loss_fraction.votes, self.loss_fraction.transfers, dps=opts.pp_decimals)); result.push_str(&format!("Loss by fraction: {:.dps$} ({:.dps$})\n", self.loss_fraction.votes, self.loss_fraction.transfers, dps=opts.pp_decimals));
let mut total_vote = self.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); let mut total_vote = self.candidates.iter().filter_map(|(c, cc)| if c.is_dummy { None } else { Some(cc) }).fold(N::zero(), |acc, cc| { acc + &cc.votes });
total_vote += &self.exhausted.votes; total_vote += &self.exhausted.votes;
total_vote += &self.loss_fraction.votes; total_vote += &self.loss_fraction.votes;
result.push_str(&format!("Total votes: {:.dps$}\n", total_vote, dps=opts.pp_decimals)); result.push_str(&format!("Total votes: {:.dps$}\n", total_vote, dps=opts.pp_decimals));
@ -306,6 +321,12 @@ pub enum StageKind<'a> {
SurplusOf(&'a Candidate), SurplusOf(&'a Candidate),
/// Exclusion of ... /// Exclusion of ...
ExclusionOf(Vec<&'a Candidate>), ExclusionOf(Vec<&'a Candidate>),
/// Rolled back (--constraint-mode repeat_count)
Rollback,
/// Exhausted ballots (--constraint-mode repeat_count)
RollbackExhausted,
/// Ballots of ... (--constraint-mode repeat_count)
BallotsOf(&'a Candidate),
/// Surpluses distributed (Meek) /// Surpluses distributed (Meek)
SurplusesDistributed, SurplusesDistributed,
/// Bulk election /// Bulk election
@ -319,6 +340,9 @@ impl<'a> StageKind<'a> {
StageKind::FirstPreferences => "", StageKind::FirstPreferences => "",
StageKind::SurplusOf(_) => "Surplus of", StageKind::SurplusOf(_) => "Surplus of",
StageKind::ExclusionOf(_) => "Exclusion of", StageKind::ExclusionOf(_) => "Exclusion of",
StageKind::Rollback => "",
StageKind::RollbackExhausted => "",
StageKind::BallotsOf(_) => "Ballots of",
StageKind::SurplusesDistributed => "", StageKind::SurplusesDistributed => "",
StageKind::BulkElection => "", StageKind::BulkElection => "",
}; };
@ -337,6 +361,15 @@ impl<'a> fmt::Display for StageKind<'a> {
StageKind::ExclusionOf(candidates) => { StageKind::ExclusionOf(candidates) => {
return f.write_fmt(format_args!("{} {}", self.kind_as_string(), candidates.iter().map(|c| &c.name).sorted().join(", "))); return f.write_fmt(format_args!("{} {}", self.kind_as_string(), candidates.iter().map(|c| &c.name).sorted().join(", ")));
} }
StageKind::Rollback => {
return f.write_str("Constraints applied");
}
StageKind::RollbackExhausted => {
return f.write_str("Exhausted ballots");
}
StageKind::BallotsOf(candidate) => {
return f.write_fmt(format_args!("{} {}", self.kind_as_string(), candidate.name));
}
StageKind::SurplusesDistributed => { StageKind::SurplusesDistributed => {
return f.write_str("Surpluses distributed"); return f.write_str("Surpluses distributed");
} }
@ -465,6 +498,7 @@ impl<'a, N> Vote<'a, N> {
} }
/// A record of a voter's preferences /// A record of a voter's preferences
#[derive(Clone)]
#[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))] #[cfg_attr(not(target_arch = "wasm32"), derive(Archive, Deserialize, Serialize))]
pub struct Ballot<N> { pub struct Ballot<N> {
/// Original value/weight of the ballot /// Original value/weight of the ballot
@ -530,3 +564,15 @@ pub enum CandidateState {
/// Declared excluded /// Declared excluded
Excluded, Excluded,
} }
/// If --constraint-mode repeat_count and redistribution is required, tracks the ballot papers being redistributed
#[allow(missing_docs)]
#[derive(Clone)]
pub enum RollbackState<'a, N> {
/// Not rolling back
Normal,
/// Start rolling back next stage
NeedsRollback { candidates: Option<HashMap<&'a Candidate, CountCard<'a, N>>>, exhausted: Option<CountCard<'a, N>>, constraint: &'a Constraint, group: &'a ConstrainedGroup },
/// Rolling back
RollingBack { candidates: Option<HashMap<&'a Candidate, CountCard<'a, N>>>, exhausted: Option<CountCard<'a, N>>, candidate_distributing: Option<&'a Candidate>, constraint: Option<&'a Constraint>, group: Option<&'a ConstrainedGroup> },
}

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting /* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo) * Copyright © 20212022 Lee Yingtong Li (RunasSudo)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -16,6 +16,7 @@
*/ */
/// Smart logger used in election counts /// Smart logger used in election counts
#[derive(Clone)]
pub struct Logger<'a> { pub struct Logger<'a> {
/// [Vec] of log entries for the current stage /// [Vec] of log entries for the current stage
pub entries: Vec<LogEntry<'a>>, pub entries: Vec<LogEntry<'a>>,
@ -71,6 +72,7 @@ impl<'a> Logger<'a> {
} }
/// Represents either a literal or smart log entry /// Represents either a literal or smart log entry
#[derive(Clone)]
pub enum LogEntry<'a> { pub enum LogEntry<'a> {
/// Smart log entry - see [SmartLogEntry] /// Smart log entry - see [SmartLogEntry]
Smart(SmartLogEntry<'a>), Smart(SmartLogEntry<'a>),
@ -79,6 +81,7 @@ pub enum LogEntry<'a> {
} }
/// Smart log entry /// Smart log entry
#[derive(Clone)]
pub struct SmartLogEntry<'a> { pub struct SmartLogEntry<'a> {
template1: &'a str, template1: &'a str,
template2: &'a str, template2: &'a str,

View File

@ -233,12 +233,21 @@ impl ops::Mul for Rational {
impl ops::Div for Rational { impl ops::Div for Rational {
type Output = Self; type Output = Self;
fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0) } fn div(self, rhs: Self) -> Self::Output {
if rhs.0.cmp0() == Ordering::Equal {
panic!("Divide by zero");
}
return Self(self.0 / rhs.0);
}
} }
impl ops::Rem for Rational { impl ops::Rem for Rational {
type Output = Self; type Output = Self;
fn rem(self, rhs: Self) -> Self::Output { fn rem(self, rhs: Self) -> Self::Output {
if rhs.0.cmp0() == Ordering::Equal {
panic!("Divide by zero");
}
// TODO: Is there a cleaner way of implementing this? // TODO: Is there a cleaner way of implementing this?
let mut quotient = self.0 / &rhs.0; let mut quotient = self.0 / &rhs.0;
quotient.rem_trunc_mut(); quotient.rem_trunc_mut();
@ -276,12 +285,20 @@ impl ops::Mul<&Self> for Rational {
impl ops::Div<&Self> for Rational { impl ops::Div<&Self> for Rational {
type Output = Self; type Output = Self;
fn div(self, rhs: &Self) -> Self::Output { Self(self.0 / &rhs.0) } fn div(self, rhs: &Self) -> Self::Output {
if rhs.0.cmp0() == Ordering::Equal {
panic!("Divide by zero");
}
return Self(self.0 / &rhs.0);
}
} }
impl ops::Rem<&Self> for Rational { impl ops::Rem<&Self> for Rational {
type Output = Self; type Output = Self;
fn rem(self, rhs: &Self) -> Self::Output { fn rem(self, rhs: &Self) -> Self::Output {
if rhs.0.cmp0() == Ordering::Equal {
panic!("Divide by zero");
}
let mut quotient = self.0 / &rhs.0; let mut quotient = self.0 / &rhs.0;
quotient.rem_trunc_mut(); quotient.rem_trunc_mut();
quotient *= &rhs.0; quotient *= &rhs.0;
@ -314,11 +331,19 @@ impl ops::MulAssign for Rational {
} }
impl ops::DivAssign for Rational { impl ops::DivAssign for Rational {
fn div_assign(&mut self, rhs: Self) { self.0 /= rhs.0; } fn div_assign(&mut self, rhs: Self) {
if rhs.0.cmp0() == Ordering::Equal {
panic!("Divide by zero");
}
self.0 /= rhs.0;
}
} }
impl ops::RemAssign for Rational { impl ops::RemAssign for Rational {
fn rem_assign(&mut self, rhs: Self) { fn rem_assign(&mut self, rhs: Self) {
if rhs.0.cmp0() == Ordering::Equal {
panic!("Divide by zero");
}
self.0 /= &rhs.0; self.0 /= &rhs.0;
self.0.rem_trunc_mut(); self.0.rem_trunc_mut();
self.0 *= rhs.0; self.0 *= rhs.0;
@ -350,11 +375,19 @@ impl ops::MulAssign<&Self> for Rational {
} }
impl ops::DivAssign<&Self> for Rational { impl ops::DivAssign<&Self> for Rational {
fn div_assign(&mut self, rhs: &Self) { self.0 /= &rhs.0; } fn div_assign(&mut self, rhs: &Self) {
if rhs.0.cmp0() == Ordering::Equal {
panic!("Divide by zero");
}
self.0 /= &rhs.0;
}
} }
impl ops::RemAssign<&Self> for Rational { impl ops::RemAssign<&Self> for Rational {
fn rem_assign(&mut self, rhs: &Self) { fn rem_assign(&mut self, rhs: &Self) {
if rhs.0.cmp0() == Ordering::Equal {
panic!("Divide by zero");
}
self.0 /= &rhs.0; self.0 /= &rhs.0;
self.0.rem_trunc_mut(); self.0.rem_trunc_mut();
self.0 *= &rhs.0; self.0 *= &rhs.0;
@ -395,12 +428,20 @@ impl ops::Mul<Self> for &Rational {
impl ops::Div<Self> for &Rational { impl ops::Div<Self> for &Rational {
type Output = Rational; type Output = Rational;
fn div(self, rhs: Self) -> Self::Output { Rational(rug::Rational::from(&self.0 / &rhs.0)) } fn div(self, rhs: Self) -> Self::Output {
if rhs.0.cmp0() == Ordering::Equal {
panic!("Divide by zero");
}
return Rational(rug::Rational::from(&self.0 / &rhs.0));
}
} }
impl ops::Rem<Self> for &Rational { impl ops::Rem<Self> for &Rational {
type Output = Rational; type Output = Rational;
fn rem(self, rhs: Self) -> Self::Output { fn rem(self, rhs: Self) -> Self::Output {
if rhs.0.cmp0() == Ordering::Equal {
panic!("Divide by zero");
}
let mut quotient = rug::Rational::from(&self.0 / &rhs.0); let mut quotient = rug::Rational::from(&self.0 / &rhs.0);
quotient.rem_trunc_mut(); quotient.rem_trunc_mut();
quotient *= &rhs.0; quotient *= &rhs.0;

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting /* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo) * Copyright © 20212022 Lee Yingtong Li (RunasSudo)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -178,7 +178,8 @@ impl<N: Number, I: Iterator<Item=char>> BLTParser<N, I> {
for _ in 0..self.num_candidates { for _ in 0..self.num_candidates {
let name = self.string()?; let name = self.string()?;
self.election.candidates.push(Candidate { self.election.candidates.push(Candidate {
name name,
is_dummy: false,
}); });
} }
let name = self.string()?; let name = self.string()?;

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting /* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo) * Copyright © 20212022 Lee Yingtong Li (RunasSudo)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -44,6 +44,7 @@ pub fn parse_reader<R: Read, N: Number>(reader: R, require_1: bool, require_sequ
col_map.insert(i, candidates.len()); col_map.insert(i, candidates.len());
candidates.push(Candidate { candidates.push(Candidate {
name: cand_name.to_string(), name: cand_name.to_string(),
is_dummy: false,
}); });
} }

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting /* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo) * Copyright © 20212022 Lee Yingtong Li (RunasSudo)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -19,6 +19,7 @@ use ibig::UBig;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
/// Deterministic random number generator using SHA256 /// Deterministic random number generator using SHA256
#[derive(Clone)]
pub struct SHARandom<'r> { pub struct SHARandom<'r> {
seed: &'r str, seed: &'r str,
counter: usize, counter: usize,

View File

@ -201,7 +201,7 @@ where
} }
/// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus] /// Distribute the surplus of a given candidate according to the Gregory method, based on [STVOptions::surplus]
fn distribute_surplus<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, elected_candidate: &'a Candidate) pub fn distribute_surplus<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, elected_candidate: &'a Candidate)
where where
for<'r> &'r N: ops::Add<&'r N, Output=N>, for<'r> &'r N: ops::Add<&'r N, Output=N>,
for<'r> &'r N: ops::Sub<&'r N, Output=N>, for<'r> &'r N: ops::Sub<&'r N, Output=N>,

View File

@ -28,6 +28,7 @@ use std::cmp::max;
use std::collections::HashMap; use std::collections::HashMap;
/// Table describing vote transfers during a surplus distribution or exclusion /// Table describing vote transfers during a surplus distribution or exclusion
#[derive(Clone)]
pub struct TransferTable<'e, N: Number> { pub struct TransferTable<'e, N: Number> {
/// Continuing candidates /// Continuing candidates
pub hopefuls: Vec<&'e Candidate>, pub hopefuls: Vec<&'e Candidate>,
@ -565,6 +566,7 @@ fn multiply_surpfrac<N: Number>(mut number: N, surpfrac_numer: &Option<N>, surpf
} }
/// Column in a [TransferTable] /// Column in a [TransferTable]
#[derive(Clone)]
pub struct TransferTableColumn<'e, N: Number> { pub struct TransferTableColumn<'e, N: Number> {
/// Value fraction of ballots counted in this column /// Value fraction of ballots counted in this column
pub value_fraction: N, pub value_fraction: N,
@ -599,6 +601,7 @@ impl<'e, N: Number> TransferTableColumn<'e, N> {
} }
/// Cell in a [TransferTable], representing transfers to one candidate at a particular value /// Cell in a [TransferTable], representing transfers to one candidate at a particular value
#[derive(Clone)]
pub struct TransferTableCell<N: Number> { pub struct TransferTableCell<N: Number> {
/// Ballots expressing a next preference for the continuing candidate /// Ballots expressing a next preference for the continuing candidate
pub ballots: N, pub ballots: N,

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting /* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo) * Copyright © 20212022 Lee Yingtong Li (RunasSudo)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -35,6 +35,7 @@ struct BallotInTree<'b, N: Number> {
} }
/// Tree-packed ballot representation /// Tree-packed ballot representation
#[derive(Clone)]
pub struct BallotTree<'t, N: Number> { pub struct BallotTree<'t, N: Number> {
num_ballots: N, num_ballots: N,
ballots: Vec<BallotInTree<'t, N>>, ballots: Vec<BallotInTree<'t, N>>,

View File

@ -29,9 +29,8 @@ pub mod sample;
pub mod wasm; pub mod wasm;
use crate::constraints; use crate::constraints;
use crate::election::Election;
use crate::numbers::Number; use crate::numbers::Number;
use crate::election::{Candidate, CandidateState, CountCard, CountState, StageKind, Vote}; use crate::election::{Candidate, CandidateState, CountCard, CountState, Election, RollbackState, StageKind, Vote};
use crate::sharandom::SHARandom; use crate::sharandom::SHARandom;
use crate::ties::{self, TieStrategy}; use crate::ties::{self, TieStrategy};
@ -221,6 +220,7 @@ impl STVOptions {
if self.quota_mode != QuotaMode::DynamicByTotal { return Err(STVError::InvalidOptions("--surplus meek requires --quota-mode dynamic_by_total")); } if self.quota_mode != QuotaMode::DynamicByTotal { return Err(STVError::InvalidOptions("--surplus meek requires --quota-mode dynamic_by_total")); }
if self.transferable_only { return Err(STVError::InvalidOptions("--surplus meek is incompatible with --transferable-only")); } if self.transferable_only { return Err(STVError::InvalidOptions("--surplus meek is incompatible with --transferable-only")); }
if self.exclusion != ExclusionMethod::SingleStage { return Err(STVError::InvalidOptions("--surplus meek requires --exclusion single_stage")); } if self.exclusion != ExclusionMethod::SingleStage { return Err(STVError::InvalidOptions("--surplus meek requires --exclusion single_stage")); }
if self.constraints_path.is_some() && self.constraint_mode == ConstraintMode::RepeatCount { return Err(STVError::InvalidOptions("--constraint-mode repeat_count requires a Gregory method for --surplus")); } // TODO: NYI?
} }
if self.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare { if self.surplus == SurplusMethod::IHare || self.surplus == SurplusMethod::Hare {
if self.round_quota != Some(0) { return Err(STVError::InvalidOptions("--surplus ihare and --surplus hare require --round-quota 0")); } if self.round_quota != Some(0) { return Err(STVError::InvalidOptions("--surplus ihare and --surplus hare require --round-quota 0")); }
@ -228,6 +228,7 @@ impl STVOptions {
if self.sample == SampleMethod::StratifyLR && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify is incompatible with --sample-per-ballot")); } if self.sample == SampleMethod::StratifyLR && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify is incompatible with --sample-per-ballot")); }
//if self.sample == SampleMethod::StratifyFloor && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify_floor is incompatible with --sample-per-ballot")); } //if self.sample == SampleMethod::StratifyFloor && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify_floor is incompatible with --sample-per-ballot")); }
if self.sample_per_ballot && !self.immediate_elect { return Err(STVError::InvalidOptions("--sample-per-ballot is incompatible with --no-immediate-elect")); } if self.sample_per_ballot && !self.immediate_elect { return Err(STVError::InvalidOptions("--sample-per-ballot is incompatible with --no-immediate-elect")); }
if self.constraints_path.is_some() && self.constraint_mode == ConstraintMode::RepeatCount { return Err(STVError::InvalidOptions("--constraint-mode repeat_count requires a Gregory method for --surplus")); } // TODO: NYI?
} }
if self.subtract_nontransferable && !self.transferable_only { return Err(STVError::InvalidOptions("--subtract-nontransferable requires --transferable-only")) } if self.subtract_nontransferable && !self.transferable_only { return Err(STVError::InvalidOptions("--subtract-nontransferable requires --transferable-only")) }
if self.min_threshold != "0" && self.defer_surpluses { return Err(STVError::InvalidOptions("--min-threshold is incompatible with --defer-surpluses (not yet implemented)")); } // TODO: NYI if self.min_threshold != "0" && self.defer_surpluses { return Err(STVError::InvalidOptions("--min-threshold is incompatible with --defer-surpluses (not yet implemented)")); } // TODO: NYI
@ -567,8 +568,8 @@ impl<S: AsRef<str>> From<S> for SampleMethod {
pub enum ConstraintMode { pub enum ConstraintMode {
/// Guard or doom candidates as soon as required to secure a conformant result /// Guard or doom candidates as soon as required to secure a conformant result
GuardDoom, GuardDoom,
/// TODO: NYI /// If constraints violated, exclude/reintroduce candidates as required and redistribute ballot papers
Rollback, RepeatCount,
} }
impl ConstraintMode { impl ConstraintMode {
@ -576,7 +577,7 @@ impl ConstraintMode {
fn describe(self) -> String { fn describe(self) -> String {
match self { match self {
ConstraintMode::GuardDoom => "--constraint-mode guard_doom", ConstraintMode::GuardDoom => "--constraint-mode guard_doom",
ConstraintMode::Rollback => "--constraint-mode rollback", ConstraintMode::RepeatCount => "--constraint-mode repeat_count",
}.to_string() }.to_string()
} }
} }
@ -585,7 +586,7 @@ impl<S: AsRef<str>> From<S> for ConstraintMode {
fn from(s: S) -> Self { fn from(s: S) -> Self {
match s.as_ref() { match s.as_ref() {
"guard_doom" => ConstraintMode::GuardDoom, "guard_doom" => ConstraintMode::GuardDoom,
"rollback" => ConstraintMode::Rollback, "repeat_count" => ConstraintMode::RepeatCount,
_ => panic!("Invalid --constraint-mode"), _ => panic!("Invalid --constraint-mode"),
} }
} }
@ -674,6 +675,13 @@ where
return Ok(true); return Ok(true);
} }
if let RollbackState::Normal = state.rollback_state {
} else if constraints::rollback_one_stage(state, opts)? {
elect_hopefuls(state, opts, true)?;
update_tiebreaks(state, opts);
return Ok(false);
}
// Attempt early bulk election // Attempt early bulk election
if opts.early_bulk_elect { if opts.early_bulk_elect {
if bulk_elect(state, opts)? { if bulk_elect(state, opts)? {
@ -727,20 +735,25 @@ where
} }
/// See [next_preferences] /// See [next_preferences]
struct NextPreferencesResult<'a, N> { pub struct NextPreferencesResult<'a, N> {
candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>, /// [NextPreferencesEntry] for each [Candidate]
exhausted: NextPreferencesEntry<'a, N>, pub candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>,
total_ballots: N, /// [NextPreferencesEntry] for exhausted ballots
pub exhausted: NextPreferencesEntry<'a, N>,
/// Total weight of ballots examined
pub total_ballots: N,
} }
/// See [next_preferences] /// See [next_preferences]
struct NextPreferencesEntry<'a, N> { pub struct NextPreferencesEntry<'a, N> {
votes: Vec<Vote<'a, N>>, /// Votes recording a next preference for the candidate
num_ballots: N, pub votes: Vec<Vote<'a, N>>,
/// Weight of such ballots
pub num_ballots: N,
} }
/// Count the given votes, grouping according to next available preference /// Count the given votes, grouping according to next available preference
fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a, N>>) -> NextPreferencesResult<'a, N> { pub fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a, N>>) -> NextPreferencesResult<'a, N> {
let mut result = NextPreferencesResult { let mut result = NextPreferencesResult {
candidates: HashMap::new(), candidates: HashMap::new(),
exhausted: NextPreferencesEntry { exhausted: NextPreferencesEntry {
@ -1015,6 +1028,12 @@ fn meets_vre<N: Number>(state: &CountState<N>, count_card: &CountCard<N>, opts:
/// ///
/// Returns `true` if any candidates were elected. /// Returns `true` if any candidates were elected.
fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> { fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError> {
// Do not interrupt rolling back!!
if let RollbackState::Normal = state.rollback_state {
} else {
return Ok(false);
}
if state.num_elected >= state.election.seats { if state.num_elected >= state.election.seats {
return Ok(false); return Ok(false);
} }
@ -1057,7 +1076,7 @@ fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOp
let mut leading_hopefuls: Vec<&Candidate> = hopefuls.iter().take(num_vacancies).map(|(c, _)| *c).collect(); let mut leading_hopefuls: Vec<&Candidate> = hopefuls.iter().take(num_vacancies).map(|(c, _)| *c).collect();
match constraints::try_constraints(state, &leading_hopefuls, CandidateState::Elected) { match constraints::test_constraints_any_time(state, &leading_hopefuls, CandidateState::Elected) {
Ok(_) => {} Ok(_) => {}
Err(_) => { return Ok(false); } // Bulk election conflicts with constraints Err(_) => { return Ok(false); } // Bulk election conflicts with constraints
} }
@ -1087,7 +1106,7 @@ fn elect_sure_winners<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOp
state.logger.log_smart( state.logger.log_smart(
"As they cannot now be overtaken, {} is elected to fill the remaining vacancy.", "As they cannot now be overtaken, {} is elected to fill the remaining vacancy.",
"As they cannot now be overtaken, {} are elected to fill the remaining vacancies.", "As they cannot now be overtaken, {} are elected to fill the remaining vacancies.",
vec![&candidate.name] vec![candidate.name.as_str()] // rust-analyzer doesn't understand &String -> &str
); );
leading_hopefuls.remove(leading_hopefuls.iter().position(|c| *c == candidate).unwrap()); leading_hopefuls.remove(leading_hopefuls.iter().position(|c| *c == candidate).unwrap());
@ -1127,6 +1146,22 @@ fn elect_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOption
max_cands[0] max_cands[0]
}; };
if opts.constraint_mode == ConstraintMode::RepeatCount && state.election.constraints.is_some() {
if let Err((constraint, group)) = constraints::test_constraints_immediate(state, &[candidate], CandidateState::Elected) {
// This election would violate a constraint, so stop here
state.logger.log_smart(
"The election of {} now would violate constraints.",
"The election of {} now would violate constraints.",
vec![candidate.name.as_str()]
);
// Trigger rollback
constraints::init_repeat_count_rollback(state, constraint, group);
return Ok(elected);
}
}
let count_card = state.candidates.get_mut(candidate).unwrap(); let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.state = CandidateState::Elected; count_card.state = CandidateState::Elected;
state.num_elected += 1; state.num_elected += 1;
@ -1139,7 +1174,7 @@ fn elect_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOption
state.logger.log_smart( state.logger.log_smart(
"{} meets the quota and is elected.", "{} meets the quota and is elected.",
"{} meet the quota and are elected.", "{} meet the quota and are elected.",
vec![&candidate.name] vec![candidate.name.as_str()]
); );
} else { } else {
// Elected with vote required // Elected with vote required
@ -1147,7 +1182,7 @@ fn elect_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOption
state.logger.log_smart( state.logger.log_smart(
"{} meets the vote required and is elected.", "{} meets the vote required and is elected.",
"{} meet the vote required and are elected.", "{} meet the vote required and are elected.",
vec![&candidate.name] vec![candidate.name.as_str()]
); );
} }
@ -1291,7 +1326,7 @@ fn do_bulk_elect<N: Number>(state: &mut CountState<N>, opts: &STVOptions, templa
state.logger.log_smart( state.logger.log_smart(
template1, template1,
template2, template2,
vec![&candidate.name] vec![candidate.name.as_str()]
); );
if constraints::update_constraints(state, opts) { if constraints::update_constraints(state, opts) {
@ -1396,7 +1431,7 @@ fn hopefuls_below_threshold<'a, N: Number>(state: &CountState<'a, N>, opts: &STV
.collect(); .collect();
// Do not exclude if this violates constraints // Do not exclude if this violates constraints
match constraints::try_constraints(state, &excluded_candidates, CandidateState::Excluded) { match constraints::test_constraints_any_time(state, &excluded_candidates, CandidateState::Excluded) {
Ok(_) => { return excluded_candidates; } Ok(_) => { return excluded_candidates; }
Err(_) => { return Vec::new(); } // Bulk exclusion conflicts with constraints Err(_) => { return Vec::new(); } // Bulk exclusion conflicts with constraints
} }
@ -1438,7 +1473,7 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST
let try_exclude: Vec<&Candidate> = try_exclude.iter().map(|(c, _)| **c).collect(); let try_exclude: Vec<&Candidate> = try_exclude.iter().map(|(c, _)| **c).collect();
// Do not exclude if this violates constraints // Do not exclude if this violates constraints
match constraints::try_constraints(state, &try_exclude, CandidateState::Excluded) { match constraints::test_constraints_any_time(state, &try_exclude, CandidateState::Excluded) {
Ok(_) => {} Ok(_) => {}
Err(_) => { break; } // Bulk exclusion conflicts with constraints Err(_) => { break; } // Bulk exclusion conflicts with constraints
} }
@ -1556,7 +1591,7 @@ where
} }
/// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion] /// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion]
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) -> Result<(), STVError> pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>) -> Result<(), STVError>
where where
for<'r> &'r N: ops::Sub<&'r N, Output=N>, 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::Mul<&'r N, Output=N>,

View File

@ -18,7 +18,7 @@
#![allow(rustdoc::private_intra_doc_links)] #![allow(rustdoc::private_intra_doc_links)]
#![allow(unused_unsafe)] // Confuses cargo check #![allow(unused_unsafe)] // Confuses cargo check
use crate::constraints::Constraints; use crate::constraints::{self, Constraints};
use crate::election::{CandidateState, CountState, Election, StageKind}; use crate::election::{CandidateState, CountState, Election, StageKind};
//use crate::numbers::{DynNum, Fixed, GuardedFixed, NativeFloat64, Number, NumKind, Rational}; //use crate::numbers::{DynNum, Fixed, GuardedFixed, NativeFloat64, Number, NumKind, Rational};
use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational}; use crate::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
@ -91,16 +91,21 @@ macro_rules! impl_type {
/// Call [Constraints::from_con] and set [Election::constraints] /// Call [Constraints::from_con] and set [Election::constraints]
#[cfg_attr(feature = "wasm", wasm_bindgen)] #[cfg_attr(feature = "wasm", wasm_bindgen)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn [<election_load_constraints_$type>](election: &mut [<Election$type>], text: String) { pub fn [<election_load_constraints_$type>](election: &mut [<Election$type>], text: String, opts: &STVOptions) {
election.0.constraints = match Constraints::from_con(text.lines()) { election.0.constraints = match Constraints::from_con(text.lines()) {
Ok(c) => Some(c), Ok(c) => Some(c),
Err(err) => wasm_error!("Constraint Syntax Error", err), Err(err) => wasm_error!("Constraint Syntax Error", err),
}; };
// Validate constraints // Validate constraints
if let Err(err) = election.0.constraints.as_ref().unwrap().validate_constraints(election.0.candidates.len()) { if let Err(err) = election.0.constraints.as_ref().unwrap().validate_constraints(election.0.candidates.len(), opts.0.constraint_mode) {
wasm_error!("Constraint Validation Error", err); wasm_error!("Constraint Validation Error", err);
} }
// Add dummy candidates if required
if opts.0.constraint_mode == stv::ConstraintMode::RepeatCount {
constraints::init_repeat_count(&mut election.0);
}
} }
/// Wrapper for [stv::preprocess_election] /// Wrapper for [stv::preprocess_election]
@ -327,7 +332,7 @@ pub fn describe_count<N: Number>(filename: String, election: &Election<N>, opts:
let mut result = String::from("<p>Count computed by OpenTally (revision "); let mut result = String::from("<p>Count computed by OpenTally (revision ");
result.push_str(crate::VERSION); result.push_str(crate::VERSION);
let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value }); let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value });
result.push_str(&format!(r#"). Read {:.0} ballots from &lsquo;{}&rsquo; for election &lsquo;{}&rsquo;. There are {} candidates for {} vacancies. "#, total_ballots, filename, election.name, election.candidates.len(), election.seats)); result.push_str(&format!(r#"). Read {:.0} ballots from &lsquo;{}&rsquo; for election &lsquo;{}&rsquo;. There are {} candidates for {} vacancies. "#, total_ballots, filename, election.name, election.candidates.iter().filter(|c| !c.is_dummy).count(), election.seats));
let opts_str = opts.describe::<N>(); let opts_str = opts.describe::<N>();
if !opts_str.is_empty() { if !opts_str.is_empty() {
@ -348,6 +353,10 @@ pub fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOpti
} }
for candidate in election.candidates.iter() { for candidate in election.candidates.iter() {
if candidate.is_dummy {
continue;
}
if report_style == "votes_transposed" { if report_style == "votes_transposed" {
result.push_str(&format!(r#"<tr class="candidate transfers"><td class="candidate-name">{}</td></tr>"#, candidate.name)); result.push_str(&format!(r#"<tr class="candidate transfers"><td class="candidate-name">{}</td></tr>"#, candidate.name));
} else { } else {
@ -381,7 +390,7 @@ pub fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>,
// Insert borders to left of new exclusions in Wright STV // Insert borders to left of new exclusions in Wright STV
let classes_o; // Outer version let classes_o; // Outer version
let classes_i; // Inner version let classes_i; // Inner version
if opts.exclusion == stv::ExclusionMethod::Wright && matches!(state.title, StageKind::ExclusionOf(_)) { if (opts.exclusion == stv::ExclusionMethod::Wright && matches!(state.title, StageKind::ExclusionOf(_))) || matches!(state.title, StageKind::Rollback) {
classes_o = r#" class="blw""#; classes_o = r#" class="blw""#;
classes_i = r#"blw "#; classes_i = r#"blw "#;
} else { } else {
@ -395,6 +404,10 @@ pub fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>,
hide_xfers_trsp = true; hide_xfers_trsp = true;
} else if opts.exclusion == stv::ExclusionMethod::Wright && matches!(state.title, StageKind::ExclusionOf(_)) { } else if opts.exclusion == stv::ExclusionMethod::Wright && matches!(state.title, StageKind::ExclusionOf(_)) {
hide_xfers_trsp = true; hide_xfers_trsp = true;
} else if let StageKind::Rollback = state.title {
hide_xfers_trsp = true;
} else if let StageKind::BulkElection = state.title {
hide_xfers_trsp = true;
} else { } else {
hide_xfers_trsp = false; hide_xfers_trsp = false;
} }
@ -403,7 +416,7 @@ pub fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>,
let kind_str = state.title.kind_as_string(); let kind_str = state.title.kind_as_string();
let title_str; let title_str;
match &state.title { match &state.title {
StageKind::FirstPreferences | StageKind::SurplusesDistributed | StageKind::BulkElection => { StageKind::FirstPreferences | StageKind::Rollback | StageKind::RollbackExhausted | StageKind::SurplusesDistributed | StageKind::BulkElection => {
title_str = format!("{}", state.title); title_str = format!("{}", state.title);
} }
StageKind::SurplusOf(candidate) => { StageKind::SurplusOf(candidate) => {
@ -417,6 +430,9 @@ pub fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>,
title_str = candidates.iter().map(|c| &c.name).join(",<br>"); title_str = candidates.iter().map(|c| &c.name).join(",<br>");
} }
} }
StageKind::BallotsOf(candidate) => {
title_str = candidate.name.clone();
}
}; };
match report_style { match report_style {
@ -447,6 +463,10 @@ pub fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>,
} }
for candidate in state.election.candidates.iter() { for candidate in state.election.candidates.iter() {
if candidate.is_dummy {
continue;
}
let count_card = &state.candidates[candidate]; let count_card = &state.candidates[candidate];
// TODO: REFACTOR THIS!! // TODO: REFACTOR THIS!!
@ -585,7 +605,7 @@ pub fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>,
} }
// Calculate total votes // Calculate total votes
let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); let mut total_vote = state.candidates.iter().filter_map(|(c, cc)| if c.is_dummy { None } else { Some(cc) }).fold(N::zero(), |acc, cc| { acc + &cc.votes });
total_vote += &state.exhausted.votes; total_vote += &state.exhausted.votes;
total_vote += &state.loss_fraction.votes; total_vote += &state.loss_fraction.votes;
@ -667,6 +687,10 @@ pub fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &s
// Candidate states // Candidate states
for candidate in state.election.candidates.iter() { for candidate in state.election.candidates.iter() {
if candidate.is_dummy {
continue;
}
let count_card = &state.candidates[candidate]; let count_card = &state.candidates[candidate];
if count_card.state == stv::CandidateState::Elected { if count_card.state == stv::CandidateState::Elected {
result.push(&format!(r#"<td{} class="bb elected">ELECTED {}</td>"#, rowspan, count_card.order_elected).into()); result.push(&format!(r#"<td{} class="bb elected">ELECTED {}</td>"#, rowspan, count_card.order_elected).into());

View File

@ -1,5 +1,5 @@
/* OpenTally: Open-source election vote counting /* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo) * Copyright © 20212022 Lee Yingtong Li (RunasSudo)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -220,7 +220,7 @@ where
/// Prompt the candidate for input, depending on CLI or WebAssembly target /// Prompt the candidate for input, depending on CLI or WebAssembly target
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn prompt<'c, N: Number>(state: &CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> { pub fn prompt<'c, N: Number>(state: &CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> {
// Show intrastage progress if required // Show intrastage progress if required
if !state.logger.entries.is_empty() { if !state.logger.entries.is_empty() {
// Print stage details // Print stage details
@ -269,8 +269,9 @@ extern "C" {
fn get_user_input(s: &str) -> Option<String>; fn get_user_input(s: &str) -> Option<String>;
} }
/// Prompt the candidate for input, depending on CLI or WebAssembly target
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
fn prompt<'c, N: Number>(state: &CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> { pub fn prompt<'c, N: Number>(state: &CountState<N>, opts: &STVOptions, candidates: &[&'c Candidate], prompt_text: &str) -> Result<&'c Candidate, STVError> {
let mut message = String::new(); let mut message = String::new();
// Show intrastage progress if required // Show intrastage progress if required