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') {
|
} else if (evt.data.type === 'finalResultSummary') {
|
||||||
divLogs2.insertAdjacentHTML('beforeend', evt.data.summary);
|
divLogs2.insertAdjacentHTML('beforeend', evt.data.summary);
|
||||||
document.getElementById('printPane').style.display = 'block';
|
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();
|
initWasm();
|
||||||
|
|
||||||
|
var numbers, election, opts, state, stageNum;
|
||||||
|
|
||||||
onmessage = function(evt) {
|
onmessage = function(evt) {
|
||||||
if (evt.data.type === 'countElection') {
|
if (evt.data.type === 'countElection') {
|
||||||
let numbers;
|
|
||||||
if (evt.data.numbers === 'fixed') {
|
if (evt.data.numbers === 'fixed') {
|
||||||
numbers = 'Fixed';
|
numbers = 'Fixed';
|
||||||
wasm.fixed_set_dps(evt.data.decimals);
|
wasm.fixed_set_dps(evt.data.decimals);
|
||||||
|
@ -23,14 +24,14 @@ onmessage = function(evt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init election
|
// 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) {
|
if (evt.data.normaliseBallots) {
|
||||||
wasm['election_normalise_ballots_' + numbers](election);
|
wasm['election_normalise_ballots_' + numbers](election);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init STV options
|
// Init STV options
|
||||||
let opts = wasm.STVOptions.new.apply(null, evt.data.optsStr);
|
opts = wasm.STVOptions.new.apply(null, evt.data.optsStr);
|
||||||
|
|
||||||
// Describe count
|
// Describe count
|
||||||
postMessage({'type': 'describeCount', 'content': wasm['describe_count_' + numbers](evt.data.filePath, election, opts)});
|
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)});
|
postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election, opts)});
|
||||||
|
|
||||||
// Step election
|
// Step election
|
||||||
let state = wasm['CountState' + numbers].new(election);
|
state = wasm['CountState' + numbers].new(election);
|
||||||
wasm['count_init_' + numbers](state, opts);
|
wasm['count_init_' + numbers](state, opts);
|
||||||
|
|
||||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](1, state, opts)});
|
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](1, state, opts)});
|
||||||
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
|
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);
|
let isDone = wasm['count_one_stage_' + numbers](state, opts);
|
||||||
if (isDone) {
|
if (isDone) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex === "RequireInput") {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)});
|
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)});
|
||||||
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
|
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': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state)});
|
||||||
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + 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 logger;
|
||||||
pub mod numbers;
|
pub mod numbers;
|
||||||
pub mod stv;
|
pub mod stv;
|
||||||
|
pub mod ties;
|
||||||
|
|
||||||
use git_version::git_version;
|
use git_version::git_version;
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
|
@ -208,7 +208,7 @@ where
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let is_done = stv::count_one_stage(&mut state, &stv_opts);
|
let is_done = stv::count_one_stage(&mut state, &stv_opts);
|
||||||
if is_done {
|
if is_done.unwrap() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
stage_num += 1;
|
stage_num += 1;
|
||||||
|
|
|
@ -22,6 +22,7 @@ pub mod wasm;
|
||||||
|
|
||||||
use crate::numbers::Number;
|
use crate::numbers::Number;
|
||||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
|
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
|
||||||
|
use crate::ties::TieStrategy;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
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) {
|
pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) {
|
||||||
distribute_first_preferences(&mut state);
|
distribute_first_preferences(&mut state);
|
||||||
calculate_quota(&mut state, opts);
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_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
|
where
|
||||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
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::Div<&'r N, Output=N>,
|
||||||
|
@ -281,36 +288,36 @@ where
|
||||||
|
|
||||||
// Finish count
|
// Finish count
|
||||||
if finished_before_stage(&state) {
|
if finished_before_stage(&state) {
|
||||||
return true;
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue exclusions
|
// Continue exclusions
|
||||||
if continue_exclusion(&mut state, &opts) {
|
if continue_exclusion(&mut state, &opts) {
|
||||||
calculate_quota(&mut state, opts);
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_quota(&mut state, opts);
|
elect_meeting_quota(&mut state, opts);
|
||||||
return false;
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Distribute surpluses
|
// Distribute surpluses
|
||||||
if distribute_surpluses(&mut state, &opts) {
|
if distribute_surpluses(&mut state, &opts)? {
|
||||||
calculate_quota(&mut state, opts);
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_quota(&mut state, opts);
|
elect_meeting_quota(&mut state, opts);
|
||||||
return false;
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt bulk election
|
// Attempt bulk election
|
||||||
if bulk_elect(&mut state) {
|
if bulk_elect(&mut state) {
|
||||||
return false;
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclude lowest hopeful
|
// Exclude lowest hopeful
|
||||||
if exclude_hopefuls(&mut state, &opts) {
|
if exclude_hopefuls(&mut state, &opts)? {
|
||||||
calculate_quota(&mut state, opts);
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_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> {
|
struct NextPreferencesResult<'a, N> {
|
||||||
|
@ -596,14 +603,15 @@ where
|
||||||
return true;
|
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
|
where
|
||||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
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::Div<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Neg<Output=N>
|
for<'r> &'r N: ops::Neg<Output=N>
|
||||||
{
|
{
|
||||||
let quota = state.quota.as_ref().unwrap();
|
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)
|
.filter(|(_, cc)| &cc.votes > quota)
|
||||||
.collect();
|
.collect();
|
||||||
let total_surpluses = has_surplus.iter()
|
let total_surpluses = has_surplus.iter()
|
||||||
|
@ -614,28 +622,37 @@ where
|
||||||
if opts.defer_surpluses {
|
if opts.defer_surpluses {
|
||||||
if can_defer_surpluses(state, opts, &total_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));
|
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 {
|
match opts.surplus_order {
|
||||||
SurplusOrder::BySize => {
|
SurplusOrder::BySize => {
|
||||||
// Compare b with a to sort high-low
|
// 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 => {
|
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
|
// Distribute top candidate's surplus
|
||||||
// TODO: Handle ties
|
let elected_candidate;
|
||||||
let elected_candidate = has_surplus.first_mut().unwrap().0;
|
|
||||||
|
// 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);
|
distribute_surplus(state, &opts, elected_candidate);
|
||||||
|
|
||||||
return true;
|
return Ok(true);
|
||||||
}
|
}
|
||||||
return false;
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the denominator of the transfer value
|
/// 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;
|
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
|
where
|
||||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
{
|
{
|
||||||
|
@ -958,16 +975,23 @@ where
|
||||||
|
|
||||||
// Exclude lowest ranked candidate
|
// Exclude lowest ranked candidate
|
||||||
if excluded_candidates.len() == 0 {
|
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)
|
.filter(|(_, cc)| cc.state == CandidateState::Hopeful)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Sort by votes
|
// Sort by votes
|
||||||
// TODO: Handle ties
|
hopefuls.sort_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
|
||||||
hopefuls.sort_unstable_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];
|
excluded_candidates = vec![&hopefuls.first().unwrap().0];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect();
|
let mut names: Vec<&str> = excluded_candidates.iter().map(|c| c.name.as_str()).collect();
|
||||||
names.sort();
|
names.sort();
|
||||||
|
@ -981,7 +1005,7 @@ where
|
||||||
|
|
||||||
exclude_candidates(state, opts, excluded_candidates);
|
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
|
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
|
// 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()
|
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))
|
.filter(|(_, cc)| cc.state == CandidateState::Excluded && cc.parcels.iter().any(|p| p.len() > 0))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ use crate::stv;
|
||||||
extern crate console_error_panic_hook;
|
extern crate console_error_panic_hook;
|
||||||
|
|
||||||
use js_sys::Array;
|
use js_sys::Array;
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
|
|
||||||
|
@ -61,8 +61,11 @@ macro_rules! impl_type {
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) -> bool {
|
pub fn [<count_one_stage_$type>](state: &mut [<CountState$type>], opts: &stv::STVOptions) -> Result<bool, JsValue> {
|
||||||
return stv::count_one_stage(&mut state.0, &opts);
|
match stv::count_one_stage(&mut state.0, &opts) {
|
||||||
|
Ok(v) => Ok(v),
|
||||||
|
Err(stv::STVError::RequireInput) => Err("RequireInput".into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reporting
|
// Reporting
|
||||||
|
|
|
@ -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 {
|
while stage_num < stage {
|
||||||
// Step through stages
|
// Step through stages
|
||||||
// Assert count not yet done
|
// 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;
|
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;
|
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 {
|
fn get_cand_stage(candidate: &Element, idx: usize) -> &Element {
|
||||||
|
|
|
@ -69,7 +69,7 @@ where
|
||||||
while stage_num < stage {
|
while stage_num < stage {
|
||||||
// Step through stages
|
// Step through stages
|
||||||
// Assert count not yet done
|
// 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;
|
stage_num += 1;
|
||||||
}
|
}
|
||||||
validate_stage(idx, &state, &records);
|
validate_stage(idx, &state, &records);
|
||||||
|
|
Loading…
Reference in New Issue