2021-06-11 18:09:26 +02: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-12 16:15:14 +02:00
|
|
|
use crate::election::{Candidate, CountState};
|
2021-06-12 16:56:18 +02:00
|
|
|
use crate::logger::smart_join;
|
2021-06-12 16:15:14 +02:00
|
|
|
use crate::numbers::Number;
|
2021-07-31 09:41:28 +02:00
|
|
|
use crate::stv::{STVError, STVOptions};
|
2021-06-11 18:09:26 +02:00
|
|
|
|
|
|
|
#[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
|
2021-08-04 17:12:53 +02:00
|
|
|
#[derive(Clone, 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
|
2021-06-11 18:09:26 +02:00
|
|
|
Forwards,
|
2021-06-16 09:20:29 +02:00
|
|
|
/// Break ties according to the candidate who most recently had more/fewer votes
|
2021-06-11 18:09:26 +02:00
|
|
|
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
|
2021-06-11 18:09:26 +02:00
|
|
|
Prompt,
|
|
|
|
}
|
|
|
|
|
2021-08-04 17:12:53 +02:00
|
|
|
/// Get a [Vec] of [TieStrategy] based on string representations
|
|
|
|
pub fn from_strs<S: AsRef<str>>(strs: Vec<S>, mut random_seed: Option<String>) -> Vec<TieStrategy> {
|
|
|
|
strs.into_iter().map(|t| match t.as_ref() {
|
|
|
|
"forwards" => TieStrategy::Forwards,
|
|
|
|
"backwards" => TieStrategy::Backwards,
|
|
|
|
"random" => TieStrategy::Random(random_seed.take().expect("Must provide a --random-seed if using --ties random")),
|
|
|
|
"prompt" => TieStrategy::Prompt,
|
|
|
|
_ => panic!("Invalid --ties"),
|
|
|
|
}).collect()
|
|
|
|
}
|
|
|
|
|
2021-06-12 19:15:15 +02:00
|
|
|
impl TieStrategy {
|
2021-06-14 12:43:36 +02:00
|
|
|
/// Convert to CLI argument representation
|
2021-06-12 16:15:14 +02:00
|
|
|
pub fn describe(&self) -> String {
|
2021-06-11 18:09:26 +02:00
|
|
|
match self {
|
2021-06-12 16:15:14 +02:00
|
|
|
Self::Forwards => "forwards",
|
|
|
|
Self::Backwards => "backwards",
|
2021-06-12 16:39:49 +02:00
|
|
|
Self::Random(_) => "random",
|
2021-06-12 16:15:14 +02:00
|
|
|
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-07-31 09:51:09 +02:00
|
|
|
pub fn choose_highest<'c, N: Number>(&self, state: &mut CountState<N>, opts: &STVOptions, candidates: &Vec<&'c Candidate>, prompt_text: &str) -> Result<&'c Candidate, STVError> {
|
2021-06-12 16:15:14 +02:00
|
|
|
match self {
|
|
|
|
Self::Forwards => {
|
2021-08-19 18:06:45 +02:00
|
|
|
match &state.forwards_tiebreak {
|
|
|
|
Some(tb) => {
|
|
|
|
let mut candidates = candidates.clone();
|
|
|
|
// Compare b to a to sort high-to-low
|
|
|
|
candidates.sort_unstable_by(|a, b| tb[b].cmp(&tb[a]));
|
|
|
|
if tb[candidates[0]] == tb[candidates[1]] {
|
|
|
|
return Err(STVError::UnresolvedTie);
|
|
|
|
} else {
|
|
|
|
state.logger.log_literal(format!("Tie between {} broken forwards.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
|
|
|
|
return Ok(candidates[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
// First stage
|
|
|
|
return Err(STVError::UnresolvedTie);
|
|
|
|
}
|
2021-06-12 16:15:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Self::Backwards => {
|
2021-08-19 18:06:45 +02:00
|
|
|
match &state.backwards_tiebreak {
|
|
|
|
Some(tb) => {
|
|
|
|
let mut candidates = candidates.clone();
|
|
|
|
candidates.sort_unstable_by(|a, b| tb[b].cmp(&tb[a]));
|
|
|
|
if tb[candidates[0]] == tb[candidates[1]] {
|
|
|
|
return Err(STVError::UnresolvedTie);
|
|
|
|
} else {
|
|
|
|
state.logger.log_literal(format!("Tie between {} broken backwards.", smart_join(&candidates.iter().map(|c| c.name.as_str()).collect())));
|
|
|
|
return Ok(candidates[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
// First stage
|
|
|
|
return Err(STVError::UnresolvedTie);
|
|
|
|
}
|
2021-06-12 16:15:14 +02:00
|
|
|
}
|
|
|
|
}
|
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())]);
|
|
|
|
}
|
2021-06-11 18:09:26 +02:00
|
|
|
Self::Prompt => {
|
2021-07-31 09:51:09 +02:00
|
|
|
match prompt(state, opts, candidates, prompt_text) {
|
2021-06-12 16:56:18 +02:00
|
|
|
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-11 18:09:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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-07-31 09:51:09 +02:00
|
|
|
pub fn choose_lowest<'c, N: Number>(&self, state: &mut CountState<N>, opts: &STVOptions, candidates: &Vec<&'c Candidate>, prompt_text: &str) -> Result<&'c Candidate, STVError> {
|
2021-06-11 18:09:26 +02:00
|
|
|
match self {
|
2021-06-12 16:15:14 +02:00
|
|
|
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-12 16:15:14 +02:00
|
|
|
);
|
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]] {
|
2021-06-12 16:15:14 +02:00
|
|
|
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())));
|
2021-06-12 16:15:14 +02:00
|
|
|
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-12 16:15:14 +02:00
|
|
|
);
|
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]] {
|
2021-06-12 16:15:14 +02:00
|
|
|
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())));
|
2021-06-12 16:15:14 +02:00
|
|
|
return Ok(candidates[0]);
|
|
|
|
}
|
|
|
|
}
|
2021-06-11 18:09:26 +02:00
|
|
|
Self::Random(_seed) => {
|
2021-07-31 09:51:09 +02:00
|
|
|
return self.choose_highest(state, opts, candidates, prompt_text);
|
2021-06-11 18:09:26 +02:00
|
|
|
}
|
|
|
|
Self::Prompt => {
|
2021-07-31 09:51:09 +02:00
|
|
|
return self.choose_highest(state, opts, candidates, prompt_text);
|
2021-06-11 18:09:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
2021-06-11 18:09:26 +02:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2021-07-31 09:51:09 +02:00
|
|
|
fn prompt<'c, N: Number>(state: &CountState<N>, opts: &STVOptions, candidates: &Vec<&'c Candidate>, prompt_text: &str) -> Result<&'c Candidate, STVError> {
|
2021-07-31 09:41:28 +02:00
|
|
|
// Show intrastage progress if required
|
|
|
|
if !state.logger.entries.is_empty() {
|
|
|
|
// Print stage details
|
|
|
|
match state.kind {
|
|
|
|
None => { println!("Tie during: {}", state.title); }
|
|
|
|
Some(kind) => { println!("Tie during: {} {}", kind, state.title); }
|
|
|
|
};
|
|
|
|
println!("{}", state.logger.render().join(" "));
|
|
|
|
|
|
|
|
// Print candidates
|
|
|
|
print!("{}", state.describe_candidates(opts));
|
|
|
|
|
|
|
|
// Print summary rows
|
|
|
|
print!("{}", state.describe_summary(opts));
|
|
|
|
|
|
|
|
println!("");
|
|
|
|
}
|
|
|
|
|
2021-06-11 18:09:26 +02:00
|
|
|
println!("Multiple tied candidates:");
|
|
|
|
for (i, candidate) in candidates.iter().enumerate() {
|
|
|
|
println!("{}. {}", i + 1, candidate.name);
|
|
|
|
}
|
|
|
|
let mut buffer = String::new();
|
|
|
|
loop {
|
2021-07-31 09:51:09 +02:00
|
|
|
print!("{} [1-{}] ", prompt_text, candidates.len());
|
2021-06-11 18:09:26 +02:00
|
|
|
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" {
|
2021-07-27 14:57:53 +02:00
|
|
|
fn get_user_input(s: &str) -> Option<String>;
|
2021-06-11 18:09:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
2021-07-31 09:51:09 +02:00
|
|
|
fn prompt<'c, N: Number>(state: &CountState<N>, opts: &STVOptions, candidates: &Vec<&'c Candidate>, prompt_text: &str) -> Result<&'c Candidate, STVError> {
|
2021-07-31 09:41:28 +02:00
|
|
|
let mut message = String::new();
|
|
|
|
|
|
|
|
// Show intrastage progress if required
|
|
|
|
if !state.logger.entries.is_empty() {
|
|
|
|
// Print stage details
|
|
|
|
match state.kind {
|
|
|
|
None => { message.push_str(&format!("Tie during: {}\n", state.title)); }
|
|
|
|
Some(kind) => { message.push_str(&format!("Tie during: {} {}\n", kind, state.title)); }
|
|
|
|
};
|
|
|
|
message.push_str(&state.logger.render().join(" "));
|
|
|
|
message.push('\n');
|
|
|
|
|
|
|
|
// Print candidates
|
|
|
|
message.push_str(&state.describe_candidates(opts));
|
|
|
|
message.push('\n');
|
|
|
|
|
|
|
|
// Print summary rows
|
|
|
|
message.push_str(&state.describe_summary(opts));
|
|
|
|
message.push('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
message.push_str(&"Multiple tied candidates:\n");
|
2021-06-11 18:09:26 +02:00
|
|
|
for (i, candidate) in candidates.iter().enumerate() {
|
|
|
|
message.push_str(&format!("{}. {}\n", i + 1, candidate.name));
|
|
|
|
}
|
2021-07-31 09:51:09 +02:00
|
|
|
message.push_str(&format!("{} [1-{}] ", prompt_text, candidates.len()));
|
2021-06-11 18:09:26 +02:00
|
|
|
|
2021-07-27 14:57:53 +02:00
|
|
|
loop {
|
|
|
|
let response = get_user_input(&message);
|
|
|
|
|
|
|
|
match response {
|
|
|
|
Some(response) => {
|
|
|
|
match response.trim().parse::<usize>() {
|
|
|
|
Ok(val) => {
|
|
|
|
if val >= 1 && val <= candidates.len() {
|
|
|
|
return Ok(candidates[val - 1]);
|
|
|
|
} else {
|
|
|
|
// Invalid selection
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(_) => {
|
|
|
|
// Invalid selection
|
|
|
|
continue;
|
2021-06-11 18:09:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-07-27 14:57:53 +02:00
|
|
|
None => {
|
|
|
|
// No available user input in buffer - stack will be unwound
|
|
|
|
unreachable!();
|
|
|
|
}
|
2021-06-11 18:09:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|