Describe election count/options in web UI

This commit is contained in:
RunasSudo 2021-06-03 21:35:25 +10:00
parent 3bb538e99e
commit ba8d9bf79c
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
11 changed files with 120 additions and 22 deletions

View File

@ -80,6 +80,11 @@ async function clickCount() {
parseInt(document.getElementById('txtPPDP').value), parseInt(document.getElementById('txtPPDP').value),
); );
// Describe count
let filePath = document.getElementById('bltFile').value;
filePath = filePath.substring(Math.max(filePath.lastIndexOf('\\'), filePath.lastIndexOf('/')) + 1);
document.getElementById('resultLogs1').innerHTML = wasm.describe_count_Rational(filePath, election, opts);
// Step election // Step election
let state = wasm.CountStateRational.new(election); let state = wasm.CountStateRational.new(election);
wasm.count_init_Rational(state, opts); wasm.count_init_Rational(state, opts);

View File

@ -71,7 +71,7 @@ table {
} }
.result td { .result td {
padding: 0px 8px; padding: 0px 8px;
min-height: 1em; height: 1em;
} }
td.count { td.count {
text-align: right; text-align: right;

View File

@ -21,6 +21,7 @@ use crate::numbers::Number;
use std::collections::HashMap; use std::collections::HashMap;
pub struct Election<N> { pub struct Election<N> {
pub name: String,
pub seats: usize, pub seats: usize,
pub candidates: Vec<Candidate>, pub candidates: Vec<Candidate>,
pub ballots: Vec<Ballot<N>>, pub ballots: Vec<Ballot<N>>,
@ -36,6 +37,7 @@ impl<N: Number> Election<N> {
// Initialise the Election object // Initialise the Election object
let mut election = Election { let mut election = Election {
name: String::new(),
seats: seats, seats: seats,
candidates: Vec::with_capacity(num_candidates), candidates: Vec::with_capacity(num_candidates),
ballots: Vec::new(), ballots: Vec::new(),
@ -66,7 +68,7 @@ impl<N: Number> Election<N> {
} }
// Read candidates // Read candidates
for line in lines.take(num_candidates) { for line in lines.by_ref().take(num_candidates) {
let mut line = &line[..]; let mut line = &line[..];
if line.starts_with("\"") && line.ends_with("\"") { if line.starts_with("\"") && line.ends_with("\"") {
line = &line[1..line.len()-1]; line = &line[1..line.len()-1];
@ -75,6 +77,14 @@ impl<N: Number> Election<N> {
election.candidates.push(Candidate { name: line.to_string() }); election.candidates.push(Candidate { name: line.to_string() });
} }
// Read name
let line = lines.next().expect("Syntax Error");
let mut line = &line[..];
if line.starts_with("\"") && line.ends_with("\"") {
line = &line[1..line.len()-1];
}
election.name.push_str(line);
return election; return election;
} }
} }

View File

@ -19,3 +19,7 @@ pub mod election;
pub mod logger; pub mod logger;
pub mod numbers; pub mod numbers;
pub mod stv; pub mod stv;
use git_version::git_version;
pub const VERSION: &str = git_version!(args=["--always", "--dirty=-dev"], fallback="unknown");

View File

@ -20,17 +20,14 @@ use opentally::election::{Candidate, CandidateState, CountCard, CountState, Coun
use opentally::numbers::{NativeFloat64, Number, Rational}; use opentally::numbers::{NativeFloat64, Number, Rational};
use clap::{AppSettings, Clap}; use clap::{AppSettings, Clap};
use git_version::git_version;
use std::fs::File; use std::fs::File;
use std::io::{self, BufRead}; use std::io::{self, BufRead};
use std::ops; use std::ops;
const VERSION: &str = git_version!(args=["--always", "--dirty=-dev"], fallback="unknown");
/// Open-source election vote counting /// Open-source election vote counting
#[derive(Clap)] #[derive(Clap)]
#[clap(name="OpenTally", version=VERSION)] #[clap(name="OpenTally", version=opentally::VERSION)]
struct Opts { struct Opts {
#[clap(subcommand)] #[clap(subcommand)]
command: Command, command: Command,

View File

@ -45,6 +45,8 @@ where
{ {
fn new() -> Self; fn new() -> Self;
fn describe() -> String;
fn pow_assign(&mut self, exponent: i32); fn pow_assign(&mut self, exponent: i32);
fn floor_mut(&mut self, dps: usize); fn floor_mut(&mut self, dps: usize);
fn ceil_mut(&mut self, dps: usize); fn ceil_mut(&mut self, dps: usize);

View File

@ -31,6 +31,8 @@ pub struct NativeFloat64(ImplType);
impl Number for NativeFloat64 { impl Number for NativeFloat64 {
fn new() -> Self { Self(0.0) } fn new() -> Self { Self(0.0) }
fn describe() -> String { "--numbers float64".to_string() }
fn pow_assign(&mut self, exponent: i32) { fn pow_assign(&mut self, exponent: i32) {
self.0 = self.0.powi(exponent); self.0 = self.0.powi(exponent);
} }

View File

@ -30,6 +30,8 @@ pub struct Rational(BigRational);
impl Number for Rational { impl Number for Rational {
fn new() -> Self { Self(BigRational::zero()) } fn new() -> Self { Self(BigRational::zero()) }
fn describe() -> String { "--numbers rational".to_string() }
fn pow_assign(&mut self, exponent: i32) { fn pow_assign(&mut self, exponent: i32) {
self.0 = self.0.pow(exponent); self.0 = self.0.pow(exponent);
} }

View File

@ -30,6 +30,8 @@ pub struct Rational(rug::Rational);
impl Number for Rational { impl Number for Rational {
fn new() -> Self { Self(rug::Rational::new()) } fn new() -> Self { Self(rug::Rational::new()) }
fn describe() -> String { "--numbers rational".to_string() }
fn pow_assign(&mut self, exponent: i32) { fn pow_assign(&mut self, exponent: i32) {
self.0.pow_assign(exponent); self.0.pow_assign(exponent);
} }

View File

@ -99,8 +99,29 @@ impl STVOptions {
} }
} }
impl STVOptions {
// Not exported to WebAssembly
pub fn describe<N: Number>(&self) -> String {
let mut flags = Vec::new();
flags.push(N::describe());
if let Some(dps) = self.round_tvs { flags.push(format!("--round-tvs {}", dps)); }
if let Some(dps) = self.round_weights { flags.push(format!("--round-weights {}", dps)); }
if let Some(dps) = self.round_votes { flags.push(format!("--round-votes {}", dps)); }
if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); }
if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); }
if self.quota_criterion != QuotaCriterion::GreaterOrEqual { flags.push(self.quota_criterion.describe()); }
if self.surplus != SurplusMethod::WIG { flags.push(self.surplus.describe()); }
if self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); }
if self.transferable_only { flags.push("--transferable-only".to_string()); }
if self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); }
if self.pp_decimals != 2 { flags.push(format!("--pp-decimals {}", self.pp_decimals)); }
return flags.join(" ");
}
}
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum QuotaType { pub enum QuotaType {
Droop, Droop,
Hare, Hare,
@ -108,15 +129,37 @@ pub enum QuotaType {
HareExact, HareExact,
} }
impl QuotaType {
fn describe(self) -> String {
match self {
QuotaType::Droop => "--quota droop",
QuotaType::Hare => "--quota hare",
QuotaType::DroopExact => "--quota droop_exact",
QuotaType::HareExact => "--quota hare_exact",
}.to_string()
}
}
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum QuotaCriterion { pub enum QuotaCriterion {
GreaterOrEqual, GreaterOrEqual,
Greater, Greater,
} }
impl QuotaCriterion {
fn describe(self) -> String {
match self {
QuotaCriterion::GreaterOrEqual => "--quota-criterion geq",
QuotaCriterion::Greater => "--quota-criterion gt",
}.to_string()
}
}
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum SurplusMethod { pub enum SurplusMethod {
WIG, WIG,
UIG, UIG,
@ -124,21 +167,53 @@ pub enum SurplusMethod {
Meek, Meek,
} }
impl SurplusMethod {
fn describe(self) -> String {
match self {
SurplusMethod::WIG => "--surplus wig",
SurplusMethod::UIG => "--surplus uig",
SurplusMethod::EG => "--surplus eg",
SurplusMethod::Meek => "--surplus meek",
}.to_string()
}
}
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum SurplusOrder { pub enum SurplusOrder {
BySize, BySize,
ByOrder, ByOrder,
} }
impl SurplusOrder {
fn describe(self) -> String {
match self {
SurplusOrder::BySize => "--surplus-order by_size",
SurplusOrder::ByOrder => "--surplus-order by_order",
}.to_string()
}
}
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum ExclusionMethod { pub enum ExclusionMethod {
SingleStage, SingleStage,
ByValue, ByValue,
ParcelsByOrder, ParcelsByOrder,
} }
impl ExclusionMethod {
fn describe(self) -> String {
match self {
ExclusionMethod::SingleStage => "--exclusion single_stage",
ExclusionMethod::ByValue => "--exclusion by_value",
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_value",
}.to_string()
}
}
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);

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::election::{Candidate, CandidateState, CountCard, CountState, CountStateOrRef, Election, StageResult}; use crate::election::{CandidateState, CountState, Election};
use crate::numbers::{NativeFloat64, Number, Rational}; use crate::numbers::{NativeFloat64, Number, Rational};
use crate::stv; use crate::stv;
@ -24,21 +24,6 @@ extern crate console_error_panic_hook;
use js_sys::Array; use js_sys::Array;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
// Logging
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
/// println! to console
macro_rules! cprintln {
($($t:tt)*) => (
#[allow(unused_unsafe)]
unsafe { log(&format_args!($($t)*).to_string()) }
)
}
// Helper macros for making functions // Helper macros for making functions
macro_rules! impl_type { macro_rules! impl_type {
@ -75,6 +60,12 @@ macro_rules! impl_type {
return init_results_table(&election.0); return init_results_table(&election.0);
} }
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn [<describe_count_$type>](filename: String, election: &[<Election$type>], opts: &stv::STVOptions) -> String {
return describe_count(filename, &election.0, opts);
}
#[wasm_bindgen] #[wasm_bindgen]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &stv::STVOptions) -> Array { pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &stv::STVOptions) -> Array {
@ -142,6 +133,14 @@ fn init_results_table<N: Number>(election: &Election<N>) -> String {
return result; return result;
} }
fn describe_count<N: Number>(filename: String, election: &Election<N>, opts: &stv::STVOptions) -> String {
let mut result = String::from("<p>Count computed by OpenTally (revision ");
result.push_str(crate::VERSION);
let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value });
result.push_str(&format!(r#"). Read {} ballots from &lsquo;{}&rsquo; for election &lsquo;{}&rsquo;. There are {} candidates for {} vacancies. Counting using options <span style="font-family: monospace;">{}</span>.</p>"#, total_ballots, filename, election.name, election.candidates.len(), election.seats, opts.describe::<N>()));
return result;
}
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions) -> Array { fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions) -> Array {
let result = Array::new(); let result = Array::new();
result.push(&format!(r#"<td>{}</td>"#, stage_num).into()); result.push(&format!(r#"<td>{}</td>"#, stage_num).into());