Initial implementation of prompt-based tie breaking
This commit is contained in:
parent
4c4099ee22
commit
a038efc8a4
@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,17 +40,36 @@ 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;
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex === "RequireInput") {
|
||||
return;
|
||||
} else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)});
|
||||
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
|
||||
@ -58,4 +78,16 @@ onmessage = function(evt) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,16 +975,23 @@ 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());
|
||||
|
||||
// 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();
|
||||
names.sort();
|
||||
@ -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();
|
||||
|
||||
|
@ -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
124
src/ties.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user