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-14 20:43:36 +10:00
#![ allow(rustdoc::private_intra_doc_links) ]
2021-06-27 22:09:34 +10:00
use crate ::constraints ::Constraints ;
2021-06-03 21:35:25 +10:00
use crate ::election ::{ CandidateState , CountState , Election } ;
2021-06-14 21:43:43 +10:00
use crate ::numbers ::{ Fixed , GuardedFixed , 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-16 13:00:54 +10:00
use std ::cmp ::max ;
2021-06-04 22:05:48 +10:00
// Init
2021-06-14 20:43:36 +10:00
/// Wrapper for [Fixed::set_dps]
2021-06-04 22:05:48 +10:00
#[ wasm_bindgen ]
pub fn fixed_set_dps ( dps : usize ) {
Fixed ::set_dps ( dps ) ;
}
2021-06-14 21:43:43 +10:00
/// Wrapper for [GuardedFixed::set_dps]
#[ wasm_bindgen ]
pub fn gfixed_set_dps ( dps : usize ) {
GuardedFixed ::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
2021-06-14 20:43:36 +10:00
/// Wrapper for [Election::from_blt]
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-14 20:43:36 +10:00
/// Wrapper for [Election::normalise_ballots]
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-06-27 22:09:34 +10:00
/// Call [Constraints::from_con] and set [Election::constraints]
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
pub fn [ < election_load_constraints_ $type > ] ( election : & mut [ < Election $type > ] , text : String ) {
election . 0. constraints = Some ( Constraints ::from_con ( text . lines ( ) . map ( | s | s . to_string ( ) ) . into_iter ( ) ) ) ;
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::count_init]
2021-05-30 23:00:28 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
2021-06-28 00:56:28 +10:00
pub fn [ < count_init_ $type > ] ( state : & mut [ < CountState $type > ] , opts : & STVOptions ) -> Result < bool , JsValue > {
match stv ::count_init ( & mut state . 0 , opts . as_static ( ) ) {
Ok ( v ) = > Ok ( v ) ,
Err ( e ) = > Err ( e . name ( ) . into ( ) ) ,
}
2021-05-30 23:00:28 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::count_one_stage]
2021-05-30 23:00:28 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
2021-06-13 00:39:49 +10:00
pub fn [ < count_one_stage_ $type > ] ( state : & mut [ < CountState $type > ] , opts : & STVOptions ) -> Result < bool , JsValue > {
2021-06-27 17:44:30 +10:00
match stv ::count_one_stage ::< [ < $type > ] > ( & mut state . 0 , & opts . 0 ) {
2021-06-12 02:09:26 +10:00
Ok ( v ) = > Ok ( v ) ,
2021-06-28 00:56:28 +10:00
Err ( e ) = > Err ( e . name ( ) . into ( ) ) ,
2021-06-12 02:09:26 +10:00
}
2021-05-30 23:00:28 +10:00
}
// Reporting
2021-06-14 20:43:36 +10:00
/// Wrapper for [init_results_table]
2021-05-30 23:00:28 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
2021-06-13 00:39:49 +10:00
pub fn [ < init_results_table_ $type > ] ( election : & [ < Election $type > ] , opts : & STVOptions ) -> String {
2021-06-13 00:15:14 +10:00
return init_results_table ( & election . 0 , & opts . 0 ) ;
2021-06-02 22:46:36 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [describe_count]
2021-06-03 21:35:25 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
2021-06-13 00:39:49 +10:00
pub fn [ < describe_count_ $type > ] ( filename : String , election : & [ < Election $type > ] , opts : & STVOptions ) -> String {
2021-06-13 00:15:14 +10:00
return describe_count ( filename , & election . 0 , & opts . 0 ) ;
2021-06-03 21:35:25 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [update_results_table]
2021-06-02 22:46:36 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
2021-06-13 00:39:49 +10:00
pub fn [ < update_results_table_ $type > ] ( stage_num : usize , state : & [ < CountState $type > ] , opts : & STVOptions ) -> Array {
2021-06-13 00:15:14 +10:00
return update_results_table ( stage_num , & state . 0 , & opts . 0 ) ;
2021-06-02 22:46:36 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [update_stage_comments]
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 ) ;
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [finalise_results_table]
2021-06-03 15:47:19 +10:00
#[ wasm_bindgen ]
#[ allow(non_snake_case) ]
pub fn [ < finalise_results_table_ $type > ] ( state : & [ < CountState $type > ] ) -> Array {
return finalise_results_table ( & state . 0 ) ;
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [final_result_summary]
2021-06-03 15:47:19 +10:00
#[ wasm_bindgen ]
2021-06-02 22:46:36 +10:00
#[ allow(non_snake_case) ]
2021-06-16 13:00:54 +10:00
pub fn [ < final_result_summary_ $type > ] ( state : & [ < CountState $type > ] , opts : & STVOptions ) -> String {
return final_result_summary ( & state . 0 , & opts . 0 ) ;
2021-06-03 15:47:19 +10:00
}
2021-05-30 23:00:28 +10:00
// Wrapper structs
2021-06-14 20:43:36 +10:00
/// Wrapper for [CountState]
2021-06-16 17:20:29 +10:00
///
/// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187).
///
2021-05-30 23:00:28 +10:00
#[ wasm_bindgen ]
pub struct [ < CountState $type > ] ( CountState < 'static , $type > ) ;
#[ wasm_bindgen ]
impl [ < CountState $type > ] {
2021-06-16 17:20:29 +10:00
/// Create a new [CountState] wrapper
2021-05-30 23:00:28 +10:00
pub fn new ( election : & [ < Election $type > ] ) -> Self {
return [ < CountState $type > ] ( CountState ::new ( election . as_static ( ) ) ) ;
}
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [Election]
2021-06-16 17:20:29 +10:00
///
/// This is required as `&'static` cannot be specified in wasm-bindgen: see [issue 1187](https://github.com/rustwasm/wasm-bindgen/issues/1187).
///
2021-05-30 23:00:28 +10:00
#[ wasm_bindgen ]
pub struct [ < Election $type > ] ( Election < $type > ) ;
#[ wasm_bindgen ]
impl [ < Election $type > ] {
2021-06-16 17:20:29 +10:00
/// Return [Election::seats]
2021-05-30 23:00:28 +10:00
pub fn seats ( & self ) -> usize { self . 0. seats }
2021-06-16 17:20:29 +10:00
/// Return the underlying [Election] as a `&'static Election`
///
/// # Safety
/// This assumes that the underlying [Election] is valid for the `'static` lifetime, as it would be if the [Election] were created from Javascript.
///
2021-05-30 23:00:28 +10:00
fn as_static ( & self ) -> & 'static Election < $type > {
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-06-14 21:43:43 +10:00
impl_type! ( GuardedFixed ) ;
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-14 20:43:36 +10:00
/// Wrapper for [stv::STVOptions]
2021-06-13 00:15:14 +10:00
#[ wasm_bindgen ]
2021-06-13 03:15:15 +10:00
pub struct STVOptions ( stv ::STVOptions ) ;
2021-06-13 00:39:49 +10:00
#[ wasm_bindgen ]
impl STVOptions {
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::STVOptions::new]
2021-06-13 00:39:49 +10:00
pub fn new (
round_tvs : Option < usize > ,
round_weights : Option < usize > ,
round_votes : Option < usize > ,
round_quota : Option < usize > ,
sum_surplus_transfers : & str ,
2021-06-27 21:57:24 +10:00
meek_surplus_tolerance : & str ,
2021-06-13 00:39:49 +10:00
normalise_ballots : bool ,
quota : & str ,
quota_criterion : & str ,
quota_mode : & str ,
ties : Array ,
2021-06-13 03:15:15 +10:00
random_seed : String ,
2021-06-13 00:39:49 +10:00
surplus : & str ,
surplus_order : & str ,
transferable_only : bool ,
exclusion : & str ,
2021-06-20 01:28:54 +10:00
meek_nz_exclusion : bool ,
2021-06-23 00:52:25 +10:00
early_bulk_elect : bool ,
2021-06-13 00:39:49 +10:00
bulk_exclude : bool ,
defer_surpluses : bool ,
2021-06-18 18:48:12 +10:00
meek_immediate_elect : bool ,
2021-06-27 22:24:25 +10:00
constraints_path : Option < String > ,
2021-06-27 21:57:24 +10:00
constraint_mode : & str ,
2021-06-13 00:39:49 +10:00
pp_decimals : usize ,
) -> Self {
Self ( stv ::STVOptions ::new (
round_tvs ,
round_weights ,
round_votes ,
round_quota ,
sum_surplus_transfers ,
2021-06-27 21:57:24 +10:00
meek_surplus_tolerance ,
2021-06-13 00:39:49 +10:00
normalise_ballots ,
quota ,
quota_criterion ,
quota_mode ,
& ties . iter ( ) . map ( | v | v . as_string ( ) . unwrap ( ) ) . collect ( ) ,
2021-06-13 03:15:15 +10:00
& Some ( random_seed ) ,
2021-06-13 00:39:49 +10:00
surplus ,
surplus_order ,
transferable_only ,
exclusion ,
2021-06-20 01:28:54 +10:00
meek_nz_exclusion ,
2021-06-23 00:52:25 +10:00
early_bulk_elect ,
2021-06-13 00:39:49 +10:00
bulk_exclude ,
defer_surpluses ,
2021-06-18 18:48:12 +10:00
meek_immediate_elect ,
2021-06-27 22:24:25 +10:00
constraints_path . as_deref ( ) ,
2021-06-27 21:57:24 +10:00
constraint_mode ,
2021-06-13 00:39:49 +10:00
pp_decimals ,
) )
}
2021-06-22 14:34:26 +10:00
/// Wrapper for [stv::STVOptions::validate]
pub fn validate ( & self ) {
self . 0. validate ( ) ;
}
2021-06-13 00:39:49 +10:00
}
2021-06-13 00:15:14 +10:00
2021-06-13 03:15:15 +10:00
impl STVOptions {
2021-06-14 20:43:36 +10:00
/// Return the underlying [stv::STVOptions] as a `&'static stv::STVOptions`
///
/// # Safety
2021-06-16 17:20:29 +10:00
/// This assumes that the underlying [stv::STVOptions] is valid for the `'static` lifetime, as it would be if the [stv::STVOptions] were created from Javascript.
2021-06-14 20:43:36 +10:00
///
2021-06-13 03:15:15 +10:00
fn as_static ( & self ) -> & 'static stv ::STVOptions {
unsafe {
let ptr = & self . 0 as * const stv ::STVOptions ;
& * ptr
}
}
}
2021-05-30 18:28:39 +10:00
// Reporting
2021-06-14 20:43:36 +10:00
/// Generate the lead-in description of the count in HTML
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-07-21 10:59:06 +10:00
#[ inline ]
fn should_show_vre ( opts : & stv ::STVOptions ) -> bool {
2021-07-21 13:43:16 +10:00
//return opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76 || opts.early_bulk_elect;
return opts . quota_mode = = stv ::QuotaMode ::ERS97 | | opts . quota_mode = = stv ::QuotaMode ::ERS76 ;
2021-07-21 10:59:06 +10:00
}
2021-06-14 20:43:36 +10:00
/// Generate the first column of the HTML results table
fn init_results_table < N : Number > ( election : & Election < N > , opts : & stv ::STVOptions ) -> String {
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>"# ) ;
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-07-21 10:59:06 +10:00
if should_show_vre ( opts ) {
2021-06-14 20:43:36 +10:00
result . push_str ( r # "<tr class="info transfers"><td>Vote required for election</td></tr>"# ) ;
}
return result ;
}
/// Generate subsequent columns of the HTML results table
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 ( ) ;
2021-06-22 15:23:46 +10:00
// Insert borders to left of new exclusions in Wright STV
let mut tdclasses1 = " " ;
let mut tdclasses2 = " " ;
if opts . exclusion = = stv ::ExclusionMethod ::Wright & & state . kind = = Some ( " Exclusion of " ) {
tdclasses1 = r # " class="blw""# ;
tdclasses2 = r # "blw "# ;
}
2021-07-23 00:04:43 +10:00
result . push ( & format! ( r ## "<td{0}><a href="#stage{1}">{1}</a></td>"## , tdclasses1 , stage_num ) . into ( ) ) ;
2021-06-22 15:23:46 +10:00
result . push ( & format! ( r # "<td{}>{}</td>"# , tdclasses1 , state . kind . unwrap_or ( " " ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td{}>{}</td>"# , tdclasses1 , state . title ) . into ( ) ) ;
2021-06-02 22:46:36 +10:00
for candidate in state . election . candidates . iter ( ) {
2021-06-29 15:31:38 +10:00
let count_card = & state . candidates [ candidate ] ;
2021-06-27 22:09:34 +10:00
match count_card . state {
CandidateState ::Hopeful | CandidateState ::Guarded = > {
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , tdclasses2 , pp ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , tdclasses2 , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
}
CandidateState ::Elected = > {
result . push ( & format! ( r # "<td class="{}count elected">{}</td>"# , tdclasses2 , pp ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="{}count elected">{}</td>"# , tdclasses2 , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
}
CandidateState ::Doomed = > {
result . push ( & format! ( r # "<td class="{}count excluded">{}</td>"# , tdclasses2 , pp ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-06-22 15:23:46 +10:00
result . push ( & format! ( r # "<td class="{}count excluded">{}</td>"# , tdclasses2 , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-06-03 15:47:19 +10:00
}
2021-06-27 22:09:34 +10:00
CandidateState ::Withdrawn = > {
result . push ( & format! ( r # "<td class="{}count excluded"></td>"# , tdclasses2 ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="{}count excluded">WD</td>"# , tdclasses2 ) . into ( ) ) ;
}
CandidateState ::Excluded = > {
result . push ( & format! ( r # "<td class="{}count excluded">{}</td>"# , tdclasses2 , pp ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-07-23 00:10:53 +10:00
if count_card . parcels . iter ( ) . all ( | p | p . votes . is_empty ( ) ) {
2021-06-27 22:09:34 +10:00
result . push ( & format! ( r # "<td class="{}count excluded">Ex</td>"# , tdclasses2 ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count excluded">{}</td>"# , tdclasses2 , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
}
}
2021-06-02 22:46:36 +10:00
}
}
2021-06-22 15:23:46 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , tdclasses2 , pp ( & state . exhausted . transfers , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , tdclasses2 , pp ( & state . exhausted . votes , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , tdclasses2 , pp ( & state . loss_fraction . transfers , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , tdclasses2 , 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-22 15:23:46 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , tdclasses2 , pp ( & total_vote , opts . pp_decimals ) ) . into ( ) ) ;
2021-06-02 22:46:36 +10:00
2021-06-22 15:23:46 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , tdclasses2 , pp ( state . quota . as_ref ( ) . unwrap ( ) , opts . pp_decimals ) ) . into ( ) ) ;
2021-07-21 10:59:06 +10:00
if should_show_vre ( opts ) {
2021-07-21 13:43:16 +10:00
if let Some ( vre ) = & state . vote_required_election {
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , tdclasses2 , pp ( vre , opts . pp_decimals ) ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count"></td>"# , tdclasses2 ) . into ( ) ) ;
}
2021-06-07 20:52:18 +10:00
}
2021-06-02 22:46:36 +10:00
return result ;
}
2021-06-14 20:43:36 +10:00
/// Get the comment for the current stage
2021-06-03 15:47:19 +10:00
fn update_stage_comments < N : Number > ( state : & CountState < N > ) -> String {
return state . logger . render ( ) . join ( " " ) ;
}
2021-06-14 20:43:36 +10:00
/// Generate the final column of the HTML results table
2021-06-03 15:47:19 +10:00
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 ( ) {
2021-06-29 15:31:38 +10:00
let count_card = & state . candidates [ candidate ] ;
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-14 20:43:36 +10:00
/// Generate the final lead-out text summarising the result of the election
2021-06-16 13:00:54 +10:00
fn final_result_summary < N : Number > ( state : & CountState < N > , opts : & stv ::STVOptions ) -> String {
2021-06-03 15:47:19 +10:00
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-16 13:00:54 +10:00
winners . push ( ( candidate , count_card . order_elected , & count_card . keep_value ) ) ;
2021-06-03 15:47:19 +10:00
}
}
2021-06-29 15:31:38 +10:00
winners . sort_unstable_by ( | a , b | a . 1. cmp ( & b . 1 ) ) ;
2021-05-30 18:28:39 +10:00
2021-06-16 13:00:54 +10:00
for ( winner , _ , kv_opt ) in winners . into_iter ( ) {
if let Some ( kv ) = kv_opt {
result . push_str ( & format! ( " <li> {} (<i>kv</i> = {:.dps2$} )</li> " , winner . name , kv , dps2 = max ( opts . pp_decimals , 2 ) ) ) ;
} else {
result . push_str ( & format! ( " <li> {} </li> " , winner . name ) ) ;
}
2021-06-03 15:47:19 +10:00
}
2021-05-30 18:28:39 +10:00
2021-06-03 15:47:19 +10:00
result . push_str ( " </ol> " ) ;
return result ;
}
2021-06-14 20:43:36 +10:00
/// HTML pretty-print the number to the specified decimal places
2021-06-03 15:47:19 +10:00
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 ;
}