2021-05-28 11:58:40 +02: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-27 13:57:24 +02:00
use opentally ::constraints ::Constraints ;
2021-07-31 09:41:28 +02:00
use opentally ::election ::{ CandidateState , CountState , Election } ;
2021-06-14 13:43:43 +02:00
use opentally ::numbers ::{ Fixed , GuardedFixed , NativeFloat64 , Number , Rational } ;
2021-07-31 09:41:28 +02:00
use opentally ::stv ::{ self , STVOptions } ;
2021-05-28 11:58:40 +02:00
2021-05-29 09:51:45 +02:00
use clap ::{ AppSettings , Clap } ;
2021-05-28 12:42:01 +02:00
2021-06-16 05:00:54 +02:00
use std ::cmp ::max ;
2021-07-28 19:24:51 +02:00
use std ::fs ::File ;
2021-07-31 07:24:23 +02:00
use std ::io ::{ self , BufRead } ;
2021-05-28 19:01:07 +02:00
use std ::ops ;
2021-05-28 11:58:40 +02:00
2021-05-28 14:37:07 +02:00
/// Open-source election vote counting
2021-05-28 12:42:01 +02:00
#[ derive(Clap) ]
2021-06-03 13:35:25 +02:00
#[ clap(name= " OpenTally " , version=opentally::VERSION) ]
2021-05-28 12:42:01 +02:00
struct Opts {
#[ clap(subcommand) ]
command : Command ,
}
#[ derive(Clap) ]
enum Command {
STV ( STV ) ,
}
/// Count a single transferable vote (STV) election
#[ derive(Clap) ]
2021-05-29 15:25:05 +02:00
#[ clap(setting=AppSettings::DeriveDisplayOrder) ]
2021-05-28 12:42:01 +02:00
struct STV {
2021-05-31 15:17:21 +02:00
// ----------------
2021-05-29 09:51:45 +02:00
// -- File input --
2021-05-28 12:42:01 +02:00
/// Path to the BLT file to be counted
filename : String ,
2021-05-29 09:51:45 +02:00
2021-05-31 15:17:21 +02:00
// ----------------------
2021-05-29 09:51:45 +02:00
// -- Numbers settings --
2021-05-28 19:01:07 +02:00
/// Numbers mode
2021-06-14 13:43:43 +02:00
#[ clap(help_heading=Some( " NUMBERS " ), short, long, possible_values=& [ " rational " , " fixed " , " gfixed " , " float64 " ] , default_value= " rational " , value_name= " mode " ) ]
2021-05-28 19:01:07 +02:00
numbers : String ,
2021-05-29 09:51:45 +02:00
2021-06-04 14:05:48 +02:00
/// Decimal places if --numbers fixed
#[ clap(help_heading=Some( " NUMBERS " ), long, default_value= " 5 " , value_name= " dps " ) ]
decimals : usize ,
2021-06-11 13:23:08 +02:00
/// Convert ballots with value >1 to multiple ballots of value 1
#[ clap(help_heading=Some( " NUMBERS " ), long) ]
normalise_ballots : bool ,
2021-05-31 15:17:21 +02:00
// -----------------------
2021-05-29 09:51:45 +02:00
// -- Rounding settings --
2021-08-03 08:46:21 +02:00
/// Round surplus fractions to specified decimal places
2021-08-03 15:22:52 +02:00
#[ clap(help_heading=Some( " ROUNDING " ), long, alias= " round-tvs " , value_name= " dps " ) ]
2021-08-03 08:46:21 +02:00
round_surplus_fractions : Option < usize > ,
2021-06-01 13:20:38 +02:00
2021-08-03 08:46:21 +02:00
/// Round ballot values to specified decimal places
2021-08-03 15:22:52 +02:00
#[ clap(help_heading=Some( " ROUNDING " ), long, alias= " round-weights " , value_name= " dps " ) ]
2021-08-03 08:46:21 +02:00
round_values : Option < usize > ,
2021-06-01 13:20:38 +02:00
2021-05-29 09:51:45 +02:00
/// Round votes to specified decimal places
2021-05-29 15:25:05 +02:00
#[ clap(help_heading=Some( " ROUNDING " ), long, value_name= " dps " ) ]
2021-05-29 09:51:45 +02:00
round_votes : Option < usize > ,
2021-06-01 13:20:38 +02:00
/// Round quota to specified decimal places
#[ clap(help_heading=Some( " ROUNDING " ), long, value_name= " dps " ) ]
round_quota : Option < usize > ,
2021-07-21 16:40:01 +02:00
/// (Gregory STV) How to calculate votes to credit to candidates in surplus transfers
2021-06-11 13:22:28 +02:00
#[ clap(help_heading=Some( " ROUNDING " ), long, possible_values=& [ " single_step " , " by_value " , " per_ballot " ] , default_value= " single_step " , value_name= " mode " ) ]
sum_surplus_transfers : String ,
2021-06-18 10:48:12 +02: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 ,
2021-06-02 10:07:05 +02:00
// -----------
// -- Quota --
/// Quota type
#[ clap(help_heading=Some( " QUOTA " ), short, long, possible_values=& [ " droop " , " hare " , " droop_exact " , " hare_exact " ] , default_value= " droop_exact " ) ]
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 ,
2021-06-12 13:16:53 +02:00
/// Whether to apply a form of progressive quota
2021-07-18 12:01:35 +02:00
#[ clap(help_heading=Some( " QUOTA " ), long, possible_values=& [ " static " , " ers97 " , " ers76 " ] , default_value= " static " , value_name= " mode " ) ]
2021-06-07 12:52:18 +02:00
quota_mode : String ,
2021-05-31 15:17:21 +02:00
// ------------------
2021-05-29 18:28:52 +02:00
// -- STV variants --
2021-06-12 16:15:14 +02:00
/// 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 > ,
2021-06-12 19:15:15 +02:00
/// Random seed to use with --ties random
#[ clap(help_heading=Some( " STV VARIANTS " ), long, value_name= " seed " ) ]
random_seed : Option < String > ,
2021-06-09 04:16:25 +02:00
/// Method of surplus distributions
2021-08-03 10:38:45 +02:00
#[ clap(help_heading=Some( " STV VARIANTS " ), short='s', long, possible_values=& [ " wig " , " uig " , " eg " , " meek " , " cincinnati " , " hare " ] , default_value= " wig " , value_name= " method " ) ]
2021-05-31 14:25:53 +02:00
surplus : String ,
2021-07-21 16:40:01 +02:00
/// (Gregory STV) Order to distribute surpluses
2021-06-01 10:57:56 +02:00
#[ clap(help_heading=Some( " STV VARIANTS " ), long, possible_values=& [ " by_size " , " by_order " ] , default_value= " by_size " , value_name= " order " ) ]
surplus_order : String ,
2021-07-21 16:40:01 +02:00
/// (Gregory STV) Examine only transferable papers during surplus distributions
2021-05-31 15:17:21 +02:00
#[ clap(help_heading=Some( " STV VARIANTS " ), long) ]
transferable_only : bool ,
2021-07-21 16:40:01 +02:00
/// (Gregory STV) Method of exclusions
2021-07-19 15:15:17 +02:00
#[ 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 " ) ]
2021-05-29 18:28:52 +02:00
exclusion : String ,
2021-06-19 17:28:54 +02:00
/// (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-06-08 14:22:43 +02:00
// -------------------------
// -- Count optimisations --
2021-06-22 16:52:25 +02:00
/// Continue count even if continuing candidates fill all remaining vacancies
#[ clap(help_heading=Some( " COUNT OPTIMISATIONS " ), long) ]
no_early_bulk_elect : bool ,
2021-06-09 04:16:25 +02:00
/// Use bulk exclusion
2021-06-08 14:22:43 +02:00
#[ clap(help_heading=Some( " COUNT OPTIMISATIONS " ), long) ]
bulk_exclude : bool ,
2021-06-09 04:16:25 +02:00
/// Defer surplus distributions if possible
#[ clap(help_heading=Some( " COUNT OPTIMISATIONS " ), long) ]
defer_surpluses : bool ,
2021-06-18 10:48:12 +02:00
/// (Meek STV) Immediately elect candidates even if keep values have not converged
#[ clap(help_heading=Some( " COUNT OPTIMISATIONS " ), long) ]
meek_immediate_elect : bool ,
2021-08-03 15:22:52 +02:00
/// 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 ,
2021-06-27 13:57:24 +02:00
// -----------------
// -- Constraints --
/// Path to a CON file specifying constraints
#[ clap(help_heading=Some( " CONSTRAINTS " ), long) ]
constraints : Option < String > ,
/// Mode of handling constraints
#[ clap(help_heading=Some( " CONSTRAINTS " ), long, possible_values=& [ " guard_doom " ] , default_value= " guard_doom " ) ]
constraint_mode : String ,
2021-05-31 15:17:21 +02:00
// ----------------------
2021-05-29 09:51:45 +02:00
// -- Display settings --
2021-05-28 16:43:58 +02:00
/// Hide excluded candidates from results report
2021-05-29 15:25:05 +02:00
#[ clap(help_heading=Some( " DISPLAY " ), long) ]
2021-05-28 16:43:58 +02:00
hide_excluded : bool ,
2021-05-31 15:17:21 +02:00
2021-05-28 16:43:58 +02:00
/// Sort candidates by votes in results report
2021-05-29 15:25:05 +02:00
#[ clap(help_heading=Some( " DISPLAY " ), long) ]
2021-05-28 16:43:58 +02:00
sort_votes : bool ,
2021-05-31 15:17:21 +02:00
2021-05-28 12:42:01 +02:00
/// Print votes to specified decimal places in results report
2021-05-29 15:25:05 +02:00
#[ clap(help_heading=Some( " DISPLAY " ), long, default_value= " 2 " , value_name= " dps " ) ]
2021-05-28 12:42:01 +02:00
pp_decimals : usize ,
}
2021-05-28 11:58:40 +02:00
fn main ( ) {
2021-07-31 07:24:23 +02:00
match main_ ( ) {
Ok ( _ ) = > { }
Err ( code ) = > {
std ::process ::exit ( code ) ;
}
}
}
fn main_ ( ) -> Result < ( ) , i32 > {
2021-05-28 12:42:01 +02:00
// Read arguments
let opts : Opts = Opts ::parse ( ) ;
let Command ::STV ( cmd_opts ) = opts . command ;
2021-07-31 07:24:23 +02:00
// Read and count election according to --numbers
2021-05-28 19:01:07 +02:00
if cmd_opts . numbers = = " rational " {
2021-07-31 07:24:23 +02:00
let mut election = election_from_file ( & cmd_opts . filename ) ? ;
2021-06-27 13:57:24 +02:00
maybe_load_constraints ( & mut election , & cmd_opts . constraints ) ;
2021-06-27 09:44:30 +02:00
// Must specify ::<N> here and in a few other places because ndarray causes E0275 otherwise
2021-07-31 07:24:23 +02:00
count_election ::< Rational > ( election , cmd_opts ) ? ;
2021-05-29 09:51:45 +02:00
} else if cmd_opts . numbers = = " float64 " {
2021-07-31 07:24:23 +02:00
let mut election = election_from_file ( & cmd_opts . filename ) ? ;
2021-06-27 13:57:24 +02:00
maybe_load_constraints ( & mut election , & cmd_opts . constraints ) ;
2021-07-31 07:24:23 +02:00
count_election ::< NativeFloat64 > ( election , cmd_opts ) ? ;
2021-06-04 14:05:48 +02:00
} else if cmd_opts . numbers = = " fixed " {
Fixed ::set_dps ( cmd_opts . decimals ) ;
2021-07-31 07:24:23 +02:00
let mut election = election_from_file ( & cmd_opts . filename ) ? ;
2021-06-27 13:57:24 +02:00
maybe_load_constraints ( & mut election , & cmd_opts . constraints ) ;
2021-07-31 07:24:23 +02:00
count_election ::< Fixed > ( election , cmd_opts ) ? ;
2021-06-14 13:43:43 +02:00
} else if cmd_opts . numbers = = " gfixed " {
GuardedFixed ::set_dps ( cmd_opts . decimals ) ;
2021-06-27 13:57:24 +02:00
2021-07-31 07:24:23 +02:00
let mut election = election_from_file ( & cmd_opts . filename ) ? ;
2021-06-27 13:57:24 +02:00
maybe_load_constraints ( & mut election , & cmd_opts . constraints ) ;
2021-07-31 07:24:23 +02:00
count_election ::< GuardedFixed > ( election , cmd_opts ) ? ;
}
return Ok ( ( ) ) ;
}
fn election_from_file < N : Number > ( path : & str ) -> Result < Election < N > , i32 > {
match Election ::from_file ( path ) {
Ok ( e ) = > return Ok ( e ) ,
Err ( err ) = > {
println! ( " Syntax Error: {} " , err ) ;
return Err ( 1 ) ;
}
2021-05-28 19:01:07 +02:00
}
}
2021-06-27 13:57:24 +02:00
fn maybe_load_constraints < N : Number > ( election : & mut Election < N > , constraints : & Option < String > ) {
if let Some ( c ) = constraints {
let file = File ::open ( c ) . expect ( " IO Error " ) ;
let lines = io ::BufReader ::new ( file ) . lines ( ) ;
election . constraints = Some ( Constraints ::from_con ( lines . map ( | r | r . expect ( " IO Error " ) . to_string ( ) ) . into_iter ( ) ) ) ;
}
}
2021-07-31 07:24:23 +02:00
fn count_election < N : Number > ( mut election : Election < N > , cmd_opts : STV ) -> Result < ( ) , i32 >
2021-05-28 19:01:07 +02:00
where
for < ' r > & ' r N : ops ::Sub < & ' r N , Output = N > ,
2021-06-16 05:00:54 +02:00
for < ' r > & ' r N : ops ::Mul < & ' r N , Output = N > ,
2021-05-29 18:28:52 +02:00
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
2021-05-28 19:01:07 +02:00
for < ' r > & ' r N : ops ::Neg < Output = N >
{
2021-05-29 09:51:45 +02:00
// Copy applicable options
2021-07-31 09:41:28 +02:00
let opts = STVOptions ::new (
2021-08-03 08:46:21 +02:00
cmd_opts . round_surplus_fractions ,
cmd_opts . round_values ,
2021-06-01 11:04:03 +02:00
cmd_opts . round_votes ,
2021-06-01 13:20:38 +02:00
cmd_opts . round_quota ,
2021-06-11 13:22:28 +02:00
& cmd_opts . sum_surplus_transfers ,
2021-06-18 10:48:12 +02:00
& cmd_opts . meek_surplus_tolerance ,
2021-06-12 08:03:31 +02:00
cmd_opts . normalise_ballots ,
2021-06-02 10:07:05 +02:00
& cmd_opts . quota ,
& cmd_opts . quota_criterion ,
2021-06-07 12:52:18 +02:00
& cmd_opts . quota_mode ,
2021-06-12 16:15:14 +02:00
& cmd_opts . ties ,
2021-06-12 19:15:15 +02:00
& cmd_opts . random_seed ,
2021-06-01 11:04:03 +02:00
& cmd_opts . surplus ,
& cmd_opts . surplus_order ,
cmd_opts . transferable_only ,
& cmd_opts . exclusion ,
2021-06-22 06:29:33 +02:00
cmd_opts . meek_nz_exclusion ,
2021-06-22 16:52:25 +02:00
! cmd_opts . no_early_bulk_elect ,
2021-06-08 14:22:43 +02:00
cmd_opts . bulk_exclude ,
2021-06-09 04:16:25 +02:00
cmd_opts . defer_surpluses ,
2021-06-18 10:48:12 +02:00
cmd_opts . meek_immediate_elect ,
2021-08-03 15:22:52 +02:00
cmd_opts . min_threshold ,
2021-06-27 14:24:25 +02:00
cmd_opts . constraints . as_deref ( ) ,
2021-06-27 13:57:24 +02:00
& cmd_opts . constraint_mode ,
2021-07-31 09:41:28 +02:00
cmd_opts . hide_excluded ,
cmd_opts . sort_votes ,
2021-06-01 11:04:03 +02:00
cmd_opts . pp_decimals ,
) ;
2021-05-29 09:51:45 +02:00
2021-06-22 06:29:33 +02:00
// Validate options
2021-07-31 09:41:28 +02:00
match opts . validate ( ) {
2021-07-31 07:24:23 +02:00
Ok ( _ ) = > { }
Err ( err ) = > {
println! ( " Error: {} " , err . describe ( ) ) ;
return Err ( 1 ) ;
}
}
2021-06-22 06:29:33 +02:00
2021-06-12 08:03:31 +02:00
// Describe count
let total_ballots = election . ballots . iter ( ) . fold ( N ::zero ( ) , | acc , b | { acc + & b . orig_value } ) ;
print! ( " Count computed by OpenTally (revision {} ). Read {:.0} ballots from \" {} \" for election \" {} \" . There are {} candidates for {} vacancies. " , opentally ::VERSION , total_ballots , cmd_opts . filename , election . name , election . candidates . len ( ) , election . seats ) ;
2021-07-31 09:41:28 +02:00
let opts_str = opts . describe ::< N > ( ) ;
2021-06-12 08:03:31 +02:00
if opts_str . len ( ) > 0 {
println! ( " Counting using options \" {} \" . " , opts_str ) ;
} else {
println! ( " Counting using default options. " ) ;
}
println! ( ) ;
2021-06-11 13:23:08 +02:00
// Normalise ballots if requested
if cmd_opts . normalise_ballots {
election . normalise_ballots ( ) ;
}
2021-05-28 11:58:40 +02:00
// Initialise count state
let mut state = CountState ::new ( & election ) ;
// Distribute first preferences
2021-07-31 09:41:28 +02:00
match stv ::count_init ( & mut state , & opts ) {
2021-07-31 07:24:23 +02:00
Ok ( _ ) = > { }
Err ( err ) = > {
println! ( " Error: {} " , err . describe ( ) ) ;
return Err ( 1 ) ;
}
}
2021-05-28 11:58:40 +02:00
let mut stage_num = 1 ;
2021-07-31 09:41:28 +02:00
print_stage ( stage_num , & state , & opts ) ;
2021-05-28 11:58:40 +02:00
loop {
2021-07-31 09:41:28 +02:00
match stv ::count_one_stage ( & mut state , & opts ) {
2021-07-31 07:24:23 +02:00
Ok ( is_done ) = > {
if is_done {
break ;
}
}
Err ( err ) = > {
println! ( " Error: {} " , err . describe ( ) ) ;
return Err ( 1 ) ;
}
2021-05-28 11:58:40 +02:00
}
2021-07-31 07:24:23 +02:00
2021-05-29 09:51:45 +02:00
stage_num + = 1 ;
2021-07-31 09:41:28 +02:00
print_stage ( stage_num , & state , & opts ) ;
2021-05-28 11:58:40 +02:00
}
println! ( " Count complete. The winning candidates are, in order of election: " ) ;
let mut winners = Vec ::new ( ) ;
for ( candidate , count_card ) in state . candidates . iter ( ) {
2021-06-11 16:50:01 +02:00
if count_card . state = = CandidateState ::Elected {
2021-06-16 10:23:47 +02:00
winners . push ( ( candidate , count_card ) ) ;
2021-05-28 11:58:40 +02:00
}
}
2021-06-29 07:31:38 +02:00
winners . sort_unstable_by ( | a , b | a . 1. order_elected . cmp ( & b . 1. order_elected ) ) ;
2021-05-28 11:58:40 +02:00
2021-06-16 10:23:47 +02:00
for ( i , ( winner , count_card ) ) in winners . into_iter ( ) . enumerate ( ) {
if let Some ( kv ) = & count_card . keep_value {
2021-07-31 09:41:28 +02:00
println! ( " {} . {} (kv = {:.dps2$} ) " , i + 1 , winner . name , kv , dps2 = max ( opts . pp_decimals , 2 ) ) ;
2021-06-16 10:23:47 +02:00
} else {
println! ( " {} . {} " , i + 1 , winner . name ) ;
}
2021-05-28 11:58:40 +02:00
}
2021-07-31 07:24:23 +02:00
return Ok ( ( ) ) ;
2021-05-28 11:58:40 +02:00
}
2021-05-28 19:01:07 +02:00
2021-07-31 09:41:28 +02:00
fn print_stage < N : Number > ( stage_num : usize , state : & CountState < N > , opts : & STVOptions ) {
2021-05-28 19:01:07 +02:00
// Print stage details
2021-06-29 07:31:38 +02:00
match state . kind {
None = > { println! ( " {} . {} " , stage_num , state . title ) ; }
Some ( kind ) = > { println! ( " {} . {} {} " , stage_num , kind , state . title ) ; }
2021-05-28 19:01:07 +02:00
} ;
2021-07-31 09:41:28 +02:00
println! ( " {} " , state . logger . render ( ) . join ( " " ) ) ;
2021-05-28 19:01:07 +02:00
// Print candidates
2021-07-31 09:41:28 +02:00
print! ( " {} " , state . describe_candidates ( opts ) ) ;
2021-05-28 19:01:07 +02:00
// Print summary rows
2021-07-31 09:41:28 +02:00
print! ( " {} " , state . describe_summary ( opts ) ) ;
2021-05-28 19:01:07 +02:00
println! ( " " ) ;
}