2021-08-20 02:16:54 +10:00
/* OpenTally: Open-source election vote counting
2022-03-23 00:34:43 +11:00
* Copyright © 2021 – 2022 Lee Yingtong Li ( RunasSudo )
2021-08-20 02:16:54 +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/>.
* /
2022-04-16 02:27:59 +10:00
use crate ::constraints ::{ self , Constraints } ;
2021-09-09 04:07:18 +10:00
use crate ::election ::{ CandidateState , CountState , Election , StageKind } ;
2021-08-20 02:16:54 +10:00
use crate ::numbers ::{ Fixed , GuardedFixed , NativeFloat64 , Number , Rational } ;
2021-09-02 17:17:45 +10:00
use crate ::parser ::{ bin , blt } ;
2021-08-20 02:16:54 +10:00
use crate ::stv ::{ self , STVOptions } ;
use crate ::ties ;
use clap ::{ AppSettings , Clap } ;
2021-09-09 04:07:18 +10:00
use itertools ::Itertools ;
2021-08-20 02:16:54 +10:00
use std ::cmp ::max ;
use std ::fs ::File ;
use std ::io ::{ self , BufRead } ;
use std ::ops ;
/// Count a single transferable vote (STV) election
#[ derive(Clap) ]
#[ clap(setting=AppSettings::DeriveDisplayOrder) ]
pub struct SubcmdOptions {
// ----------------
// -- File input --
/// Path to the BLT file to be counted
2021-09-02 17:17:45 +10:00
#[ clap(help_heading=Some( " INPUT " )) ]
2021-08-20 02:16:54 +10:00
filename : String ,
2021-09-02 17:17:45 +10:00
/// Input is in serialised binary format from "opentally convert"
#[ clap(help_heading=Some( " INPUT " ), long) ]
bin : bool ,
2021-08-20 02:16:54 +10:00
// ----------------------
// -- Numbers settings --
/// Numbers mode
#[ clap(help_heading=Some( " NUMBERS " ), short, long, possible_values=& [ " rational " , " fixed " , " gfixed " , " float64 " ] , default_value= " rational " , value_name= " mode " ) ]
numbers : String ,
/// Decimal places if --numbers fixed
#[ clap(help_heading=Some( " NUMBERS " ), long, default_value= " 5 " , value_name= " dps " ) ]
decimals : usize ,
// -----------------------
// -- Rounding settings --
/// Round surplus fractions to specified decimal places
#[ clap(help_heading=Some( " ROUNDING " ), long, alias= " round-tvs " , value_name= " dps " ) ]
round_surplus_fractions : Option < usize > ,
/// Round ballot values to specified decimal places
#[ clap(help_heading=Some( " ROUNDING " ), long, alias= " round-weights " , value_name= " dps " ) ]
round_values : Option < usize > ,
/// Round votes to specified decimal places
#[ clap(help_heading=Some( " ROUNDING " ), long, value_name= " dps " ) ]
round_votes : Option < usize > ,
/// Round quota to specified decimal places
#[ clap(help_heading=Some( " ROUNDING " ), long, value_name= " dps " ) ]
round_quota : Option < usize > ,
2022-03-23 00:34:43 +11:00
/// (Gregory STV) How to round subtransfers during surpluses/exclusions
2022-03-25 03:04:00 +11:00
#[ clap(help_heading=Some( " ROUNDING " ), long, possible_values=& [ " single_step " , " by_value " , " by_value_and_source " , " by_parcel " , " per_ballot " ] , default_value= " single_step " , value_name= " mode " ) ]
2022-03-23 00:34:43 +11:00
round_subtransfers : String ,
2021-08-20 02:16:54 +10:00
/// (Meek STV) Limit for stopping iteration of surplus distribution
#[ clap(help_heading=Some( " ROUNDING " ), long, default_value= " 0.001% " , value_name= " tolerance " ) ]
meek_surplus_tolerance : String ,
// -----------
// -- Quota --
/// Quota type
#[ clap(help_heading=Some( " QUOTA " ), short, long, possible_values=& [ " droop " , " hare " , " droop_exact " , " hare_exact " ] , default_value= " droop " ) ]
quota : String ,
/// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota
#[ clap(help_heading=Some( " QUOTA " ), short='c', long, possible_values=& [ " geq " , " gt " ] , default_value= " gt " , value_name= " criterion " ) ]
quota_criterion : String ,
/// Whether to apply a form of progressive quota
#[ clap(help_heading=Some( " QUOTA " ), long, possible_values=& [ " static " , " ers97 " , " ers76 " , " dynamic_by_total " , " dynamic_by_active " ] , default_value= " static " , value_name= " mode " ) ]
quota_mode : String ,
// ------------------
// -- STV variants --
/// Tie-breaking method
#[ clap(help_heading=Some( " STV VARIANTS " ), short='t', long, possible_values=& [ " forwards " , " backwards " , " random " , " prompt " ] , default_value= " prompt " , value_name= " methods " ) ]
ties : Vec < String > ,
/// Random seed to use with --ties random
#[ clap(help_heading=Some( " STV VARIANTS " ), long, value_name= " seed " ) ]
random_seed : Option < String > ,
2021-09-27 19:02:30 +10:00
/// Method of surplus distributions [default: wig] [possible values: wig, uig, eg, meek, ihare, hare]
#[ clap(help_heading=Some( " STV VARIANTS " ), short='s', long, possible_values=& [ " wig " , " uig " , " eg " , " meek " , " ihare " , " hare " , " eh " ] , default_value= " wig " , value_name= " method " , hide_possible_values=true, hide_default_value=true) ]
2021-08-20 02:16:54 +10:00
surplus : String ,
/// (Gregory STV) Order to distribute surpluses
#[ clap(help_heading=Some( " STV VARIANTS " ), long, possible_values=& [ " by_size " , " by_order " ] , default_value= " by_size " , value_name= " order " ) ]
surplus_order : String ,
/// (Gregory STV) Examine only transferable papers during surplus distributions
#[ clap(help_heading=Some( " STV VARIANTS " ), long) ]
transferable_only : bool ,
2022-03-25 02:46:30 +11:00
/// (Gregory STV) If --transferable-only, calculate value of transferable papers by subtracting value of non-transferable papers
#[ clap(help_heading=Some( " STV VARIANTS " ), long) ]
subtract_nontransferable : bool ,
2021-08-20 02:16:54 +10:00
/// (Gregory STV) Method of exclusions
#[ clap(help_heading=Some( " STV VARIANTS " ), long, possible_values=& [ " single_stage " , " by_value " , " by_source " , " parcels_by_order " , " wright " ] , default_value= " single_stage " , value_name= " method " ) ]
exclusion : String ,
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
#[ clap(help_heading=Some( " STV VARIANTS " ), long) ]
meek_nz_exclusion : bool ,
2021-09-27 19:02:30 +10:00
/// (Hare) Method of drawing a sample [default: stratify] [possible values: stratify, by_order, cincinnati]
#[ clap(help_heading=Some( " STV VARIANTS " ), long, possible_values=& [ " stratify " , " stratify_lr " , " by_order " , " cincinnati " , " nth_ballot " ] , default_value= " stratify " , value_name= " method " , hide_possible_values=true, hide_default_value=true) ]
2021-08-20 02:16:54 +10:00
sample : String ,
2021-09-27 19:02:30 +10:00
/// (Hare) Sample-based methods: Check for candidate election after each individual ballot paper transfer
2021-08-20 02:16:54 +10:00
#[ clap(help_heading=Some( " STV VARIANTS " ), long) ]
sample_per_ballot : bool ,
// -------------------------
// -- Count optimisations --
/// Continue count even if continuing candidates fill all remaining vacancies
#[ clap(help_heading=Some( " COUNT OPTIMISATIONS " ), long) ]
no_early_bulk_elect : bool ,
/// Use bulk exclusion
#[ clap(help_heading=Some( " COUNT OPTIMISATIONS " ), long) ]
bulk_exclude : bool ,
/// Defer surplus distributions if possible
#[ clap(help_heading=Some( " COUNT OPTIMISATIONS " ), long) ]
defer_surpluses : bool ,
/// Elect candidates only when their surpluses are distributed; (Meek STV) Wait for keep values to converge before electing candidates
#[ clap(help_heading=Some( " COUNT OPTIMISATIONS " ), long) ]
no_immediate_elect : bool ,
/// On exclusion, exclude any candidate with fewer than this many votes
#[ clap(help_heading=Some( " COUNT OPTIMISATIONS " ), long, default_value= " 0 " , value_name= " votes " ) ]
min_threshold : String ,
// -----------------
// -- Constraints --
/// Path to a CON file specifying constraints
#[ clap(help_heading=Some( " CONSTRAINTS " ), long) ]
constraints : Option < String > ,
/// Mode of handling constraints
2022-04-16 02:27:59 +10:00
#[ clap(help_heading=Some( " CONSTRAINTS " ), long, possible_values=& [ " guard_doom " , " repeat_count " ] , default_value= " guard_doom " ) ]
2021-08-20 02:16:54 +10:00
constraint_mode : String ,
2021-09-09 04:07:18 +10:00
// ---------------------
// -- Output settings --
#[ clap(help_heading=Some( " OUTPUT " ), short, long, possible_values=& [ " text " , " csv " ] , default_value= " text " ) ]
output : String ,
2021-08-20 02:16:54 +10:00
/// Hide excluded candidates from results report
2021-09-09 04:07:18 +10:00
#[ clap(help_heading=Some( " OUTPUT " ), long) ]
2021-08-20 02:16:54 +10:00
hide_excluded : bool ,
/// Sort candidates by votes in results report
2021-09-09 04:07:18 +10:00
#[ clap(help_heading=Some( " OUTPUT " ), long) ]
2021-08-20 02:16:54 +10:00
sort_votes : bool ,
2021-09-11 01:19:38 +10:00
/// Show details of transfers to candidates during surplus distributions/candidate exclusions
#[ clap(help_heading=Some( " OUTPUT " ), long) ]
transfers_detail : bool ,
2021-08-20 02:16:54 +10:00
/// Print votes to specified decimal places in results report
2021-09-09 04:07:18 +10:00
#[ clap(help_heading=Some( " OUTPUT " ), long, default_value= " 2 " , value_name= " dps " ) ]
2021-08-20 02:16:54 +10:00
pp_decimals : usize ,
}
/// Entrypoint for subcommand
pub fn main ( cmd_opts : SubcmdOptions ) -> Result < ( ) , i32 > {
// Read and count election according to --numbers
if cmd_opts . numbers = = " rational " {
2021-09-02 17:17:45 +10:00
let mut election = election_from_file ( & cmd_opts . filename , cmd_opts . bin ) ? ;
2022-04-16 02:27:59 +10:00
maybe_load_constraints ( & mut election , & cmd_opts . constraints , & cmd_opts . constraint_mode ) ? ;
2021-08-20 02:16:54 +10:00
// Must specify ::<N> here and in a few other places because ndarray causes E0275 otherwise
count_election ::< Rational > ( election , cmd_opts ) ? ;
} else if cmd_opts . numbers = = " float64 " {
2021-09-02 17:17:45 +10:00
let mut election = election_from_file ( & cmd_opts . filename , cmd_opts . bin ) ? ;
2022-04-16 02:27:59 +10:00
maybe_load_constraints ( & mut election , & cmd_opts . constraints , & cmd_opts . constraint_mode ) ? ;
2021-08-20 02:16:54 +10:00
count_election ::< NativeFloat64 > ( election , cmd_opts ) ? ;
} else if cmd_opts . numbers = = " fixed " {
Fixed ::set_dps ( cmd_opts . decimals ) ;
2021-09-02 17:17:45 +10:00
let mut election = election_from_file ( & cmd_opts . filename , cmd_opts . bin ) ? ;
2022-04-16 02:27:59 +10:00
maybe_load_constraints ( & mut election , & cmd_opts . constraints , & cmd_opts . constraint_mode ) ? ;
2021-08-20 02:16:54 +10:00
count_election ::< Fixed > ( election , cmd_opts ) ? ;
} else if cmd_opts . numbers = = " gfixed " {
GuardedFixed ::set_dps ( cmd_opts . decimals ) ;
2021-09-02 17:17:45 +10:00
let mut election = election_from_file ( & cmd_opts . filename , cmd_opts . bin ) ? ;
2022-04-16 02:27:59 +10:00
maybe_load_constraints ( & mut election , & cmd_opts . constraints , & cmd_opts . constraint_mode ) ? ;
2021-08-20 02:16:54 +10:00
count_election ::< GuardedFixed > ( election , cmd_opts ) ? ;
}
return Ok ( ( ) ) ;
}
2021-09-02 17:17:45 +10:00
fn election_from_file < N : Number > ( path : & str , bin : bool ) -> Result < Election < N > , i32 > {
if bin {
// BIN format
return Ok ( bin ::parse_path ( path ) ) ;
} else {
// BLT format
match blt ::parse_path ( path ) {
Ok ( e ) = > return Ok ( e ) ,
Err ( err ) = > {
println! ( " Syntax Error: {} " , err ) ;
return Err ( 1 ) ;
}
2021-08-20 02:16:54 +10:00
}
}
}
2022-04-16 02:27:59 +10:00
fn maybe_load_constraints < N : Number > ( election : & mut Election < N > , constraints : & Option < String > , constraint_mode : & str ) -> Result < ( ) , i32 > {
2021-08-20 02:16:54 +10:00
if let Some ( c ) = constraints {
let file = File ::open ( c ) . expect ( " IO Error " ) ;
let lines = io ::BufReader ::new ( file ) . lines ( ) ;
2021-10-29 23:07:38 +11:00
let lines : Vec < _ > = lines . map ( | r | r . expect ( " IO Error " ) ) . collect ( ) ;
match Constraints ::from_con ( lines . into_iter ( ) ) {
Ok ( c ) = > {
election . constraints = Some ( c ) ;
}
Err ( err ) = > {
println! ( " Constraint Syntax Error: {} " , err ) ;
return Err ( 1 ) ;
}
}
// Validate constraints
2022-04-16 02:27:59 +10:00
if let Err ( err ) = election . constraints . as_ref ( ) . unwrap ( ) . validate_constraints ( election . candidates . len ( ) , constraint_mode . into ( ) ) {
2021-10-29 23:07:38 +11:00
println! ( " Constraint Validation Error: {} " , err ) ;
return Err ( 1 ) ;
}
2022-04-16 02:27:59 +10:00
if constraint_mode = = " repeat_count " {
constraints ::init_repeat_count ( election ) ;
}
2021-08-20 02:16:54 +10:00
}
2021-10-29 23:07:38 +11:00
Ok ( ( ) )
2021-08-20 02:16:54 +10:00
}
2021-09-09 04:07:18 +10:00
fn count_election < N : Number > ( election : Election < N > , cmd_opts : SubcmdOptions ) -> Result < ( ) , i32 >
2021-08-20 02:16:54 +10:00
where
for < ' r > & ' r N : ops ::Add < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Sub < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Mul < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Neg < Output = N >
{
// Copy applicable options
let opts = STVOptions ::new (
cmd_opts . round_surplus_fractions ,
cmd_opts . round_values ,
cmd_opts . round_votes ,
cmd_opts . round_quota ,
2022-03-23 00:34:43 +11:00
cmd_opts . round_subtransfers . into ( ) ,
2021-10-27 19:52:51 +11:00
cmd_opts . meek_surplus_tolerance ,
2021-08-20 02:16:54 +10:00
cmd_opts . quota . into ( ) ,
cmd_opts . quota_criterion . into ( ) ,
cmd_opts . quota_mode . into ( ) ,
ties ::from_strs ( cmd_opts . ties , cmd_opts . random_seed ) ,
cmd_opts . surplus . into ( ) ,
cmd_opts . surplus_order . into ( ) ,
cmd_opts . transferable_only ,
2022-03-25 02:46:30 +11:00
cmd_opts . subtract_nontransferable ,
2021-08-20 02:16:54 +10:00
cmd_opts . exclusion . into ( ) ,
cmd_opts . meek_nz_exclusion ,
cmd_opts . sample . into ( ) ,
cmd_opts . sample_per_ballot ,
! cmd_opts . no_early_bulk_elect ,
cmd_opts . bulk_exclude ,
cmd_opts . defer_surpluses ,
! cmd_opts . no_immediate_elect ,
cmd_opts . min_threshold ,
cmd_opts . constraints ,
cmd_opts . constraint_mode . into ( ) ,
cmd_opts . hide_excluded ,
cmd_opts . sort_votes ,
2021-09-11 01:19:38 +10:00
cmd_opts . transfers_detail ,
2021-08-20 02:16:54 +10:00
cmd_opts . pp_decimals ,
) ;
// Validate options
match opts . validate ( ) {
Ok ( _ ) = > { }
Err ( err ) = > {
println! ( " Error: {} " , err . describe ( ) ) ;
return Err ( 1 ) ;
}
}
2021-09-09 04:07:18 +10:00
match cmd_opts . output . as_str ( ) {
" text " = > { return count_election_text ( election , & cmd_opts . filename , opts ) ; }
" csv " = > { return count_election_csv ( election , opts ) ; }
_ = > unreachable! ( )
}
}
// ---------------
// CLI text report
fn count_election_text < N : Number > ( mut election : Election < N > , filename : & str , opts : STVOptions ) -> Result < ( ) , i32 >
where
for < ' r > & ' r N : ops ::Add < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Sub < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Mul < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Neg < Output = N >
{
2021-08-20 02:16:54 +10:00
// Describe count
let total_ballots = election . ballots . iter ( ) . fold ( N ::zero ( ) , | acc , b | { acc + & b . orig_value } ) ;
2022-04-16 02:27:59 +10:00
print! ( " Count computed by OpenTally (revision {} ). Read {:.0} ballots from \" {} \" for election \" {} \" . There are {} candidates for {} vacancies. " , crate ::VERSION , total_ballots , filename , election . name , election . candidates . iter ( ) . filter ( | c | ! c . is_dummy ) . count ( ) , election . seats ) ;
2021-08-20 02:16:54 +10:00
let opts_str = opts . describe ::< N > ( ) ;
2021-10-27 19:52:51 +11:00
if ! opts_str . is_empty ( ) {
2021-08-20 02:16:54 +10:00
println! ( " Counting using options \" {} \" . " , opts_str ) ;
} else {
println! ( " Counting using default options. " ) ;
}
println! ( ) ;
2021-09-04 22:46:29 +10:00
stv ::preprocess_election ( & mut election , & opts ) ;
2021-09-03 23:53:15 +10:00
2021-08-20 02:16:54 +10:00
// Initialise count state
let mut state = CountState ::new ( & election ) ;
// Distribute first preferences
match stv ::count_init ( & mut state , & opts ) {
Ok ( _ ) = > { }
Err ( err ) = > {
println! ( " Error: {} " , err . describe ( ) ) ;
return Err ( 1 ) ;
}
}
let mut stage_num = 1 ;
print_stage ( stage_num , & state , & opts ) ;
loop {
match stv ::count_one_stage ( & mut state , & opts ) {
Ok ( is_done ) = > {
if is_done {
break ;
}
}
Err ( err ) = > {
println! ( " Error: {} " , err . describe ( ) ) ;
return Err ( 1 ) ;
}
}
stage_num + = 1 ;
print_stage ( stage_num , & state , & opts ) ;
}
println! ( " Count complete. The winning candidates are, in order of election: " ) ;
let mut winners = Vec ::new ( ) ;
for ( candidate , count_card ) in state . candidates . iter ( ) {
if count_card . state = = CandidateState ::Elected {
winners . push ( ( candidate , count_card ) ) ;
}
}
winners . sort_unstable_by ( | a , b | a . 1. order_elected . cmp ( & b . 1. order_elected ) ) ;
for ( i , ( winner , count_card ) ) in winners . into_iter ( ) . enumerate ( ) {
if let Some ( kv ) = & count_card . keep_value {
println! ( " {} . {} (kv = {:.dps2$} ) " , i + 1 , winner . name , kv , dps2 = max ( opts . pp_decimals , 2 ) ) ;
} else {
println! ( " {} . {} " , i + 1 , winner . name ) ;
}
}
return Ok ( ( ) ) ;
}
2021-09-09 04:07:18 +10:00
fn print_stage < N : Number > ( stage_num : u32 , state : & CountState < N > , opts : & STVOptions ) {
2021-08-20 02:16:54 +10:00
// Print stage details
2021-09-06 02:43:33 +10:00
println! ( " {} . {} " , stage_num , state . title ) ;
2021-08-20 02:16:54 +10:00
println! ( " {} " , state . logger . render ( ) . join ( " " ) ) ;
2021-09-11 18:42:15 +10:00
if opts . transfers_detail {
if let Some ( tt ) = & state . transfer_table {
println! ( ) ;
2021-09-12 00:20:49 +10:00
println! ( " {} " , tt . render_text ( opts ) ) ;
2021-09-11 18:42:15 +10:00
}
}
2021-08-20 02:16:54 +10:00
// Print candidates
print! ( " {} " , state . describe_candidates ( opts ) ) ;
// Print summary rows
print! ( " {} " , state . describe_summary ( opts ) ) ;
2021-09-11 18:42:15 +10:00
println! ( ) ;
2021-08-20 02:16:54 +10:00
}
2021-09-09 04:07:18 +10:00
2021-09-09 13:46:10 +10:00
// ----------------------------------
// Wichmann/eSTV/ERS-style CSV report
2021-09-09 04:07:18 +10:00
fn count_election_csv < N : Number > ( mut election : Election < N > , opts : STVOptions ) -> Result < ( ) , i32 >
where
for < ' r > & ' r N : ops ::Add < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Sub < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Mul < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Neg < Output = N >
{
// Header rows
let total_ballots = election . ballots . iter ( ) . fold ( N ::zero ( ) , | acc , b | { acc + & b . orig_value } ) ;
// eSTV does not consistently quote records, so we won't use a CSV library here
println! ( r # ""Election for","{}""# , election . name ) ;
println! ( r # ""Date"," / / ""# ) ;
println! ( r # ""Number to be elected",{}"# , election . seats ) ;
stv ::preprocess_election ( & mut election , & opts ) ;
// Initialise count state
let mut state = CountState ::new ( & election ) ;
let mut stage_results = vec! [ Vec ::new ( ) ; election . candidates . len ( ) + 5 ] ;
// -----------
// First stage
// Distribute first preferences
match stv ::count_init ( & mut state , & opts ) {
Ok ( _ ) = > { }
Err ( err ) = > {
println! ( " Error: {} " , err . describe ( ) ) ;
return Err ( 1 ) ;
}
}
// Subtract this from progressive NTs
2021-10-26 00:55:49 +11:00
// TODO: May fail to round correctly with minivoters
2021-09-09 04:07:18 +10:00
let invalid_votes = state . exhausted . votes . clone ( ) ;
let valid_votes = total_ballots - & invalid_votes ;
// Stage number row
stage_results [ 0 ] . push ( String ::new ( ) ) ;
stage_results [ 0 ] . push ( String ::new ( ) ) ;
// Stage kind row
stage_results [ 1 ] . push ( String ::new ( ) ) ;
stage_results [ 1 ] . push ( String ::from ( r # ""First""# ) ) ;
// Stage title row
stage_results [ 2 ] . push ( String ::from ( r # ""Candidates""# ) ) ;
stage_results [ 2 ] . push ( String ::from ( r # ""Preferences""# ) ) ;
for ( i , candidate ) in election . candidates . iter ( ) . enumerate ( ) {
let count_card = & state . candidates [ candidate ] ;
stage_results [ 3 + i ] . push ( format! ( r # ""{}""# , candidate . name ) ) ;
2021-10-26 00:55:49 +11:00
stage_results [ 3 + i ] . push ( format! ( r # "{:.0}"# , count_card . votes ) ) ; // TODO: May fail to round correctly with minivoters
2021-09-09 04:07:18 +10:00
}
stage_results [ 3 + election . candidates . len ( ) ] . push ( String ::from ( r # ""Non-transferable""# ) ) ;
2021-10-26 00:55:49 +11:00
stage_results [ 3 + election . candidates . len ( ) ] . push ( String ::new ( ) ) ; // TODO: May fail to round correctly with minivoters
2021-09-09 04:07:18 +10:00
stage_results [ 4 + election . candidates . len ( ) ] . push ( String ::from ( r # ""Totals""# ) ) ;
stage_results [ 4 + election . candidates . len ( ) ] . push ( format! ( r # "{:.0}"# , valid_votes ) ) ;
2021-09-09 13:46:10 +10:00
//let mut orig_states = HashMap::new();
//for (candidate, count_card) in state.candidates.iter() {
// orig_states.insert(*candidate, count_card.state);
//}
2021-09-09 04:07:18 +10:00
// -----------------
// Subsequent stages
let mut stage_num : u32 = 1 ;
loop {
match stv ::count_one_stage ( & mut state , & opts ) {
Ok ( is_done ) = > {
if is_done {
break ;
}
}
Err ( err ) = > {
println! ( " Error: {} " , err . describe ( ) ) ;
return Err ( 1 ) ;
}
}
stage_num + = 1 ;
// Stage number row
stage_results [ 0 ] . push ( String ::from ( r # ""Stage""# ) ) ;
stage_results [ 0 ] . push ( format! ( r # "{}"# , stage_num ) ) ;
// Stage kind row
stage_results [ 1 ] . push ( format! ( r # ""{}""# , state . title . kind_as_string ( ) ) ) ;
stage_results [ 1 ] . push ( String ::new ( ) ) ;
// Stage title row
match & state . title {
StageKind ::FirstPreferences = > unreachable! ( ) ,
StageKind ::SurplusOf ( candidate ) = > {
stage_results [ 2 ] . push ( format! ( r # ""{}""# , candidate . name ) ) ;
}
StageKind ::ExclusionOf ( candidates ) = > {
2021-09-09 13:46:10 +10:00
stage_results [ 2 ] . push ( format! ( r # ""{}""# , candidates . iter ( ) . map ( | c | & c . name ) . sorted ( ) . join ( " + " ) ) ) ;
2021-09-09 04:07:18 +10:00
}
2022-04-16 02:27:59 +10:00
StageKind ::Rollback = > todo! ( ) ,
StageKind ::RollbackExhausted = > todo! ( ) ,
StageKind ::BallotsOf ( candidate ) = > {
stage_results [ 2 ] . push ( format! ( r # ""{}""# , candidate . name ) ) ;
}
2021-09-09 04:07:18 +10:00
StageKind ::SurplusesDistributed = > todo! ( ) ,
2021-09-09 13:46:10 +10:00
StageKind ::BulkElection = > {
//let mut elected_candidates = Vec::new();
//for candidate in election.candidates.iter() {
// if state.candidates[candidate].state == CandidateState::Hopeful && orig_states[candidate].state != CandidateState::Hopeful {
// elected_candidates.push(candidate);
// }
//}
stage_results [ 2 ] . push ( String ::from ( r # ""Bulk election""# ) ) ;
}
2021-09-09 04:07:18 +10:00
}
stage_results [ 2 ] . push ( String ::from ( r # ""# ) ) ;
for ( i , candidate ) in election . candidates . iter ( ) . enumerate ( ) {
let count_card = & state . candidates [ candidate ] ;
if count_card . transfers . is_zero ( ) {
stage_results [ 3 + i ] . push ( String ::new ( ) ) ;
} else if count_card . transfers > N ::zero ( ) {
stage_results [ 3 + i ] . push ( format! ( r # "+{:.dps$}"# , count_card . transfers , dps = opts . pp_decimals ) ) ;
} else {
stage_results [ 3 + i ] . push ( format! ( r # "{:.dps$}"# , count_card . transfers , dps = opts . pp_decimals ) ) ;
}
if count_card . votes . is_zero ( ) {
stage_results [ 3 + i ] . push ( String ::from ( r # ""-""# ) ) ;
} else {
stage_results [ 3 + i ] . push ( format! ( r # "{:.dps$}"# , count_card . votes , dps = opts . pp_decimals ) ) ;
}
}
// Nontransferable
let nt_transfers = state . exhausted . transfers . clone ( ) + & state . loss_fraction . transfers ;
if nt_transfers . is_zero ( ) {
stage_results [ 3 + election . candidates . len ( ) ] . push ( String ::new ( ) ) ;
} else if nt_transfers > N ::zero ( ) {
stage_results [ 3 + election . candidates . len ( ) ] . push ( format! ( r # "+{:.dps$}"# , nt_transfers , dps = opts . pp_decimals ) ) ;
} else {
stage_results [ 3 + election . candidates . len ( ) ] . push ( format! ( r # "{:.dps$}"# , nt_transfers , dps = opts . pp_decimals ) ) ;
}
2021-09-10 00:46:07 +10:00
stage_results [ 3 + election . candidates . len ( ) ] . push ( format! ( r # "{:.dps$}"# , & state . exhausted . votes + & state . loss_fraction . votes - & invalid_votes , dps = opts . pp_decimals ) ) ;
2021-09-09 04:07:18 +10:00
// Totals
stage_results [ 4 + election . candidates . len ( ) ] . push ( String ::new ( ) ) ;
stage_results [ 4 + election . candidates . len ( ) ] . push ( format! ( r # "{:.dps$}"# , valid_votes , dps = opts . pp_decimals ) ) ;
2021-09-09 13:46:10 +10:00
//for (candidate, count_card) in state.candidates.iter() {
// orig_states.insert(*candidate, count_card.state);
//}
2021-09-09 04:07:18 +10:00
}
// ----------------
// Candidate states
stage_results [ 3 + election . candidates . len ( ) ] . push ( String ::new ( ) ) ; // Nontransferable row
for ( i , candidate ) in election . candidates . iter ( ) . enumerate ( ) {
let count_card = & state . candidates [ candidate ] ;
if count_card . state = = CandidateState ::Elected {
stage_results [ 3 + i ] . push ( String ::from ( r # ""Elected""# ) ) ;
} else {
stage_results [ 3 + i ] . push ( String ::new ( ) ) ;
}
}
// --------------------
// Output stages to CSV
println! ( r # ""Valid votes",{:.0}"# , valid_votes ) ;
println! ( r # ""Invalid votes",{:.0}"# , invalid_votes ) ;
println! ( r # ""Quota",{:.dps$}"# , state . quota . as_ref ( ) . unwrap ( ) , dps = opts . pp_decimals ) ;
println! ( r # ""OpenTally","{}""# , crate ::VERSION ) ;
println! ( r # ""Election rules","{}""# , opts . describe ::< N > ( ) ) ;
for row in stage_results {
println! ( " {} " , row . join ( " , " ) ) ;
}
return Ok ( ( ) ) ;
}