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),
);
// 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
let state = wasm.CountStateRational.new(election);
wasm.count_init_Rational(state, opts);

View File

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

View File

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

View File

@ -19,3 +19,7 @@ pub mod election;
pub mod logger;
pub mod numbers;
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 clap::{AppSettings, Clap};
use git_version::git_version;
use std::fs::File;
use std::io::{self, BufRead};
use std::ops;
const VERSION: &str = git_version!(args=["--always", "--dirty=-dev"], fallback="unknown");
/// Open-source election vote counting
#[derive(Clap)]
#[clap(name="OpenTally", version=VERSION)]
#[clap(name="OpenTally", version=opentally::VERSION)]
struct Opts {
#[clap(subcommand)]
command: Command,

View File

@ -45,6 +45,8 @@ where
{
fn new() -> Self;
fn describe() -> String;
fn pow_assign(&mut self, exponent: i32);
fn floor_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 {
fn new() -> Self { Self(0.0) }
fn describe() -> String { "--numbers float64".to_string() }
fn pow_assign(&mut self, exponent: i32) {
self.0 = self.0.powi(exponent);
}

View File

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

View File

@ -30,6 +30,8 @@ pub struct Rational(rug::Rational);
impl Number for Rational {
fn new() -> Self { Self(rug::Rational::new()) }
fn describe() -> String { "--numbers rational".to_string() }
fn pow_assign(&mut self, exponent: i32) {
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]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum QuotaType {
Droop,
Hare,
@ -108,15 +129,37 @@ pub enum QuotaType {
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]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum QuotaCriterion {
GreaterOrEqual,
Greater,
}
impl QuotaCriterion {
fn describe(self) -> String {
match self {
QuotaCriterion::GreaterOrEqual => "--quota-criterion geq",
QuotaCriterion::Greater => "--quota-criterion gt",
}.to_string()
}
}
#[wasm_bindgen]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum SurplusMethod {
WIG,
UIG,
@ -124,21 +167,53 @@ pub enum SurplusMethod {
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]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum SurplusOrder {
BySize,
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]
#[derive(Clone, Copy)]
#[derive(PartialEq)]
pub enum ExclusionMethod {
SingleStage,
ByValue,
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) {
distribute_first_preferences(&mut state);
calculate_quota(&mut state, opts);

View File

@ -15,7 +15,7 @@
* 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::stv;
@ -24,21 +24,6 @@ extern crate console_error_panic_hook;
use js_sys::Array;
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
macro_rules! impl_type {
@ -75,6 +60,12 @@ macro_rules! impl_type {
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]
#[allow(non_snake_case)]
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;
}
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 {
let result = Array::new();
result.push(&format!(r#"<td>{}</td>"#, stage_num).into());