2021-05-30 18:28:39 +10:00
/* OpenTally: Open-source election vote counting
2022-03-14 22:27:58 +11:00
* Copyright © 2021 – 2022 Lee Yingtong Li ( RunasSudo )
2021-05-30 18:28:39 +10:00
*
* 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-07-31 15:24:23 +10:00
#![ allow(unused_unsafe) ] // Confuses cargo check
2021-06-14 20:43:36 +10:00
2022-04-16 02:27:59 +10:00
use crate ::constraints ::{ self , Constraints } ;
2021-09-06 02:43:33 +10:00
use crate ::election ::{ CandidateState , CountState , Election , StageKind } ;
2021-07-14 16:36:37 +10:00
//use crate::numbers::{DynNum, Fixed, GuardedFixed, NativeFloat64, Number, NumKind, Rational};
2021-06-14 21:43:43 +10:00
use crate ::numbers ::{ Fixed , GuardedFixed , NativeFloat64 , Number , Rational } ;
2021-08-20 02:16:54 +10:00
use crate ::parser ::blt ;
2021-05-30 18:28:39 +10:00
use crate ::stv ;
2021-08-05 01:12:53 +10:00
use crate ::ties ;
2021-05-30 18:28:39 +10:00
extern crate console_error_panic_hook ;
2021-09-06 02:43:33 +10:00
use itertools ::Itertools ;
2021-06-02 22:46:36 +10:00
use js_sys ::Array ;
2021-07-31 15:24:23 +10:00
use wasm_bindgen ::prelude ::wasm_bindgen ;
2021-05-30 18:28:39 +10:00
2021-06-16 13:00:54 +10:00
use std ::cmp ::max ;
2021-07-31 15:24:23 +10:00
// Error handling
#[ wasm_bindgen ]
extern " C " {
fn wasm_error ( message : String ) ;
}
macro_rules ! wasm_error {
( $type :expr , $err :expr ) = > { {
unsafe { wasm_error ( format! ( " {} : {} " , $type , $err ) ) ; }
panic! ( " {} : {} " , $type , $err ) ;
} }
}
2021-06-04 22:05:48 +10:00
// Init
2021-07-14 16:36:37 +10:00
/// Wrapper for [DynNum::set_kind]
//#[wasm_bindgen]
//pub fn dynnum_set_kind(kind: NumKind) {
// DynNum::set_kind(kind);
//}
2021-06-14 20:43:36 +10:00
/// Wrapper for [Fixed::set_dps]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-04 22:05:48 +10:00
pub fn fixed_set_dps ( dps : usize ) {
Fixed ::set_dps ( dps ) ;
}
2021-06-14 21:43:43 +10:00
/// Wrapper for [GuardedFixed::set_dps]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-14 21:43:43 +10:00
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-08-20 02:16:54 +10:00
/// Wrapper for [blt::parse_iterator]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
#[ allow(non_snake_case) ]
pub fn [ < election_from_blt_ $type > ] ( text : String ) -> [ < Election $type > ] {
// Install panic! hook
console_error_panic_hook ::set_once ( ) ;
2021-08-20 02:16:54 +10:00
let election : Election < $type > = match blt ::parse_iterator ( text . chars ( ) . peekable ( ) ) {
2021-07-31 15:24:23 +10:00
Ok ( e ) = > e ,
Err ( err ) = > wasm_error! ( " Syntax Error " , err ) ,
} ;
2021-05-30 23:00:28 +10:00
return [ < Election $type > ] ( election ) ;
}
2021-09-04 22:46:29 +10:00
/// Call [Constraints::from_con] and set [Election::constraints]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-09-03 23:53:15 +10:00
#[ allow(non_snake_case) ]
2022-04-16 02:27:59 +10:00
pub fn [ < election_load_constraints_ $type > ] ( election : & mut [ < Election $type > ] , text : String , opts : & STVOptions ) {
2021-10-29 23:07:38 +11:00
election . 0. constraints = match Constraints ::from_con ( text . lines ( ) ) {
Ok ( c ) = > Some ( c ) ,
Err ( err ) = > wasm_error! ( " Constraint Syntax Error " , err ) ,
} ;
// Validate constraints
2022-04-16 02:27:59 +10:00
if let Err ( err ) = election . 0. constraints . as_ref ( ) . unwrap ( ) . validate_constraints ( election . 0. candidates . len ( ) , opts . 0. constraint_mode ) {
2021-10-29 23:07:38 +11:00
wasm_error! ( " Constraint Validation Error " , err ) ;
}
2022-04-16 02:27:59 +10:00
// Add dummy candidates if required
if opts . 0. constraint_mode = = stv ::ConstraintMode ::RepeatCount {
constraints ::init_repeat_count ( & mut election . 0 ) ;
}
2021-09-03 23:53:15 +10:00
}
2021-09-04 22:46:29 +10:00
/// Wrapper for [stv::preprocess_election]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-27 22:09:34 +10:00
#[ allow(non_snake_case) ]
2021-09-04 22:46:29 +10:00
pub fn [ < preprocess_election_ $type > ] ( election : & mut [ < Election $type > ] , opts : & STVOptions ) {
stv ::preprocess_election ( & mut election . 0 , & opts . 0 ) ;
2021-06-27 22:09:34 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::count_init]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
#[ allow(non_snake_case) ]
2021-08-05 20:18:10 +10:00
pub fn [ < count_init_ $type > ] ( state : & mut [ < CountState $type > ] , opts : & STVOptions ) {
2021-06-28 00:56:28 +10:00
match stv ::count_init ( & mut state . 0 , opts . as_static ( ) ) {
2021-08-05 20:18:10 +10:00
Ok ( _ ) = > ( ) ,
2021-07-31 15:24:23 +10:00
Err ( err ) = > wasm_error! ( " Error " , err ) ,
2021-06-28 00:56:28 +10:00
}
2021-05-30 23:00:28 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [stv::count_one_stage]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
#[ allow(non_snake_case) ]
2021-07-31 15:24:23 +10:00
pub fn [ < count_one_stage_ $type > ] ( state : & mut [ < CountState $type > ] , opts : & STVOptions ) -> bool {
2021-06-27 17:44:30 +10:00
match stv ::count_one_stage ::< [ < $type > ] > ( & mut state . 0 , & opts . 0 ) {
2021-07-31 15:24:23 +10:00
Ok ( v ) = > v ,
Err ( err ) = > wasm_error! ( " Error " , err ) ,
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-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
#[ allow(non_snake_case) ]
2021-08-16 18:48:49 +10:00
pub fn [ < init_results_table_ $type > ] ( election : & [ < Election $type > ] , opts : & STVOptions , report_style : & str ) -> String {
return init_results_table ( & election . 0 , & opts . 0 , report_style ) ;
2021-06-02 22:46:36 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [describe_count]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-03 21:35:25 +10:00
#[ 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-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-02 22:46:36 +10:00
#[ allow(non_snake_case) ]
2021-08-16 18:48:49 +10:00
pub fn [ < update_results_table_ $type > ] ( stage_num : usize , state : & [ < CountState $type > ] , opts : & STVOptions , report_style : & str ) -> Array {
return update_results_table ( stage_num , & state . 0 , & opts . 0 , report_style ) ;
2021-06-02 22:46:36 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [update_stage_comments]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-03 15:47:19 +10:00
#[ allow(non_snake_case) ]
2021-09-11 21:08:36 +10:00
pub fn [ < update_stage_comments_ $type > ] ( state : & [ < CountState $type > ] , stage_num : usize ) -> String {
return update_stage_comments ( & state . 0 , stage_num ) ;
2021-06-03 15:47:19 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [finalise_results_table]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-03 15:47:19 +10:00
#[ allow(non_snake_case) ]
2021-08-16 18:48:49 +10:00
pub fn [ < finalise_results_table_ $type > ] ( state : & [ < CountState $type > ] , report_style : & str ) -> Array {
return finalise_results_table ( & state . 0 , report_style ) ;
2021-06-03 15:47:19 +10:00
}
2021-06-14 20:43:36 +10:00
/// Wrapper for [final_result_summary]
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , 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-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
pub struct [ < CountState $type > ] ( CountState < 'static , $type > ) ;
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
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-09-11 21:08:36 +10:00
/// Call [render_html](crate::stv::transfers::TransferTable::render_html) on [CountState::transfer_table]
pub fn transfer_table_render_html ( & self , opts : & STVOptions ) -> Option < String > {
2021-10-27 19:52:51 +11:00
return self . 0. transfer_table . as_ref ( ) . map ( | tt | tt . render_text ( & opts . 0 ) ) ;
2021-09-11 21:08:36 +10:00
}
2021-05-30 23:00:28 +10:00
}
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-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
pub struct [ < Election $type > ] ( Election < $type > ) ;
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-05-30 23:00:28 +10:00
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-07-14 16:36:37 +10:00
//impl_type!(DynNum);
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-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-13 03:15:15 +10:00
pub struct STVOptions ( stv ::STVOptions ) ;
2021-06-13 00:39:49 +10:00
2021-10-18 18:06:42 +11:00
#[ cfg_attr(feature = " wasm " , wasm_bindgen) ]
2021-06-13 00:39:49 +10:00
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 (
2021-08-03 16:46:21 +10:00
round_surplus_fractions : Option < usize > ,
round_values : Option < usize > ,
2021-06-13 00:39:49 +10:00
round_votes : Option < usize > ,
round_quota : Option < usize > ,
2022-03-23 00:34:43 +11:00
round_subtransfers : & str ,
2021-08-05 01:12:53 +10:00
meek_surplus_tolerance : String ,
2021-06-13 00:39:49 +10:00
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 ,
2022-03-25 02:46:30 +11:00
papers : & str ,
2021-06-13 00:39:49 +10:00
exclusion : & str ,
2021-06-20 01:28:54 +10:00
meek_nz_exclusion : bool ,
2021-08-05 18:41:39 +10:00
sample : & str ,
2021-08-04 13:46:32 +10:00
sample_per_ballot : 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-08-07 18:51:48 +10:00
immediate_elect : bool ,
2021-08-03 23:22:52 +10:00
min_threshold : String ,
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 (
2021-08-03 16:46:21 +10:00
round_surplus_fractions ,
round_values ,
2021-06-13 00:39:49 +10:00
round_votes ,
round_quota ,
2022-03-23 00:34:43 +11:00
round_subtransfers . into ( ) ,
2021-06-27 21:57:24 +10:00
meek_surplus_tolerance ,
2021-08-05 01:12:53 +10:00
quota . into ( ) ,
quota_criterion . into ( ) ,
quota_mode . into ( ) ,
ties ::from_strs ( ties . iter ( ) . map ( | v | v . as_string ( ) . unwrap ( ) ) . collect ( ) , Some ( random_seed ) ) ,
surplus . into ( ) ,
surplus_order . into ( ) ,
2022-03-25 02:46:30 +11:00
if papers = = " transferable " | | papers = = " subtract_nontransferable " { true } else { false } ,
if papers = = " subtract_nontransferable " { true } else { false } ,
2021-08-05 01:12:53 +10:00
exclusion . into ( ) ,
2021-06-20 01:28:54 +10:00
meek_nz_exclusion ,
2021-08-05 18:41:39 +10:00
sample . into ( ) ,
2021-08-04 13:46:32 +10:00
sample_per_ballot ,
2021-06-23 00:52:25 +10:00
early_bulk_elect ,
2021-06-13 00:39:49 +10:00
bulk_exclude ,
defer_surpluses ,
2021-08-07 18:51:48 +10:00
immediate_elect ,
2021-08-03 23:22:52 +10:00
min_threshold ,
2021-08-05 01:12:53 +10:00
constraints_path ,
constraint_mode . into ( ) ,
2021-07-31 17:41:28 +10:00
false ,
false ,
2021-09-11 18:42:15 +10:00
false ,
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 ) {
2021-07-31 15:24:23 +10:00
match self . 0. validate ( ) {
Ok ( _ ) = > { }
Err ( err ) = > { wasm_error! ( " Error " , err ) }
}
2021-06-22 14:34:26 +10:00
}
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-09-26 02:27:37 +10:00
pub fn describe_count < N : Number > ( filename : String , election : & Election < N > , opts : & stv ::STVOptions ) -> String {
2021-06-03 21:35:25 +10:00
let mut result = String ::from ( " <p>Count computed by OpenTally (revision " ) ;
result . push_str ( crate ::VERSION ) ;
2022-08-20 22:33:48 +10:00
let total_ballots = election . ballots . iter ( ) . fold ( N ::new ( ) , | mut acc , b | { acc + = & b . orig_value ; acc } ) ;
2022-04-16 02:27:59 +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 . iter ( ) . filter ( | c | ! c . is_dummy ) . count ( ) , election . seats ) ) ;
2021-06-12 16:03:31 +10:00
let opts_str = opts . describe ::< N > ( ) ;
2021-10-27 19:52:51 +11:00
if ! opts_str . is_empty ( ) {
2021-06-12 16:03:31 +10:00
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-14 20:43:36 +10:00
/// Generate the first column of the HTML results table
2021-09-26 02:27:37 +10:00
pub fn init_results_table < N : Number > ( election : & Election < N > , opts : & stv ::STVOptions , report_style : & str ) -> String {
2021-06-14 20:43:36 +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-08-22 17:53:55 +10:00
if report_style = = " ballots_votes " {
2021-10-27 19:52:51 +11:00
result . push_str ( r # "<tr class="hint-papers-votes"><td></td></tr>"# ) ;
2021-08-16 18:48:49 +10:00
}
2021-08-22 17:53:55 +10:00
2021-06-14 20:43:36 +10:00
for candidate in election . candidates . iter ( ) {
2022-04-16 02:27:59 +10:00
if candidate . is_dummy {
continue ;
}
2021-08-22 17:53:55 +10:00
if report_style = = " votes_transposed " {
2022-03-14 22:27:58 +11:00
result . push_str ( & format! ( r # "<tr class="candidate transfers"><td class="candidate-name">{}</td></tr>"# , candidate . name ) ) ;
2021-08-22 17:53:55 +10:00
} else {
2022-03-14 22:27:58 +11:00
result . push_str ( & format! ( r # "<tr class="candidate transfers"><td rowspan="2" class="candidate-name">{}</td></tr><tr class="candidate votes"></tr>"# , candidate . name ) ) ;
2021-08-22 17:53:55 +10:00
}
}
if report_style = = " votes_transposed " {
result . push_str ( r # "<tr class="info transfers"><td>Exhausted</td></tr>"# ) ;
} else {
result . push_str ( r # "<tr class="info transfers"><td rowspan="2">Exhausted</td></tr><tr class="info votes"></tr>"# ) ;
}
if report_style = = " votes_transposed " {
result . push_str ( r # "<tr class="info transfers"><td>Loss by fraction</td></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"# ) ;
} else {
result . push_str ( r # "<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-14 20:43:36 +10:00
}
2021-08-22 17:53:55 +10:00
2021-09-10 02:41:40 +10:00
if stv ::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>"# ) ;
}
2021-08-22 17:53:55 +10:00
2021-06-14 20:43:36 +10:00
return result ;
}
/// Generate subsequent columns of the HTML results table
2021-09-26 02:27:37 +10:00
pub fn update_results_table < N : Number > ( stage_num : usize , state : & CountState < N > , opts : & stv ::STVOptions , report_style : & str ) -> Array {
2021-06-02 22:46:36 +10:00
let result = Array ::new ( ) ;
2021-06-22 15:23:46 +10:00
// Insert borders to left of new exclusions in Wright STV
2021-08-22 17:53:55 +10:00
let classes_o ; // Outer version
let classes_i ; // Inner version
2022-04-16 02:27:59 +10:00
if ( opts . exclusion = = stv ::ExclusionMethod ::Wright & & matches! ( state . title , StageKind ::ExclusionOf ( _ ) ) ) | | matches! ( state . title , StageKind ::Rollback ) {
2021-08-22 17:53:55 +10:00
classes_o = r # " class="blw""# ;
classes_i = r # "blw "# ;
} else {
classes_o = " " ;
classes_i = " " ;
}
// Hide transfers column for first preferences if transposed
let hide_xfers_trsp ;
2021-09-06 02:43:33 +10:00
if let StageKind ::FirstPreferences = state . title {
hide_xfers_trsp = true ;
2021-10-27 19:52:51 +11:00
} else if opts . exclusion = = stv ::ExclusionMethod ::Wright & & matches! ( state . title , StageKind ::ExclusionOf ( _ ) ) {
2021-08-22 17:53:55 +10:00
hide_xfers_trsp = true ;
2022-04-16 02:27:59 +10:00
} else if let StageKind ::Rollback = state . title {
hide_xfers_trsp = true ;
} else if let StageKind ::BulkElection = state . title {
hide_xfers_trsp = true ;
2022-04-21 21:56:21 +10:00
} else if state . candidates . values ( ) . all ( | cc | cc . transfers . is_zero ( ) ) & & state . exhausted . transfers . is_zero ( ) & & state . loss_fraction . transfers . is_zero ( ) {
hide_xfers_trsp = true ;
2021-08-22 17:53:55 +10:00
} else {
hide_xfers_trsp = false ;
2021-06-22 15:23:46 +10:00
}
2021-08-16 18:48:49 +10:00
// Header rows
2021-09-06 02:43:33 +10:00
let kind_str = state . title . kind_as_string ( ) ;
let title_str ;
match & state . title {
2022-04-16 02:27:59 +10:00
StageKind ::FirstPreferences | StageKind ::Rollback | StageKind ::RollbackExhausted | StageKind ::SurplusesDistributed | StageKind ::BulkElection = > {
2021-09-06 02:43:33 +10:00
title_str = format! ( " {} " , state . title ) ;
}
StageKind ::SurplusOf ( candidate ) = > {
title_str = candidate . name . clone ( ) ;
}
StageKind ::ExclusionOf ( candidates ) = > {
if candidates . len ( ) > 5 {
let first_4_cands = candidates . iter ( ) . map ( | c | & c . name ) . sorted ( ) . take ( 4 ) . join ( " ,<br> " ) ;
title_str = format! ( " {} ,<br>and {} others " , first_4_cands , candidates . len ( ) - 4 ) ;
} else {
title_str = candidates . iter ( ) . map ( | c | & c . name ) . join ( " ,<br> " ) ;
}
}
2022-04-16 02:27:59 +10:00
StageKind ::BallotsOf ( candidate ) = > {
title_str = candidate . name . clone ( ) ;
}
2021-09-06 02:43:33 +10:00
} ;
2021-08-16 18:48:49 +10:00
match report_style {
" votes " = > {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r ## "<td{0}><a href="#stage{1}">{1}</a></td>"## , classes_o , stage_num ) . into ( ) ) ;
2021-09-06 02:43:33 +10:00
result . push ( & format! ( r # "<td{}>{}</td>"# , classes_o , kind_str ) . into ( ) ) ;
result . push ( & format! ( r # "<td{}>{}</td>"# , classes_o , title_str ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
}
" votes_transposed " = > {
if hide_xfers_trsp {
result . push ( & format! ( r ## "<td{0}><a href="#stage{1}">{1}</a></td>"## , classes_o , stage_num ) . into ( ) ) ;
2021-09-06 02:43:33 +10:00
result . push ( & format! ( r # "<td{}>{}</td>"# , classes_o , kind_str ) . into ( ) ) ;
result . push ( & format! ( r # "<td{}>{}</td>"# , classes_o , title_str ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
} else {
result . push ( & format! ( r ## "<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"## , classes_o , stage_num ) . into ( ) ) ;
2021-09-06 02:43:33 +10:00
result . push ( & format! ( r # "<td{} colspan="2">{}</td>"# , classes_o , kind_str ) . into ( ) ) ;
result . push ( & format! ( r # "<td{} colspan="2">{}</td>"# , classes_o , title_str ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
//result.push(&format!(r#"<td{}>X'fers</td><td>Total</td>"#, tdclasses1).into());
}
2021-08-16 18:48:49 +10:00
}
" ballots_votes " = > {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r ## "<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"## , classes_o , stage_num ) . into ( ) ) ;
2021-09-06 02:43:33 +10:00
result . push ( & format! ( r # "<td{} colspan="2">{}</td>"# , classes_o , kind_str ) . into ( ) ) ;
result . push ( & format! ( r # "<td{} colspan="2">{}</td>"# , classes_o , title_str ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td{}>Ballots</td><td>Votes</td>"# , classes_o ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
_ = > unreachable! ( " Invalid report_style " )
}
2021-06-02 22:46:36 +10:00
for candidate in state . election . candidates . iter ( ) {
2022-04-16 02:27:59 +10:00
if candidate . is_dummy {
continue ;
}
2021-06-29 15:31:38 +10:00
let count_card = & state . candidates [ candidate ] ;
2021-08-16 18:48:49 +10:00
2021-08-22 17:53:55 +10:00
// TODO: REFACTOR THIS!!
2021-08-16 18:48:49 +10:00
match report_style {
" votes " = > {
match count_card . state {
CandidateState ::Hopeful | CandidateState ::Guarded = > {
2021-09-04 22:24:01 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
CandidateState ::Elected = > {
2021-09-04 22:24:01 +10:00
result . push ( & format! ( r # "<td class="{}count elected">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count elected">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
CandidateState ::Doomed = > {
2021-09-04 22:24:01 +10:00
result . push ( & format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
CandidateState ::Withdrawn = > {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count excluded"></td>"# , classes_i ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="{}count excluded">WD</td>"# , classes_i ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
CandidateState ::Excluded = > {
2021-09-04 22:24:01 +10:00
result . push ( & format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
if count_card . votes . is_zero ( ) & & count_card . parcels . iter ( ) . all ( | p | p . votes . is_empty ( ) ) {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count excluded">Ex</td>"# , classes_i ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
} else {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
}
}
}
}
" votes_transposed " = > {
match count_card . state {
CandidateState ::Hopeful | CandidateState ::Guarded = > {
if hide_xfers_trsp {
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
}
}
CandidateState ::Elected = > {
if hide_xfers_trsp {
result . push ( & format! ( r # "<td class="{}count elected">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count elected">{}</td><td class="count elected">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
}
}
CandidateState ::Doomed = > {
if hide_xfers_trsp {
result . push ( & format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
}
}
CandidateState ::Withdrawn = > {
if hide_xfers_trsp {
result . push ( & format! ( r # "<td class="{}count excluded">WD</td>"# , classes_i ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count excluded"></td><td class="count excluded">WD</td>"# , classes_i ) . into ( ) ) ;
}
}
CandidateState ::Excluded = > {
if count_card . votes . is_zero ( ) & & count_card . parcels . iter ( ) . all ( | p | p . votes . is_empty ( ) ) {
if hide_xfers_trsp {
result . push ( & format! ( r # "<td class="{}count excluded">Ex</td>"# , classes_i ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">Ex</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
}
} else {
if hide_xfers_trsp {
result . push ( & format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
}
2021-08-16 18:48:49 +10:00
}
}
}
2021-06-27 22:09:34 +10:00
}
2021-08-16 18:48:49 +10:00
" ballots_votes " = > {
match count_card . state {
CandidateState ::Hopeful | CandidateState ::Guarded = > {
2021-09-04 22:24:01 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pps ( & count_card . ballot_transfers , 0 ) , pps ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pp ( & count_card . num_ballots ( ) , 0 ) , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
CandidateState ::Elected = > {
2021-09-04 22:24:01 +10:00
result . push ( & format! ( r # "<td class="{}count elected">{}</td><td class="count elected">{}</td>"# , classes_i , pps ( & count_card . ballot_transfers , 0 ) , pps ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count elected">{}</td><td class="count elected">{}</td>"# , classes_i , pp ( & count_card . num_ballots ( ) , 0 ) , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
CandidateState ::Doomed = > {
2021-09-04 22:24:01 +10:00
result . push ( & format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pps ( & count_card . ballot_transfers , 0 ) , pps ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pp ( & count_card . num_ballots ( ) , 0 ) , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
CandidateState ::Withdrawn = > {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count excluded"></td><td class="count excluded"></td>"# , classes_i ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="{}count excluded"></td><td class="count excluded">WD</td>"# , classes_i ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
CandidateState ::Excluded = > {
2021-09-04 22:24:01 +10:00
result . push ( & format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pps ( & count_card . ballot_transfers , 0 ) , pps ( & count_card . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
if count_card . votes . is_zero ( ) & & count_card . parcels . iter ( ) . all ( | p | p . votes . is_empty ( ) ) {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count excluded"></td><td class="count excluded">Ex</td>"# , classes_i ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
} else {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pp ( & count_card . num_ballots ( ) , 0 ) , pp ( & count_card . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
}
2021-06-27 22:09:34 +10:00
}
}
2021-08-16 18:48:49 +10:00
_ = > unreachable! ( " Invalid report_style " )
}
}
match report_style {
" votes " = > {
2021-09-05 00:13:29 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pps ( & state . exhausted . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & state . exhausted . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-09-05 00:13:29 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pps ( & state . loss_fraction . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & state . loss_fraction . votes , opts . pp_decimals ) ) . into ( ) ) ;
}
" votes_transposed " = > {
if hide_xfers_trsp {
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & state . exhausted . votes , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & state . loss_fraction . votes , opts . pp_decimals ) ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pps ( & state . exhausted . transfers , opts . pp_decimals ) , pp ( & state . exhausted . votes , opts . pp_decimals ) ) . into ( ) ) ;
result . push ( & format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pps ( & state . loss_fraction . transfers , opts . pp_decimals ) , pp ( & state . loss_fraction . votes , opts . pp_decimals ) ) . into ( ) ) ;
}
2021-06-02 22:46:36 +10:00
}
2021-08-16 18:48:49 +10:00
" ballots_votes " = > {
2021-09-05 00:13:29 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pps ( & state . exhausted . ballot_transfers , 0 ) , pps ( & state . exhausted . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pp ( & state . exhausted . num_ballots ( ) , 0 ) , pp ( & state . exhausted . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-09-05 00:13:29 +10:00
result . push ( & format! ( r # "<td class="{}count"></td><td class="count">{}</td>"# , classes_i , pps ( & state . loss_fraction . transfers , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count"></td><td class="count">{}</td>"# , classes_i , pp ( & state . loss_fraction . votes , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
_ = > unreachable! ( " Invalid report_style " )
2021-06-02 22:46:36 +10:00
}
// Calculate total votes
2022-08-20 22:33:48 +10:00
let mut total_vote = state . candidates . iter ( ) . filter_map ( | ( c , cc ) | if c . is_dummy { None } else { Some ( cc ) } ) . fold ( N ::new ( ) , | mut acc , cc | { acc + = & cc . votes ; acc } ) ;
2021-06-02 22:46:36 +10:00
total_vote + = & state . exhausted . votes ;
total_vote + = & state . loss_fraction . votes ;
2021-08-16 18:48:49 +10:00
match report_style {
" votes " = > {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & total_vote , opts . pp_decimals ) ) . into ( ) ) ;
}
" votes_transposed " = > {
if hide_xfers_trsp {
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & total_vote , opts . pp_decimals ) ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count"></td><td class="count">{}</td>"# , classes_i , pp ( & total_vote , opts . pp_decimals ) ) . into ( ) ) ;
}
2021-08-16 18:48:49 +10:00
}
" ballots_votes " = > {
// Calculate total ballots
2022-08-20 22:33:48 +10:00
let mut total_ballots = state . candidates . values ( ) . fold ( N ::new ( ) , | mut acc , cc | { acc + = cc . num_ballots ( ) ; acc } ) ;
2021-08-16 18:48:49 +10:00
total_ballots + = state . exhausted . num_ballots ( ) ;
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pp ( & total_ballots , 0 ) , pp ( & total_vote , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
_ = > unreachable! ( " Invalid report_style " )
}
2021-08-22 17:53:55 +10:00
if report_style = = " votes " | | ( report_style = = " votes_transposed " & & hide_xfers_trsp ) {
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( state . quota . as_ref ( ) . unwrap ( ) , opts . pp_decimals ) ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count"></td><td class="count">{}</td>"# , classes_i , pp ( state . quota . as_ref ( ) . unwrap ( ) , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
2021-09-10 02:41:40 +10:00
if stv ::should_show_vre ( opts ) {
2021-07-21 13:43:16 +10:00
if let Some ( vre ) = & state . vote_required_election {
2021-08-22 17:53:55 +10:00
if report_style = = " votes " | | ( report_style = = " votes_transposed " & & hide_xfers_trsp ) {
result . push ( & format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( vre , opts . pp_decimals ) ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count"></td><td class="count">{}</td>"# , classes_i , pp ( vre , opts . pp_decimals ) ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
2021-07-21 13:43:16 +10:00
} else {
2021-08-22 17:53:55 +10:00
if report_style = = " votes " | | ( report_style = = " votes_transposed " & & hide_xfers_trsp ) {
result . push ( & format! ( r # "<td class="{}count"></td>"# , classes_i ) . into ( ) ) ;
} else {
result . push ( & format! ( r # "<td class="{}count"></td><td class="count"></td>"# , classes_i ) . into ( ) ) ;
2021-08-16 18:48:49 +10:00
}
2021-07-21 13:43:16 +10:00
}
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-09-26 02:27:37 +10:00
pub fn update_stage_comments < N : Number > ( state : & CountState < N > , stage_num : usize ) -> String {
2021-09-11 21:08:36 +10:00
let mut comments = state . logger . render ( ) . join ( " " ) ;
2021-10-27 19:52:51 +11:00
if state . transfer_table . is_some ( ) {
2021-09-11 21:08:36 +10:00
comments . push_str ( & format! ( r ## " <a href="#" class="detailedTransfersLink" onclick="viewDetailedTransfers({});return false;">[View detailed transfers]</a>"## , stage_num ) ) ;
}
return comments ;
2021-06-03 15:47:19 +10:00
}
2021-06-14 20:43:36 +10:00
/// Generate the final column of the HTML results table
2021-09-26 02:27:37 +10:00
pub fn finalise_results_table < N : Number > ( state : & CountState < N > , report_style : & str ) -> Array {
2021-06-03 15:47:19 +10:00
let result = Array ::new ( ) ;
// Header rows
2021-08-16 18:48:49 +10:00
match report_style {
2021-08-22 17:53:55 +10:00
" votes " | " votes_transposed " = > {
2021-08-16 18:48:49 +10:00
result . push ( & r # "<td rowspan="3"></td>"# . into ( ) ) ;
result . push ( & " " . into ( ) ) ;
result . push ( & " " . into ( ) ) ;
}
" ballots_votes " = > {
result . push ( & r # "<td rowspan="4"></td>"# . into ( ) ) ;
result . push ( & " " . into ( ) ) ;
result . push ( & " " . into ( ) ) ;
result . push ( & " " . into ( ) ) ;
}
_ = > unreachable! ( " Invalid report_style " )
}
2021-06-03 15:47:19 +10:00
2021-08-22 17:53:55 +10:00
let rowspan = if report_style = = " votes_transposed " { " " } else { r # " rowspan="2""# } ;
2021-06-03 15:47:19 +10:00
// Candidate states
for candidate in state . election . candidates . iter ( ) {
2022-04-16 02:27:59 +10:00
if candidate . is_dummy {
continue ;
}
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-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td{} class="bb elected">ELECTED {}</td>"# , rowspan , count_card . order_elected ) . into ( ) ) ;
2021-06-12 00:50:01 +10:00
} else if count_card . state = = stv ::CandidateState ::Excluded {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td{} class="bb excluded">Excluded {}</td>"# , rowspan , - count_card . order_elected ) . into ( ) ) ;
2021-06-12 00:50:01 +10:00
} else if count_card . state = = stv ::CandidateState ::Withdrawn {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td{} class="bb excluded">Withdrawn</td>"# , rowspan ) . into ( ) ) ;
2021-05-30 18:28:39 +10:00
} else {
2021-08-22 17:53:55 +10:00
result . push ( & format! ( r # "<td{} class="bb"></td>"# , rowspan ) . into ( ) ) ;
}
if report_style ! = " votes_transposed " {
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-09-26 02:27:37 +10:00
pub 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 ;
}
2021-08-22 17:53:55 +10:00
/// Signed version of [pp]
fn pps < N : Number > ( n : & N , dps : usize ) -> String {
if n . is_zero ( ) {
return " " . to_string ( ) ;
}
let mut raw = format! ( " {:.dps$} " , n , dps = dps ) ;
if raw . contains ( '.' ) {
raw = raw . replacen ( " . " , " .<sup> " , 1 ) ;
raw . push_str ( " </sup> " ) ;
}
if raw . starts_with ( '-' ) {
raw = raw . replacen ( " - " , " − " , 1 ) ;
} else {
raw . insert ( 0 , '+' ) ;
}
return raw ;
}