Initial implementation of prompt-based tie breaking

This commit is contained in:
RunasSudo 2021-06-12 02:09:26 +10:00
parent 4c4099ee22
commit a038efc8a4
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
10 changed files with 234 additions and 43 deletions

View File

@ -70,6 +70,13 @@ worker.onmessage = function(evt) {
} else if (evt.data.type === 'finalResultSummary') {
divLogs2.insertAdjacentHTML('beforeend', evt.data.summary);
document.getElementById('printPane').style.display = 'block';
} else if (evt.data.type === 'requireInput') {
let response = window.prompt(evt.data.message);
while (response === null) {
response = window.prompt(evt.data.message);
}
worker.postMessage({'type': 'userInput', 'response': response});
}
}

View File

@ -8,9 +8,10 @@ async function initWasm() {
}
initWasm();
var numbers, election, opts, state, stageNum;
onmessage = function(evt) {
if (evt.data.type === 'countElection') {
let numbers;
if (evt.data.numbers === 'fixed') {
numbers = 'Fixed';
wasm.fixed_set_dps(evt.data.decimals);
@ -23,14 +24,14 @@ onmessage = function(evt) {
}
// Init election
let election = wasm['election_from_blt_' + numbers](evt.data.electionData);
election = wasm['election_from_blt_' + numbers](evt.data.electionData);
if (evt.data.normaliseBallots) {
wasm['election_normalise_ballots_' + numbers](election);
}
// Init STV options
let opts = wasm.STVOptions.new.apply(null, evt.data.optsStr);
opts = wasm.STVOptions.new.apply(null, evt.data.optsStr);
// Describe count
postMessage({'type': 'describeCount', 'content': wasm['describe_count_' + numbers](evt.data.filePath, election, opts)});
@ -39,23 +40,54 @@ onmessage = function(evt) {
postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election, opts)});
// Step election
let state = wasm['CountState' + numbers].new(election);
state = wasm['CountState' + numbers].new(election);
wasm['count_init_' + numbers](state, opts);
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](1, state, opts)});
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
for (let stageNum = 2;; stageNum++) {
stageNum = 2;
resume_count();
} else if (evt.data.type == 'userInput') {
user_input_buffer = evt.data.response;
resume_count();
}
}
function resume_count() {
for (;; stageNum++) {
try {
let isDone = wasm['count_one_stage_' + numbers](state, opts);
if (isDone) {
break;
}
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)});
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
} catch (ex) {
if (ex === "RequireInput") {
return;
} else {
throw ex;
}
}
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state)});
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state)});
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)});
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
}
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state)});
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state)});
}
var user_input_buffer = null;
function read_user_input_buffer(message) {
if (user_input_buffer === null) {
postMessage({'type': 'requireInput', 'message': message});
return null;
} else {
let user_input = user_input_buffer;
user_input_buffer = null;
return user_input;
}
}

View File

@ -19,6 +19,7 @@ pub mod election;
pub mod logger;
pub mod numbers;
pub mod stv;
pub mod ties;
use git_version::git_version;
use wasm_bindgen::prelude::wasm_bindgen;

View File

@ -208,7 +208,7 @@ where
loop {
let is_done = stv::count_one_stage(&mut state, &stv_opts);
if is_done {
if is_done.unwrap() {
break;
}
stage_num += 1;

View File

@ -22,6 +22,7 @@ pub mod wasm;
use crate::numbers::Number;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
use crate::ties::TieStrategy;
use itertools::Itertools;
use wasm_bindgen::prelude::wasm_bindgen;
@ -264,13 +265,19 @@ impl ExclusionMethod {
}
}
#[wasm_bindgen]
#[derive(Debug)]
pub enum STVError {
RequireInput,
}
pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) {
distribute_first_preferences(&mut state);
calculate_quota(&mut state, opts);
elect_meeting_quota(&mut state, opts);
}
pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Div<&'r N, Output=N>,
@ -281,36 +288,36 @@ where
// Finish count
if finished_before_stage(&state) {
return true;
return Ok(true);
}
// Continue exclusions
if continue_exclusion(&mut state, &opts) {
calculate_quota(&mut state, opts);
elect_meeting_quota(&mut state, opts);
return false;
return Ok(false);
}
// Distribute surpluses
if distribute_surpluses(&mut state, &opts) {
if distribute_surpluses(&mut state, &opts)? {
calculate_quota(&mut state, opts);
elect_meeting_quota(&mut state, opts);
return false;
return Ok(false);
}
// Attempt bulk election
if bulk_elect(&mut state) {
return false;
return Ok(false);
}
// Exclude lowest hopeful
if exclude_hopefuls(&mut state, &opts) {
if exclude_hopefuls(&mut state, &opts)? {
calculate_quota(&mut state, opts);
elect_meeting_quota(&mut state, opts);
return false;
return Ok(false);
}
todo!();
panic!("Count incomplete but unable to proceed");
}
struct NextPreferencesResult<'a, N> {
@ -596,14 +603,15 @@ where
return true;
}
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> bool
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Div<&'r N, Output=N>,
for<'r> &'r N: ops::Neg<Output=N>
{
let quota = state.quota.as_ref().unwrap();
let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
let mut has_surplus: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
.map(|c| (c, state.candidates.get(c).unwrap()))
.filter(|(_, cc)| &cc.votes > quota)
.collect();
let total_surpluses = has_surplus.iter()
@ -614,28 +622,37 @@ where
if opts.defer_surpluses {
if can_defer_surpluses(state, opts, &total_surpluses) {
state.logger.log_literal(format!("Distribution of surpluses totalling {:.dps$} votes will be deferred.", total_surpluses, dps=opts.pp_decimals));
return false;
return Ok(false);
}
}
match opts.surplus_order {
SurplusOrder::BySize => {
// Compare b with a to sort high-low
has_surplus.sort_unstable_by(|a, b| b.1.votes.partial_cmp(&a.1.votes).unwrap());
has_surplus.sort_by(|a, b| b.1.votes.partial_cmp(&a.1.votes).unwrap());
}
SurplusOrder::ByOrder => {
has_surplus.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap());
has_surplus.sort_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap());
}
}
// Distribute top candidate's surplus
// TODO: Handle ties
let elected_candidate = has_surplus.first_mut().unwrap().0;
let elected_candidate;
// Handle ties
if has_surplus.len() > 1 && has_surplus[0].1.votes == has_surplus[1].1.votes {
let max_votes = &has_surplus[0].1.votes;
let has_surplus = has_surplus.into_iter().filter_map(|(c, cc)| if &cc.votes == max_votes { Some(c) } else { None }).collect();
elected_candidate = TieStrategy::Prompt.choose_highest(has_surplus)?;
} else {
elected_candidate = has_surplus.first_mut().unwrap().0;
}
distribute_surplus(state, &opts, elected_candidate);
return true;
return Ok(true);
}
return false;
return Ok(false);
}
/// Return the denominator of the transfer value
@ -945,7 +962,7 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST
return excluded_candidates;
}
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
where
for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
@ -958,15 +975,22 @@ where
// Exclude lowest ranked candidate
if excluded_candidates.len() == 0 {
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
let mut hopefuls: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
.map(|c| (c, state.candidates.get(c).unwrap()))
.filter(|(_, cc)| cc.state == CandidateState::Hopeful)
.collect();
// Sort by votes
// TODO: Handle ties
hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
hopefuls.sort_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
excluded_candidates = vec![&hopefuls.first().unwrap().0];
// Handle ties
if hopefuls.len() > 1 && hopefuls[0].1.votes == hopefuls[1].1.votes {
let min_votes = &hopefuls[0].1.votes;
let hopefuls = hopefuls.into_iter().filter_map(|(c, cc)| if &cc.votes == min_votes { Some(c) } else { None }).collect();
excluded_candidates = vec![TieStrategy::Prompt.choose_lowest(hopefuls)?];
} else {
excluded_candidates = vec![&hopefuls.first().unwrap().0];
}
}
let mut names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect();
@ -981,7 +1005,7 @@ where
exclude_candidates(state, opts, excluded_candidates);
return true;
return Ok(true);
}
fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
@ -990,7 +1014,7 @@ where
{
// Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers
let mut excluded_with_votes: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
//.filter(|(_, cc)| cc.state == CandidateState::Excluded && !cc.votes.is_zero())
//.filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && !cc.votes.is_zero())
.filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| p.len() > 0))
.collect();

View File

@ -22,7 +22,7 @@ use crate::stv;
extern crate console_error_panic_hook;
use js_sys::Array;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
// Init
@ -61,8 +61,11 @@ macro_rules! impl_type {
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) -> bool {
return stv::count_one_stage(&mut state.0, &opts);
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) -> Result<bool, JsValue> {
match stv::count_one_stage(&mut state.0, &opts) {
Ok(v) => Ok(v),
Err(stv::STVError::RequireInput) => Err("RequireInput".into()),
}
}
// Reporting

124
src/ties.rs Normal file
View File

@ -0,0 +1,124 @@
/* 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;
use crate::stv::STVError;
#[allow(unused_imports)]
use wasm_bindgen::prelude::wasm_bindgen;
#[allow(unused_imports)]
use std::io::{stdin, stdout, Write};
pub enum TieStrategy<'s> {
Forwards,
Backwards,
Random(&'s str),
Prompt,
}
impl<'s> TieStrategy<'s> {
pub fn choose_highest<'c>(&self, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
match self {
Self::Forwards => { todo!() }
Self::Backwards => { todo!() }
Self::Random(_seed) => { todo!() }
Self::Prompt => {
return prompt(candidates);
}
}
}
pub fn choose_lowest<'c>(&self, candidates: Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
match self {
Self::Forwards => { todo!() }
Self::Backwards => { todo!() }
Self::Random(_seed) => {
return self.choose_highest(candidates);
}
Self::Prompt => {
return self.choose_highest(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::<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);
}
}
}

View File

@ -83,7 +83,7 @@ fn ers97_rational() {
while stage_num < stage {
// Step through stages
// Assert count not yet done
assert_eq!(stv::count_one_stage(&mut state, &stv_opts), false);
assert_eq!(stv::count_one_stage(&mut state, &stv_opts).unwrap(), false);
stage_num += 1;
}

View File

@ -119,11 +119,11 @@ fn scotland_linn07_fixed5() {
}
}
assert_eq!(stv::count_one_stage(&mut state, &stv_opts), false);
assert_eq!(stv::count_one_stage(&mut state, &stv_opts).unwrap(), false);
stage_num += 1;
}
assert_eq!(stv::count_one_stage(&mut state, &stv_opts), true);
assert_eq!(stv::count_one_stage(&mut state, &stv_opts).unwrap(), true);
}
fn get_cand_stage(candidate: &Element, idx: usize) -> &Element {

View File

@ -69,7 +69,7 @@ where
while stage_num < stage {
// Step through stages
// Assert count not yet done
assert_eq!(stv::count_one_stage(&mut state, &stv_opts), false);
assert_eq!(stv::count_one_stage(&mut state, &stv_opts).unwrap(), false);
stage_num += 1;
}
validate_stage(idx, &state, &records);