OpenTally/src/ties.rs

267 lines
8.4 KiB
Rust
Raw Normal View History

/* 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/>.
*/
use crate::election::{Candidate, CountState};
2021-06-12 16:56:18 +02:00
use crate::logger::smart_join;
use crate::numbers::Number;
use crate::stv::STVError;
#[allow(unused_imports)]
use wasm_bindgen::prelude::wasm_bindgen;
#[allow(unused_imports)]
use std::io::{stdin, stdout, Write};
2021-06-14 12:43:36 +02:00
/// Strategy for breaking ties
#[derive(PartialEq)]
2021-06-12 19:15:15 +02:00
pub enum TieStrategy {
2021-06-16 09:20:29 +02:00
/// Break ties according to the candidate who first had more/fewer votes
Forwards,
2021-06-16 09:20:29 +02:00
/// Break ties according to the candidate who most recently had more/fewer votes
Backwards,
2021-06-16 09:20:29 +02:00
/// Break ties randomly (see [crate::sharandom])
2021-06-12 19:15:15 +02:00
Random(String),
2021-06-16 09:20:29 +02:00
/// Prompt the user to break ties
Prompt,
}
2021-06-12 19:15:15 +02:00
impl TieStrategy {
2021-06-14 12:43:36 +02:00
/// Convert to CLI argument representation
pub fn describe(&self) -> String {
match self {
Self::Forwards => "forwards",
Self::Backwards => "backwards",
Self::Random(_) => "random",
Self::Prompt => "prompt",
}.to_string()
}
2021-06-14 12:43:36 +02:00
/// Break a tie between the given candidates, selecting the highest candidate
///
/// The given candidates are assumed to be tied in this round
2021-06-12 16:56:18 +02:00
pub fn choose_highest<'c, N: Number>(&self, state: &mut CountState<N>, candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
match self {
Self::Forwards => {
let mut candidates = candidates.clone();
candidates.sort_unstable_by(|a, b|
// Compare b to a to sort high-to-low
2021-06-29 07:31:38 +02:00
state.forwards_tiebreak.as_ref().unwrap()[b]
.cmp(&state.forwards_tiebreak.as_ref().unwrap()[a])
);
2021-06-29 07:31:38 +02:00
if state.forwards_tiebreak.as_ref().unwrap()[candidates[0]] == state.forwards_tiebreak.as_ref().unwrap()[candidates[1]] {
return Err(STVError::UnresolvedTie);
} else {
2021-06-12 16:56:18 +02:00
state.logger.log_literal(format!("Tie between {} broken forwards.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
return Ok(candidates[0]);
}
}
Self::Backwards => {
let mut candidates = candidates.clone();
candidates.sort_unstable_by(|a, b|
2021-06-29 07:31:38 +02:00
state.backwards_tiebreak.as_ref().unwrap()[b]
.cmp(&state.backwards_tiebreak.as_ref().unwrap()[a])
);
2021-06-29 07:31:38 +02:00
if state.backwards_tiebreak.as_ref().unwrap()[candidates[0]] == state.backwards_tiebreak.as_ref().unwrap()[candidates[1]] {
return Err(STVError::UnresolvedTie);
} else {
2021-06-12 16:56:18 +02:00
state.logger.log_literal(format!("Tie between {} broken backwards.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
return Ok(candidates[0]);
}
}
2021-06-12 19:15:15 +02:00
Self::Random(_) => {
state.logger.log_literal(format!("Tie between {} broken at random.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
return Ok(candidates[state.random.as_mut().unwrap().next(candidates.len())]);
}
Self::Prompt => {
2021-06-12 16:56:18 +02:00
match prompt(candidates) {
Ok(c) => {
state.logger.log_literal(format!("Tie between {} broken by manual intervention.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
return Ok(c);
}
Err(e) => { return Err(e); }
}
}
}
}
2021-06-14 12:43:36 +02:00
/// Break a tie between the given candidates, selecting the lowest candidate
///
/// The given candidates are assumed to be tied in this round
2021-06-12 16:56:18 +02:00
pub fn choose_lowest<'c, N: Number>(&self, state: &mut CountState<N>, candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
match self {
Self::Forwards => {
let mut candidates = candidates.clone();
candidates.sort_unstable_by(|a, b|
2021-06-29 07:31:38 +02:00
state.forwards_tiebreak.as_ref().unwrap()[a]
.cmp(&state.forwards_tiebreak.as_ref().unwrap()[b])
);
2021-06-29 07:31:38 +02:00
if state.forwards_tiebreak.as_ref().unwrap()[candidates[0]] == state.forwards_tiebreak.as_ref().unwrap()[candidates[1]] {
return Err(STVError::UnresolvedTie);
} else {
2021-06-12 16:56:18 +02:00
state.logger.log_literal(format!("Tie between {} broken forwards.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
return Ok(candidates[0]);
}
}
Self::Backwards => {
let mut candidates = candidates.clone();
candidates.sort_unstable_by(|a, b|
2021-06-29 07:31:38 +02:00
state.backwards_tiebreak.as_ref().unwrap()[a]
.cmp(&state.backwards_tiebreak.as_ref().unwrap()[b])
);
2021-06-29 07:31:38 +02:00
if state.backwards_tiebreak.as_ref().unwrap()[candidates[0]] == state.backwards_tiebreak.as_ref().unwrap()[candidates[1]] {
return Err(STVError::UnresolvedTie);
} else {
2021-06-12 16:56:18 +02:00
state.logger.log_literal(format!("Tie between {} broken backwards.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
return Ok(candidates[0]);
}
}
Self::Random(_seed) => {
return self.choose_highest(state, candidates);
}
Self::Prompt => {
return self.choose_highest(state, candidates);
}
}
}
}
2021-06-29 07:31:38 +02:00
/// Return all maximal items according to the given key
pub fn multiple_max_by<E: Copy, K, C: Ord>(items: &Vec<E>, key: K) -> Vec<E>
where
K: Fn(&E) -> C
{
let mut max_key = None;
let mut max_items = Vec::new();
for item in items.iter() {
let item_key = key(item);
match &max_key {
Some(v) => {
if &item_key > v {
max_key = Some(item_key);
max_items.clear();
max_items.push(*item);
} else if &item_key == v {
max_items.push(*item);
}
}
None => {
max_key = Some(item_key);
max_items.clear();
max_items.push(*item);
}
}
}
return max_items;
}
/// Return all minimal items according to the given key
pub fn multiple_min_by<E: Copy, K, C: Ord>(items: &Vec<E>, key: K) -> Vec<E>
where
K: Fn(&E) -> C
{
let mut min_key = None;
let mut min_items = Vec::new();
for item in items.iter() {
let item_key = key(item);
match &min_key {
Some(v) => {
if &item_key < v {
min_key = Some(item_key);
min_items.clear();
min_items.push(*item);
} else if &item_key == v {
min_items.push(*item);
}
}
None => {
min_key = Some(item_key);
min_items.clear();
min_items.push(*item);
}
}
}
return min_items;
}
2021-06-14 12:43:36 +02:00
/// Prompt the candidate for input, depending on CLI or WebAssembly target
// FIXME: This may have unexpected behaviour if the tie occurs in the middle of a stage
#[cfg(not(target_arch = "wasm32"))]
fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
println!("Multiple tied candidates:");
for (i, candidate) in candidates.iter().enumerate() {
println!("{}. {}", i + 1, candidate.name);
}
let mut buffer = String::new();
loop {
print!("Which candidate to select? [1-{}] ", candidates.len());
stdout().flush().expect("IO Error");
stdin().read_line(&mut buffer).expect("IO Error");
match buffer.trim().parse::<usize>() {
Ok(val) => {
if val >= 1 && val <= candidates.len() {
println!();
return Ok(candidates[val - 1]);
} else {
println!("Invalid selection");
continue;
}
}
Err(_) => {
println!("Invalid selection");
continue;
}
}
}
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
extern "C" {
fn read_user_input_buffer(s: &str) -> Option<String>;
}
#[cfg(target_arch = "wasm32")]
fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
let mut message = String::from("Multiple tied candidates:\n");
for (i, candidate) in candidates.iter().enumerate() {
message.push_str(&format!("{}. {}\n", i + 1, candidate.name));
}
message.push_str(&format!("Which candidate to select? [1-{}] ", candidates.len()));
match read_user_input_buffer(&message) {
Some(response) => {
match response.trim().parse::<usize>() {
Ok(val) => {
if val >= 1 && val <= candidates.len() {
return Ok(candidates[val - 1]);
} else {
let _ = read_user_input_buffer(&message);
return Err(STVError::RequireInput);
}
}
Err(_) => {
let _ = read_user_input_buffer(&message);
return Err(STVError::RequireInput);
}
}
}
None => {
return Err(STVError::RequireInput);
}
}
}