2021-05-30 18:28:39 +10:00
/* 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/>.
* /
2021-06-03 21:35:25 +10:00
use crate ::election ::{ CandidateState , CountState , Election } ;
2021-06-04 22:05:48 +10:00
use crate ::numbers ::{ Fixed , NativeFloat64 , Number , Rational } ;
2021-05-30 18:28:39 +10:00
use crate ::stv ;
extern crate console_error_panic_hook ;
2021-06-02 22:46:36 +10:00
use js_sys ::Array ;
2021-06-12 02:09:26 +10:00
use wasm_bindgen ::{ JsValue , prelude ::wasm_bindgen } ;
2021-05-30 18:28:39 +10:00
2021-06-04 22:05:48 +10:00
// Init
#[ wasm_bindgen ]
pub fn fixed_set_dps ( dps : usize ) {
Fixed ::set_dps ( dps ) ;
}
2021-05-30 23:00:28 +10:00
// Helper macros for making functions
macro_rules ! impl_type {
( $type :ident ) = > { paste ::item! {
2021-06-04 22:05:48 +10:00
// Counting
2021-05-30 23:00:28 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
pub fn [ < election_from_blt_ $type > ] ( text : String ) -> [ < Election $type > ] {
// Install panic! hook
console_error_panic_hook ::set_once ( ) ;
let election : Election < $type > = Election ::from_blt ( text . lines ( ) . map ( | s | s . to_string ( ) ) . into_iter ( ) ) ;
return [ < Election $type > ] ( election ) ;
}
2021-06-11 21:23:08 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
pub fn [ < election_normalise_ballots_ $type > ] ( election : & mut [ < Election $type > ] ) {
election . 0. normalise_ballots ( ) ;
}
2021-05-30 23:00:28 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
2021-06-13 00:15:14 +10:00
pub fn [ < count_init_ $type > ] ( state : & mut [ < CountState $type > ] , opts : & STVOptionsWrapper ) {
stv ::count_init ( & mut state . 0 , & opts . 0 ) ;
2021-05-30 23:00:28 +10:00
}
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
2021-06-13 00:15:14 +10:00
pub fn [ < count_one_stage_ $type > ] ( state : & mut [ < CountState $type > ] , opts : & STVOptionsWrapper ) -> Result < bool , JsValue > {
match stv ::count_one_stage ( & mut state . 0 , & opts . 0 ) {
2021-06-12 02:09:26 +10:00
Ok ( v ) = > Ok ( v ) ,
Err ( stv ::STVError ::RequireInput ) = > Err ( " RequireInput " . into ( ) ) ,
2021-06-13 00:15:14 +10:00
Err ( stv ::STVError ::UnresolvedTie ) = > Err ( " UnresolvedTie " . into ( ) ) ,
2021-06-12 02:09:26 +10:00
}
2021-05-30 23:00:28 +10:00
}
// Reporting
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
2021-06-13 00:15:14 +10:00
pub fn [ < init_results_table_ $type > ] ( election : & [ < Election $type > ] , opts : & STVOptionsWrapper ) -> String {
return init_results_table ( & election . 0 , & opts . 0 ) ;
2021-06-02 22:46:36 +10:00
}
2021-06-03 21:35:25 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
2021-06-13 00:15:14 +10:00
pub fn [ < describe_count_ $type > ] ( filename : String , election : & [ < Election $type > ] , opts : & STVOptionsWrapper ) -> String {
return describe_count ( filename , & election . 0 , & opts . 0 ) ;
2021-06-03 21:35:25 +10:00
}
2021-06-02 22:46:36 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
2021-06-13 00:15:14 +10:00
pub fn [ < update_results_table_ $type > ] ( stage_num : usize , state : & [ < CountState $type > ] , opts : & STVOptionsWrapper ) -> Array {
return update_results_table ( stage_num , & state . 0 , & opts . 0 ) ;
2021-06-02 22:46:36 +10:00
}
2021-06-03 15:47:19 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
pub fn [ < update_stage_comments_ $type > ] ( state : & [ < CountState $type > ] ) -> String {
return update_stage_comments ( & state . 0 ) ;
}
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
pub fn [ < finalise_results_table_ $type > ] ( state : & [ < CountState $type > ] ) -> Array {
return finalise_results_table ( & state . 0 ) ;
}
#[ wasm_bindgen ]
2021-06-02 22:46:36 +10:00
#[ allow(non_snake_case) ]
2021-06-03 15:47:19 +10:00
pub fn [ < final_result_summary_ $type > ] ( state : & [ < CountState $type > ] ) -> String {
return final_result_summary ( & state . 0 ) ;
}
2021-05-30 23:00:28 +10:00
// Wrapper structs
// Required as we cannot specify &'static in wasm-bindgen: issue #1187
#[ wasm_bindgen ]
pub struct [ < CountState $type > ] ( CountState < 'static , $type > ) ;
#[ wasm_bindgen ]
impl [ < CountState $type > ] {
pub fn new ( election : & [ < Election $type > ] ) -> Self {
return [ < CountState $type > ] ( CountState ::new ( election . as_static ( ) ) ) ;
}
}
#[ wasm_bindgen ]
pub struct [ < Election $type > ] ( Election < $type > ) ;
#[ wasm_bindgen ]
impl [ < Election $type > ] {
pub fn seats ( & self ) -> usize { self . 0. seats }
fn as_static ( & self ) -> & 'static Election < $type > {
// Need to have this as we cannot specify &'static in wasm-bindgen: issue #1187
unsafe {
let ptr = & self . 0 as * const Election < $type > ;
& * ptr
}
}
}
} }
2021-05-30 18:28:39 +10:00
}
2021-06-04 22:05:48 +10:00
impl_type! ( Fixed ) ;
2021-05-30 23:00:28 +10:00
impl_type! ( NativeFloat64 ) ;
2021-06-04 22:05:48 +10:00
impl_type! ( Rational ) ;
2021-05-30 18:28:39 +10:00
2021-06-13 00:15:14 +10:00
#[ wasm_bindgen ]
pub struct STVOptionsWrapper ( stv ::STVOptions < 'static > ) ;
2021-05-30 18:28:39 +10:00
// Reporting
2021-06-07 20:52:18 +10:00
fn init_results_table < N : Number > ( election : & Election < N > , opts : & stv ::STVOptions ) -> String {
2021-06-03 15:47:19 +10:00
let mut result = String ::from ( r # "<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"# ) ;
2021-06-02 22:46:36 +10:00
for candidate in election . candidates . iter ( ) {
result . push_str ( & format! ( r # "<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"# , candidate . name ) ) ;
}
result . push_str ( r # "<tr class="info transfers"><td rowspan="2">Exhausted</td></tr><tr class="info votes"></tr><tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr><tr class="info votes"></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"# ) ;
2021-06-07 20:52:18 +10:00
if opts . quota_mode = = stv ::QuotaMode ::ERS97 {
result . push_str ( r # "<tr class="info transfers"><td>Vote required for election</td></tr>"# ) ;
}
2021-06-02 22:46:36 +10:00
return result ;
}
2021-06-03 21:35:25 +10:00
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 } ) ;
2021-06-12 16:03:31 +10:00
result . push_str ( & format! ( r # "). Read {:.0} ballots from ‘{}’ for election ‘{}’. There are {} candidates for {} vacancies. "# , total_ballots , filename , election . name , election . candidates . len ( ) , election . seats ) ) ;
let opts_str = opts . describe ::< N > ( ) ;
if opts_str . len ( ) > 0 {
result . push_str ( & format! ( r # "Counting using options <span style="font-family: monospace;">{}</span>.</p>"# , opts_str ) )
} else {
result . push_str ( r # "Counting using default options.</p>"# ) ;
}
2021-06-03 21:35:25 +10:00
return result ;
}
2021-06-02 22:46:36 +10:00
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 ( ) ) ;
result . push ( & format! ( r # "<td>{}</td>"# , state . kind . unwrap_or ( " " ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td>{}</td>"# , state . title ) . into ( ) ) ;
for candidate in state . election . candidates . iter ( ) {
let count_card = state . candidates . get ( candidate ) . unwrap ( ) ;
2021-06-12 00:50:01 +10:00
if count_card . state = = stv ::CandidateState ::Elected {
2021-06-03 15:47:19 +10:00
result . push ( & format! ( r # "<td class="count elected">{}</td>"# , pp ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="count elected">{}</td>"# , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-06-12 00:50:01 +10:00
} else if count_card . state = = stv ::CandidateState ::Excluded {
2021-06-03 15:47:19 +10:00
result . push ( & format! ( r # "<td class="count excluded">{}</td>"# , pp ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
if count_card . votes . is_zero ( ) {
result . push ( & r # "<td class="count excluded">Ex</td>"# . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="count excluded">{}</td>"# , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
}
2021-06-12 00:50:01 +10:00
} else if count_card . state = = stv ::CandidateState ::Withdrawn {
result . push ( & r # "<td class="count excluded"></td>"# . into ( ) ) ;
result . push ( & r # "<td class="count excluded">WD</td>"# . into ( ) ) ;
2021-06-02 22:46:36 +10:00
} else {
2021-06-03 15:47:19 +10:00
result . push ( & format! ( r # "<td class="count">{}</td>"# , pp ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="count">{}</td>"# , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-06-02 22:46:36 +10:00
}
}
2021-06-03 15:47:19 +10:00
result . push ( & format! ( r # "<td class="count">{}</td>"# , pp ( & state . exhausted . transfers , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="count">{}</td>"# , pp ( & state . exhausted . votes , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="count">{}</td>"# , pp ( & state . loss_fraction . transfers , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="count">{}</td>"# , pp ( & state . loss_fraction . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-06-02 22:46:36 +10:00
// Calculate total votes
let mut total_vote = state . candidates . values ( ) . fold ( N ::zero ( ) , | acc , cc | { acc + & cc . votes } ) ;
total_vote + = & state . exhausted . votes ;
total_vote + = & state . loss_fraction . votes ;
2021-06-03 15:47:19 +10:00
result . push ( & format! ( r # "<td class="count">{}</td>"# , pp ( & total_vote , opts . pp_decimals ) ) . into ( ) ) ;
2021-06-02 22:46:36 +10:00
2021-06-07 20:52:18 +10:00
result . push ( & format! ( r # "<td class="count">{}</td>"# , pp ( state . quota . as_ref ( ) . unwrap ( ) , opts . pp_decimals ) ) . into ( ) ) ;
if opts . quota_mode = = stv ::QuotaMode ::ERS97 {
result . push ( & format! ( r # "<td class="count">{}</td>"# , pp ( state . vote_required_election . as_ref ( ) . unwrap ( ) , opts . pp_decimals ) ) . into ( ) ) ;
}
2021-06-02 22:46:36 +10:00
return result ;
}
2021-06-03 15:47:19 +10:00
fn update_stage_comments < N : Number > ( state : & CountState < N > ) -> String {
return state . logger . render ( ) . join ( " " ) ;
}
fn finalise_results_table < N : Number > ( state : & CountState < N > ) -> Array {
let result = Array ::new ( ) ;
// Header rows
result . push ( & r # "<td rowspan="3"></td>"# . into ( ) ) ;
result . push ( & " " . into ( ) ) ;
result . push ( & " " . into ( ) ) ;
// Candidate states
for candidate in state . election . candidates . iter ( ) {
let count_card = state . candidates . get ( candidate ) . unwrap ( ) ;
2021-06-12 00:50:01 +10:00
if count_card . state = = stv ::CandidateState ::Elected {
2021-06-03 15:47:19 +10:00
result . push ( & format! ( r # "<td rowspan="2" class="bb elected">ELECTED {}</td>"# , count_card . order_elected ) . into ( ) ) ;
2021-06-12 00:50:01 +10:00
} else if count_card . state = = stv ::CandidateState ::Excluded {
2021-06-03 15:47:19 +10:00
result . push ( & format! ( r # "<td rowspan="2" class="bb excluded">Excluded {}</td>"# , - count_card . order_elected ) . into ( ) ) ;
2021-06-12 00:50:01 +10:00
} else if count_card . state = = stv ::CandidateState ::Withdrawn {
result . push ( & r # "<td rowspan="2" class="bb excluded">Withdrawn</td>"# . into ( ) ) ;
2021-05-30 18:28:39 +10:00
} else {
2021-06-03 15:47:19 +10:00
result . push ( & r # "<td rowspan="2" class="bb"></td>"# . into ( ) ) ;
2021-05-30 18:28:39 +10:00
}
2021-06-03 15:47:19 +10:00
result . push ( & " " . into ( ) ) ;
2021-05-30 18:28:39 +10:00
}
2021-06-03 15:47:19 +10:00
return result ;
2021-05-30 18:28:39 +10:00
}
2021-06-03 15:47:19 +10:00
fn final_result_summary < N : Number > ( state : & CountState < N > ) -> String {
let mut result = String ::from ( " <p>Count complete. The winning candidates are, in order of election:</p><ol> " ) ;
2021-05-30 18:28:39 +10:00
2021-06-03 15:47:19 +10:00
let mut winners = Vec ::new ( ) ;
for ( candidate , count_card ) in state . candidates . iter ( ) {
2021-06-12 00:50:01 +10:00
if count_card . state = = CandidateState ::Elected {
2021-06-03 15:47:19 +10:00
winners . push ( ( candidate , count_card . order_elected ) ) ;
}
}
winners . sort_unstable_by ( | a , b | a . 1. partial_cmp ( & b . 1 ) . unwrap ( ) ) ;
2021-05-30 18:28:39 +10:00
2021-06-03 15:47:19 +10:00
for ( winner , _ ) in winners . into_iter ( ) {
result . push_str ( & format! ( " <li> {} </li> " , winner . name ) ) ;
}
2021-05-30 18:28:39 +10:00
2021-06-03 15:47:19 +10:00
result . push_str ( " </ol> " ) ;
return result ;
}
fn pp < N : Number > ( n : & N , dps : usize ) -> String {
if n . is_zero ( ) {
return " " . to_string ( ) ;
}
2021-05-30 18:28:39 +10:00
2021-06-03 15:47:19 +10:00
let mut raw = format! ( " {:.dps$} " , n , dps = dps ) ;
if raw . contains ( '.' ) {
raw = raw . replacen ( " . " , " .<sup> " , 1 ) ;
raw . push_str ( " </sup> " ) ;
}
2021-05-30 18:28:39 +10:00
2021-06-03 15:47:19 +10:00
if raw . starts_with ( '-' ) {
raw = raw . replacen ( " - " , " − " , 1 ) ;
}
2021-05-30 18:28:39 +10:00
2021-06-03 15:47:19 +10:00
return raw ;
}