/* 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 . */ use crate::election::{Candidate, CountState}; 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}; #[derive(PartialEq)] pub enum TieStrategy<'s> { Forwards, Backwards, Random(&'s str), Prompt, } impl<'s> TieStrategy<'s> { pub fn describe(&self) -> String { match self { Self::Forwards => "forwards", Self::Backwards => "backwards", Self::Random(_) => "random", Self::Prompt => "prompt", }.to_string() } pub fn choose_highest<'c, N: Number>(&self, state: &mut CountState, 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 state.forwards_tiebreak.as_ref().unwrap().get(b).unwrap() .cmp(&state.forwards_tiebreak.as_ref().unwrap().get(a).unwrap()) ); if state.forwards_tiebreak.as_ref().unwrap().get(candidates[0]).unwrap() == state.forwards_tiebreak.as_ref().unwrap().get(candidates[1]).unwrap() { 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]); } } Self::Backwards => { let mut candidates = candidates.clone(); candidates.sort_unstable_by(|a, b| state.backwards_tiebreak.as_ref().unwrap().get(b).unwrap() .cmp(&state.backwards_tiebreak.as_ref().unwrap().get(a).unwrap()) ); if state.backwards_tiebreak.as_ref().unwrap().get(candidates[0]).unwrap() == state.backwards_tiebreak.as_ref().unwrap().get(candidates[1]).unwrap() { 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]); } } Self::Random(_seed) => { todo!() } Self::Prompt => { 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); } } } } } pub fn choose_lowest<'c, N: Number>(&self, state: &mut CountState, candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { match self { Self::Forwards => { let mut candidates = candidates.clone(); candidates.sort_unstable_by(|a, b| state.forwards_tiebreak.as_ref().unwrap().get(a).unwrap() .cmp(&state.forwards_tiebreak.as_ref().unwrap().get(b).unwrap()) ); if state.forwards_tiebreak.as_ref().unwrap().get(candidates[0]).unwrap() == state.forwards_tiebreak.as_ref().unwrap().get(candidates[1]).unwrap() { 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]); } } Self::Backwards => { let mut candidates = candidates.clone(); candidates.sort_unstable_by(|a, b| state.backwards_tiebreak.as_ref().unwrap().get(a).unwrap() .cmp(&state.backwards_tiebreak.as_ref().unwrap().get(b).unwrap()) ); if state.backwards_tiebreak.as_ref().unwrap().get(candidates[0]).unwrap() == state.backwards_tiebreak.as_ref().unwrap().get(candidates[1]).unwrap() { 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]); } } Self::Random(_seed) => { return self.choose_highest(state, candidates); } Self::Prompt => { return self.choose_highest(state, candidates); } } } } #[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::() { 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; } #[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::() { 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); } } }