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-05-29 19:50:19 +02:00
use opentally ::stv ;
use opentally ::election ::{ Candidate , CandidateState , CountCard , CountState , CountStateOrRef , Election , StageResult } ;
2021-06-14 13:43:43 +02:00
use opentally ::numbers ::{ Fixed , GuardedFixed , NativeFloat64 , Number , Rational } ;
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-05-28 11:58:40 +02:00
use std ::fs ::File ;
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-06-01 13:20:38 +02:00
/// Round transfer values to specified decimal places
#[ clap(help_heading=Some( " ROUNDING " ), long, value_name= " dps " ) ]
round_tvs : Option < usize > ,
/// Round ballot weights to specified decimal places
#[ clap(help_heading=Some( " ROUNDING " ), long, value_name= " dps " ) ]
round_weights : Option < usize > ,
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-06-12 13:16:53 +02:00
/// 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-06-07 12:52:18 +02:00
#[ clap(help_heading=Some( " QUOTA " ), long, possible_values=& [ " static " , " ers97 " ] , default_value= " static " , value_name= " mode " ) ]
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-06-02 10:07:05 +02:00
#[ clap(help_heading=Some( " STV VARIANTS " ), short='s', long, possible_values=& [ " wig " , " uig " , " eg " , " meek " ] , default_value= " wig " , value_name= " method " ) ]
2021-05-31 14:25:53 +02:00
surplus : String ,
2021-06-12 13:16:53 +02:00
/// 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-05-31 15:17:21 +02:00
/// Examine only transferable papers during surplus distributions
#[ clap(help_heading=Some( " STV VARIANTS " ), long) ]
transferable_only : bool ,
2021-05-29 18:28:52 +02:00
/// Method of exclusions
2021-05-31 14:25:53 +02:00
#[ clap(help_heading=Some( " STV VARIANTS " ), long, possible_values=& [ " single_stage " , " by_value " , " parcels_by_order " ] , 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-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-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-05-28 12:42:01 +02:00
// Read arguments
let opts : Opts = Opts ::parse ( ) ;
let Command ::STV ( cmd_opts ) = opts . command ;
2021-05-28 11:58:40 +02:00
// Read BLT file
2021-05-28 12:42:01 +02:00
let file = File ::open ( & cmd_opts . filename ) . expect ( " IO Error " ) ;
2021-05-28 11:58:40 +02:00
let lines = io ::BufReader ::new ( file ) . lines ( ) ;
2021-05-28 19:01:07 +02:00
// Create and count election according to --numbers
if cmd_opts . numbers = = " rational " {
2021-05-30 10:28:39 +02:00
let election : Election < Rational > = Election ::from_blt ( lines . map ( | r | r . expect ( " IO Error " ) . to_string ( ) ) . into_iter ( ) ) ;
2021-05-28 19:01:07 +02:00
count_election ( election , cmd_opts ) ;
2021-05-29 09:51:45 +02:00
} else if cmd_opts . numbers = = " float64 " {
2021-05-30 10:28:39 +02:00
let election : Election < NativeFloat64 > = Election ::from_blt ( lines . map ( | r | r . expect ( " IO Error " ) . to_string ( ) ) . into_iter ( ) ) ;
2021-05-28 19:01:07 +02:00
count_election ( election , cmd_opts ) ;
2021-06-04 14:05:48 +02:00
} else if cmd_opts . numbers = = " fixed " {
Fixed ::set_dps ( cmd_opts . decimals ) ;
let election : Election < Fixed > = Election ::from_blt ( lines . map ( | r | r . expect ( " IO Error " ) . to_string ( ) ) . into_iter ( ) ) ;
count_election ( election , cmd_opts ) ;
2021-06-14 13:43:43 +02:00
} else if cmd_opts . numbers = = " gfixed " {
GuardedFixed ::set_dps ( cmd_opts . decimals ) ;
let election : Election < GuardedFixed > = Election ::from_blt ( lines . map ( | r | r . expect ( " IO Error " ) . to_string ( ) ) . into_iter ( ) ) ;
count_election ( election , cmd_opts ) ;
2021-05-28 19:01:07 +02:00
}
}
2021-06-11 13:23:08 +02:00
fn count_election < N : Number > ( mut election : Election < N > , cmd_opts : STV )
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-06-01 11:04:03 +02:00
let stv_opts = stv ::STVOptions ::new (
2021-06-01 13:20:38 +02:00
cmd_opts . round_tvs ,
cmd_opts . round_weights ,
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-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-06-19 17:28:54 +02:00
cmd_opts . meek_nz_exclusion ,
2021-06-01 11:04:03 +02:00
cmd_opts . pp_decimals ,
) ;
2021-05-29 09:51:45 +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 ) ;
let opts_str = stv_opts . describe ::< N > ( ) ;
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-05-29 09:51:45 +02:00
stv ::count_init ( & mut state , & stv_opts ) ;
2021-05-28 11:58:40 +02:00
let mut stage_num = 1 ;
2021-05-28 17:22:46 +02:00
make_and_print_result ( stage_num , & state , & cmd_opts ) ;
2021-05-28 11:58:40 +02:00
loop {
2021-05-29 09:51:45 +02:00
let is_done = stv ::count_one_stage ( & mut state , & stv_opts ) ;
2021-06-11 18:09:26 +02:00
if is_done . unwrap ( ) {
2021-05-28 11:58:40 +02:00
break ;
}
2021-05-29 09:51:45 +02:00
stage_num + = 1 ;
make_and_print_result ( stage_num , & state , & cmd_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-16 10:23:47 +02:00
winners . sort_unstable_by ( | a , b | a . 1. order_elected . partial_cmp ( & b . 1. order_elected ) . unwrap ( ) ) ;
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 {
println! ( " {} . {} (kv = {:.dps2$} ) " , i + 1 , winner . name , kv , dps2 = max ( stv_opts . pp_decimals , 2 ) ) ;
} else {
println! ( " {} . {} " , i + 1 , winner . name ) ;
}
2021-05-28 11:58:40 +02:00
}
}
2021-05-28 19:01:07 +02:00
fn print_candidates < ' a , N : ' a + Number , I : Iterator < Item = ( & ' a Candidate , & ' a CountCard < ' a , N > ) > > ( candidates : I , cmd_opts : & STV ) {
for ( candidate , count_card ) in candidates {
2021-06-11 16:50:01 +02:00
if count_card . state = = CandidateState ::Elected {
2021-06-16 05:00:54 +02:00
if let Some ( kv ) = & count_card . keep_value {
println! ( " - {} : {:.dps$} ( {:.dps$} ) - ELECTED {} (kv = {:.dps2$} ) " , candidate . name , count_card . votes , count_card . transfers , count_card . order_elected , kv , dps = cmd_opts . pp_decimals , dps2 = max ( cmd_opts . pp_decimals , 2 ) ) ;
} else {
println! ( " - {} : {:.dps$} ( {:.dps$} ) - ELECTED {} " , candidate . name , count_card . votes , count_card . transfers , count_card . order_elected , dps = cmd_opts . pp_decimals ) ;
}
2021-06-11 16:50:01 +02:00
} else if count_card . state = = CandidateState ::Excluded {
2021-05-28 19:01:07 +02:00
// If --hide-excluded, hide unless nonzero votes or nonzero transfers
if ! cmd_opts . hide_excluded | | ! count_card . votes . is_zero ( ) | | ! count_card . transfers . is_zero ( ) {
println! ( " - {} : {:.dps$} ( {:.dps$} ) - Excluded {} " , candidate . name , count_card . votes , count_card . transfers , - count_card . order_elected , dps = cmd_opts . pp_decimals ) ;
}
2021-06-11 16:50:01 +02:00
} else if count_card . state = = CandidateState ::Withdrawn {
if ! cmd_opts . hide_excluded | | ! count_card . votes . is_zero ( ) | | ! count_card . transfers . is_zero ( ) {
println! ( " - {} : {:.dps$} ( {:.dps$} ) - Withdrawn " , candidate . name , count_card . votes , count_card . transfers , dps = cmd_opts . pp_decimals ) ;
}
2021-05-28 19:01:07 +02:00
} else {
println! ( " - {} : {:.dps$} ( {:.dps$} ) " , candidate . name , count_card . votes , count_card . transfers , dps = cmd_opts . pp_decimals ) ;
}
}
}
fn print_stage < N : Number > ( stage_num : usize , result : & StageResult < N > , cmd_opts : & STV ) {
// Print stage details
match result . kind {
None = > { println! ( " {} . {} " , stage_num , result . title ) ; }
Some ( kind ) = > { println! ( " {} . {} {} " , stage_num , kind , result . title ) ; }
} ;
println! ( " {} " , result . logs . join ( " " ) ) ;
let state = result . state . as_ref ( ) ;
// Print candidates
if cmd_opts . sort_votes {
// Sort by votes if requested
let mut candidates : Vec < ( & Candidate , & CountCard < N > ) > = state . candidates . iter ( )
. map ( | ( c , cc ) | ( * c , cc ) ) . collect ( ) ;
// First sort by order of election (as a tie-breaker, if votes are equal)
candidates . sort_unstable_by ( | a , b | b . 1. order_elected . partial_cmp ( & a . 1. order_elected ) . unwrap ( ) ) ;
// Then sort by votes
candidates . sort_by ( | a , b | a . 1. votes . partial_cmp ( & b . 1. votes ) . unwrap ( ) ) ;
print_candidates ( candidates . into_iter ( ) . rev ( ) , cmd_opts ) ;
} else {
let candidates = state . election . candidates . iter ( )
. map ( | c | ( c , state . candidates . get ( c ) . unwrap ( ) ) ) ;
print_candidates ( candidates , cmd_opts ) ;
}
// Print summary rows
println! ( " Exhausted: {:.dps$} ( {:.dps$} ) " , state . exhausted . votes , state . exhausted . transfers , dps = cmd_opts . pp_decimals ) ;
println! ( " Loss by fraction: {:.dps$} ( {:.dps$} ) " , state . loss_fraction . votes , state . loss_fraction . transfers , dps = cmd_opts . pp_decimals ) ;
2021-05-29 15:25:27 +02:00
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 ;
println! ( " Total votes: {:.dps$} " , total_vote , dps = cmd_opts . pp_decimals ) ;
2021-06-07 12:52:18 +02:00
println! ( " Quota: {:.dps$} " , state . quota . as_ref ( ) . unwrap ( ) , dps = cmd_opts . pp_decimals ) ;
if cmd_opts . quota_mode = = " ers97 " {
println! ( " Vote required for election: {:.dps$} " , state . vote_required_election . as_ref ( ) . unwrap ( ) , dps = cmd_opts . pp_decimals ) ;
}
2021-05-28 19:01:07 +02:00
println! ( " " ) ;
}
fn make_and_print_result < N : Number > ( stage_num : usize , state : & CountState < N > , cmd_opts : & STV ) {
let result = StageResult {
kind : state . kind ,
title : & state . title ,
logs : state . logger . render ( ) ,
state : CountStateOrRef ::from ( & state ) ,
} ;
print_stage ( stage_num , & result , & cmd_opts ) ;
}