2021-05-28 19:58:40 +10:00
/* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li ( RunasSudo )
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https ://www.gnu.org/licenses/>.
* /
#![ allow(mutable_borrow_reservation_conflict) ]
2021-05-30 18:28:39 +10:00
//#[cfg(target_arch = "wasm32")]
pub mod wasm ;
2021-05-28 19:58:40 +10:00
use crate ::numbers ::Number ;
use crate ::election ::{ Candidate , CandidateState , CountCard , CountState , Parcel , Vote } ;
2021-05-31 22:25:53 +10:00
use wasm_bindgen ::prelude ::wasm_bindgen ;
2021-05-28 19:58:40 +10:00
use std ::collections ::HashMap ;
2021-05-30 02:28:52 +10:00
use std ::ops ;
2021-05-28 19:58:40 +10:00
2021-05-31 22:25:53 +10:00
#[ wasm_bindgen ]
pub struct STVOptions {
2021-06-01 21:20:38 +10:00
pub round_tvs : Option < usize > ,
pub round_weights : Option < usize > ,
2021-05-29 17:51:45 +10:00
pub round_votes : Option < usize > ,
2021-06-01 21:20:38 +10:00
pub round_quota : Option < usize > ,
2021-05-31 22:25:53 +10:00
pub surplus : SurplusMethod ,
2021-06-01 18:57:56 +10:00
pub surplus_order : SurplusOrder ,
2021-05-31 23:17:21 +10:00
pub transferable_only : bool ,
2021-05-31 22:25:53 +10:00
pub exclusion : ExclusionMethod ,
pub pp_decimals : usize ,
}
2021-06-01 19:04:03 +10:00
#[ wasm_bindgen ]
impl STVOptions {
pub fn new (
2021-06-01 21:20:38 +10:00
round_tvs : Option < usize > ,
round_weights : Option < usize > ,
2021-06-01 19:04:03 +10:00
round_votes : Option < usize > ,
2021-06-01 21:20:38 +10:00
round_quota : Option < usize > ,
2021-06-01 19:04:03 +10:00
surplus : & str ,
surplus_order : & str ,
transferable_only : bool ,
exclusion : & str ,
pp_decimals : usize ,
) -> Self {
return STVOptions {
2021-06-01 21:20:38 +10:00
round_tvs ,
round_weights ,
round_votes ,
round_quota ,
2021-06-01 19:04:03 +10:00
surplus : match surplus {
" wig " = > SurplusMethod ::WIG ,
" uig " = > SurplusMethod ::UIG ,
" eg " = > SurplusMethod ::EG ,
" meek " = > SurplusMethod ::Meek ,
_ = > panic! ( " Invalid --surplus " ) ,
} ,
surplus_order : match surplus_order {
" by_size " = > SurplusOrder ::BySize ,
" by_order " = > SurplusOrder ::ByOrder ,
_ = > panic! ( " Invalid --surplus-order " ) ,
} ,
2021-06-01 21:20:38 +10:00
transferable_only ,
2021-06-01 19:04:03 +10:00
exclusion : match exclusion {
" single_stage " = > ExclusionMethod ::SingleStage ,
" by_value " = > ExclusionMethod ::ByValue ,
" parcels_by_order " = > ExclusionMethod ::ParcelsByOrder ,
_ = > panic! ( " Invalid --exclusion " ) ,
} ,
2021-06-01 21:20:38 +10:00
pp_decimals ,
2021-06-01 19:04:03 +10:00
} ;
}
}
2021-05-31 22:25:53 +10:00
#[ wasm_bindgen ]
#[ derive(Clone, Copy) ]
pub enum SurplusMethod {
WIG ,
UIG ,
EG ,
Meek ,
2021-05-29 17:51:45 +10:00
}
2021-06-01 18:57:56 +10:00
#[ wasm_bindgen ]
#[ derive(Clone, Copy) ]
pub enum SurplusOrder {
BySize ,
ByOrder ,
}
2021-05-31 22:25:53 +10:00
#[ wasm_bindgen ]
#[ derive(Clone, Copy) ]
pub enum ExclusionMethod {
SingleStage ,
ByValue ,
ParcelsByOrder ,
}
pub fn count_init < N : Number > ( mut state : & mut CountState < '_ , N > , opts : & STVOptions ) {
2021-05-29 17:51:45 +10:00
distribute_first_preferences ( & mut state ) ;
2021-05-31 22:25:53 +10:00
calculate_quota ( & mut state , opts ) ;
2021-05-29 17:51:45 +10:00
elect_meeting_quota ( & mut state ) ;
}
pub fn count_one_stage < N : Number > ( mut state : & mut CountState < '_ , N > , opts : & STVOptions ) -> bool
where
2021-05-30 02:28:52 +10:00
for < ' r > & ' r N : ops ::Sub < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Neg < Output = N > ,
2021-05-29 17:51:45 +10:00
{
state . logger . entries . clear ( ) ;
state . step_all ( ) ;
// Finish count
if finished_before_stage ( & state ) {
return true ;
}
// Continue exclusions
if continue_exclusion ( & mut state , & opts ) {
elect_meeting_quota ( & mut state ) ;
return false ;
}
// Distribute surpluses
if distribute_surpluses ( & mut state , & opts ) {
elect_meeting_quota ( & mut state ) ;
return false ;
}
// Attempt bulk election
if bulk_elect ( & mut state ) {
elect_meeting_quota ( & mut state ) ;
return false ;
}
// Exclude lowest hopeful
if exclude_hopefuls ( & mut state , & opts ) {
elect_meeting_quota ( & mut state ) ;
return false ;
}
todo! ( ) ;
}
2021-05-28 19:58:40 +10:00
struct NextPreferencesResult < ' a , N > {
candidates : HashMap < & ' a Candidate , NextPreferencesEntry < ' a , N > > ,
exhausted : NextPreferencesEntry < ' a , N > ,
total_ballots : N ,
2021-05-30 02:28:52 +10:00
total_votes : N ,
2021-05-28 19:58:40 +10:00
}
struct NextPreferencesEntry < ' a , N > {
//count_card: Option<&'a CountCard<'a, N>>,
votes : Vec < Vote < ' a , N > > ,
num_ballots : N ,
num_votes : N ,
}
fn next_preferences < ' a , N : Number > ( state : & CountState < ' a , N > , votes : Vec < Vote < ' a , N > > ) -> NextPreferencesResult < ' a , N > {
let mut result = NextPreferencesResult {
candidates : HashMap ::new ( ) ,
exhausted : NextPreferencesEntry {
votes : Vec ::new ( ) ,
num_ballots : N ::new ( ) ,
num_votes : N ::new ( ) ,
} ,
total_ballots : N ::new ( ) ,
2021-05-30 02:28:52 +10:00
total_votes : N ::new ( ) ,
2021-05-28 19:58:40 +10:00
} ;
for mut vote in votes . into_iter ( ) {
result . total_ballots + = & vote . ballot . orig_value ;
2021-05-30 02:28:52 +10:00
result . total_votes + = & vote . value ;
2021-05-28 19:58:40 +10:00
let mut next_candidate = None ;
for ( i , preference ) in vote . ballot . preferences . iter ( ) . enumerate ( ) . skip ( vote . up_to_pref ) {
let candidate = & state . election . candidates [ * preference ] ;
let count_card = state . candidates . get ( candidate ) . unwrap ( ) ;
if let CandidateState ::HOPEFUL | CandidateState ::GUARDED = count_card . state {
next_candidate = Some ( candidate ) ;
vote . up_to_pref = i + 1 ;
break ;
}
}
// Have to structure like this to satisfy Rust's borrow checker
if let Some ( candidate ) = next_candidate {
if result . candidates . contains_key ( candidate ) {
let entry = result . candidates . get_mut ( candidate ) . unwrap ( ) ;
entry . num_ballots + = & vote . ballot . orig_value ;
entry . num_votes + = & vote . value ;
entry . votes . push ( vote ) ;
} else {
let entry = NextPreferencesEntry {
num_ballots : vote . ballot . orig_value . clone ( ) ,
num_votes : vote . value . clone ( ) ,
votes : vec ! [ vote ] ,
} ;
result . candidates . insert ( candidate , entry ) ;
}
} else {
result . exhausted . num_ballots + = & vote . ballot . orig_value ;
result . exhausted . num_votes + = & vote . value ;
result . exhausted . votes . push ( vote ) ;
}
}
return result ;
}
2021-05-29 17:51:45 +10:00
fn distribute_first_preferences < N : Number > ( state : & mut CountState < N > ) {
2021-05-28 22:37:18 +10:00
let votes = state . election . ballots . iter ( ) . map ( | b | Vote {
ballot : b ,
value : b . orig_value . clone ( ) ,
up_to_pref : 0 ,
} ) . collect ( ) ;
2021-05-28 19:58:40 +10:00
let result = next_preferences ( state , votes ) ;
// Transfer candidate votes
for ( candidate , entry ) in result . candidates . into_iter ( ) {
let parcel = entry . votes as Parcel < N > ;
let count_card = state . candidates . get_mut ( candidate ) . unwrap ( ) ;
count_card . parcels . push ( parcel ) ;
count_card . transfer ( & entry . num_votes ) ;
}
// Transfer exhausted votes
let parcel = result . exhausted . votes as Parcel < N > ;
state . exhausted . parcels . push ( parcel ) ;
state . exhausted . transfer ( & result . exhausted . num_votes ) ;
2021-05-29 01:22:46 +10:00
state . kind = None ;
state . title = " First preferences " . to_string ( ) ;
2021-05-29 02:13:47 +10:00
state . logger . log_literal ( " First preferences distributed. " . to_string ( ) ) ;
2021-05-28 19:58:40 +10:00
}
2021-05-31 22:25:53 +10:00
fn calculate_quota < N : Number > ( state : & mut CountState < N > , opts : & STVOptions ) {
2021-05-29 01:22:46 +10:00
let mut log = String ::new ( ) ;
2021-05-28 19:58:40 +10:00
// Calculate the total vote
2021-05-28 22:37:18 +10:00
state . quota = state . candidates . values ( ) . fold ( N ::zero ( ) , | acc , cc | { acc + & cc . votes } ) ;
2021-05-31 22:25:53 +10:00
log . push_str ( format! ( " {:.dps$} usable votes, so the quota is " , state . quota , dps = opts . pp_decimals ) . as_str ( ) ) ;
2021-05-28 19:58:40 +10:00
// TODO: Different quotas
state . quota / = N ::from ( state . election . seats + 1 ) ;
2021-06-01 21:20:38 +10:00
// Increment to next available increment
if let Some ( dps ) = opts . round_quota {
let mut factor = N ::from ( 10 ) ;
factor . pow_assign ( dps as i32 ) ;
state . quota * = & factor ;
state . quota . floor_mut ( 0 ) ;
state . quota + = N ::one ( ) ;
state . quota / = factor ;
}
2021-05-31 22:25:53 +10:00
log . push_str ( format! ( " {:.dps$} . " , state . quota , dps = opts . pp_decimals ) . as_str ( ) ) ;
2021-05-29 01:22:46 +10:00
2021-05-29 02:13:47 +10:00
state . logger . log_literal ( log ) ;
2021-05-28 19:58:40 +10:00
}
fn meets_quota < N : Number > ( quota : & N , count_card : & CountCard < N > ) -> bool {
// TODO: Different quota rules
return count_card . votes > = * quota ;
}
2021-05-29 17:51:45 +10:00
fn elect_meeting_quota < N : Number > ( state : & mut CountState < N > ) {
2021-05-28 22:37:18 +10:00
let quota = & state . quota ; // Have to do this or else the borrow checker gets confused
let mut cands_meeting_quota : Vec < ( & & Candidate , & mut CountCard < N > ) > = state . candidates . iter_mut ( )
. filter ( | ( _ , cc ) | cc . state = = CandidateState ::HOPEFUL & & meets_quota ( quota , cc ) )
. collect ( ) ;
2021-05-28 19:58:40 +10:00
if cands_meeting_quota . len ( ) > 0 {
// Sort by votes
cands_meeting_quota . sort_unstable_by ( | a , b | a . 1. votes . partial_cmp ( & b . 1. votes ) . unwrap ( ) ) ;
// Declare elected in descending order of votes
2021-05-29 01:22:46 +10:00
for ( candidate , count_card ) in cands_meeting_quota . into_iter ( ) . rev ( ) {
2021-05-28 19:58:40 +10:00
count_card . state = CandidateState ::ELECTED ;
state . num_elected + = 1 ;
count_card . order_elected = state . num_elected as isize ;
2021-05-29 02:13:47 +10:00
state . logger . log_smart (
" {} meets the quota and is elected. " ,
" {} meet the quota and are elected. " ,
vec! [ & candidate . name ]
) ;
2021-05-28 19:58:40 +10:00
}
}
}
2021-05-29 17:51:45 +10:00
fn distribute_surpluses < N : Number > ( state : & mut CountState < N > , opts : & STVOptions ) -> bool
2021-05-29 00:43:58 +10:00
where
2021-05-30 02:28:52 +10:00
for < ' r > & ' r N : ops ::Sub < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Neg < Output = N >
2021-05-29 00:43:58 +10:00
{
2021-05-28 22:37:18 +10:00
let mut has_surplus : Vec < ( & & Candidate , & CountCard < N > ) > = state . candidates . iter ( )
. filter ( | ( _ , cc ) | cc . votes > state . quota )
. collect ( ) ;
2021-05-28 19:58:40 +10:00
if has_surplus . len ( ) > 0 {
2021-06-01 18:57:56 +10:00
match opts . surplus_order {
SurplusOrder ::BySize = > {
2021-06-01 21:20:38 +10:00
// Compare b with a to sort high-low
has_surplus . sort_unstable_by ( | a , b | b . 1. votes . partial_cmp ( & a . 1. votes ) . unwrap ( ) ) ;
2021-06-01 18:57:56 +10:00
}
SurplusOrder ::ByOrder = > {
has_surplus . sort_unstable_by ( | a , b | a . 1. order_elected . partial_cmp ( & b . 1. order_elected ) . unwrap ( ) ) ;
}
}
2021-05-28 19:58:40 +10:00
// Distribute top candidate's surplus
// TODO: Handle ties
let elected_candidate = has_surplus . first_mut ( ) . unwrap ( ) . 0 ;
2021-05-29 17:51:45 +10:00
distribute_surplus ( state , & opts , elected_candidate ) ;
2021-05-28 19:58:40 +10:00
return true ;
}
return false ;
}
2021-05-31 23:17:21 +10:00
/// Return the denominator of the transfer value
fn calculate_transfer_denom < N : Number > ( surplus : & N , result : & NextPreferencesResult < N > , transferable_votes : & N , weighted : bool , transferable_only : bool ) -> Option < N >
where
for < ' r > & ' r N : ops ::Sub < & ' r N , Output = N >
{
if transferable_only {
let total_units = if weighted { & result . total_votes } else { & result . total_ballots } ;
let exhausted_units = if weighted { & result . exhausted . num_votes } else { & result . exhausted . num_ballots } ;
let transferable_units = total_units - exhausted_units ;
if transferable_votes > surplus {
return Some ( transferable_units ) ;
} else {
return None ;
}
} else {
if weighted {
return Some ( result . total_votes . clone ( ) ) ;
} else {
return Some ( result . total_ballots . clone ( ) ) ;
}
}
}
2021-06-01 21:20:38 +10:00
fn reweight_vote < N : Number > (
num_votes : & N ,
num_ballots : & N ,
surplus : & N ,
weighted : bool ,
transfer_value : & Option < N > ,
transfer_denom : & Option < N > ,
round_tvs : Option < usize > ,
rounding : Option < usize > ) -> N
{
let mut result ;
2021-05-31 23:17:21 +10:00
match transfer_denom {
Some ( v ) = > {
2021-06-01 21:20:38 +10:00
if let Some ( _ ) = round_tvs {
// Rounding requested: use the rounded transfer value
if weighted {
result = num_votes . clone ( ) * transfer_value . as_ref ( ) . unwrap ( ) ;
} else {
result = num_ballots . clone ( ) * transfer_value . as_ref ( ) . unwrap ( ) ;
}
2021-05-31 23:17:21 +10:00
} else {
2021-06-01 21:20:38 +10:00
// Avoid unnecessary rounding error by first multiplying by the surplus
if weighted {
result = num_votes . clone ( ) * surplus / v ;
} else {
result = num_ballots . clone ( ) * surplus / v ;
}
2021-05-31 23:17:21 +10:00
}
}
None = > {
2021-06-01 21:20:38 +10:00
result = num_votes . clone ( ) ;
2021-05-31 23:17:21 +10:00
}
}
2021-06-01 21:20:38 +10:00
// Round down if requested
if let Some ( dps ) = rounding {
result . floor_mut ( dps ) ;
}
return result ;
2021-05-31 23:17:21 +10:00
}
2021-05-29 17:51:45 +10:00
fn distribute_surplus < N : Number > ( state : & mut CountState < N > , opts : & STVOptions , elected_candidate : & Candidate )
2021-05-29 00:43:58 +10:00
where
2021-05-30 02:28:52 +10:00
for < ' r > & ' r N : ops ::Sub < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Neg < Output = N >
2021-05-29 00:43:58 +10:00
{
2021-05-28 19:58:40 +10:00
let count_card = state . candidates . get ( elected_candidate ) . unwrap ( ) ;
let surplus = & count_card . votes - & state . quota ;
2021-05-31 22:25:53 +10:00
let votes ;
match opts . surplus {
SurplusMethod ::WIG | SurplusMethod ::UIG = > {
// Inclusive Gregory
votes = state . candidates . get ( elected_candidate ) . unwrap ( ) . parcels . concat ( ) ;
}
SurplusMethod ::EG = > {
// Exclusive Gregory
// Should be safe to unwrap() - or else how did we get a quota!
votes = state . candidates . get_mut ( elected_candidate ) . unwrap ( ) . parcels . pop ( ) . unwrap ( ) ;
}
SurplusMethod ::Meek = > {
todo! ( ) ;
}
}
2021-05-28 19:58:40 +10:00
// Count next preferences
let result = next_preferences ( state , votes ) ;
2021-05-31 23:17:21 +10:00
state . kind = Some ( " Surplus of " ) ;
state . title = String ::from ( & elected_candidate . name ) ;
2021-05-28 19:58:40 +10:00
// Transfer candidate votes
2021-05-31 23:17:21 +10:00
// TODO: Refactor??
let is_weighted = match opts . surplus {
SurplusMethod ::WIG = > { true }
SurplusMethod ::UIG | SurplusMethod ::EG = > { false }
SurplusMethod ::Meek = > { todo! ( ) }
} ;
let transferable_votes = & result . total_votes - & result . exhausted . num_votes ;
let transfer_denom = calculate_transfer_denom ( & surplus , & result , & transferable_votes , is_weighted , opts . transferable_only ) ;
2021-06-01 21:20:38 +10:00
let mut transfer_value ;
2021-05-31 23:17:21 +10:00
match transfer_denom {
Some ( ref v ) = > {
2021-06-01 21:20:38 +10:00
transfer_value = Some ( surplus . clone ( ) / v ) ;
// Round down if requested
if let Some ( dps ) = opts . round_tvs {
transfer_value . as_mut ( ) . unwrap ( ) . floor_mut ( dps ) ;
}
state . logger . log_literal ( format! ( " Surplus of {} distributed at value {:.dps$} . " , elected_candidate . name , transfer_value . as_ref ( ) . unwrap ( ) , dps = opts . pp_decimals ) ) ;
2021-05-31 22:25:53 +10:00
}
2021-05-31 23:17:21 +10:00
None = > {
2021-06-01 21:20:38 +10:00
transfer_value = None ;
2021-05-31 23:17:21 +10:00
state . logger . log_literal ( format! ( " Surplus of {} distributed at values received. " , elected_candidate . name ) ) ;
2021-05-31 22:25:53 +10:00
}
}
2021-05-29 01:22:46 +10:00
2021-05-29 00:43:58 +10:00
let mut checksum = N ::new ( ) ;
2021-05-28 19:58:40 +10:00
for ( candidate , entry ) in result . candidates . into_iter ( ) {
let mut parcel = entry . votes as Parcel < N > ;
// Reweight votes
for vote in parcel . iter_mut ( ) {
2021-06-01 21:20:38 +10:00
vote . value = reweight_vote ( & vote . value , & vote . ballot . orig_value , & surplus , is_weighted , & transfer_value , & transfer_denom , opts . round_tvs , opts . round_weights ) ;
2021-05-28 19:58:40 +10:00
}
let count_card = state . candidates . get_mut ( candidate ) . unwrap ( ) ;
count_card . parcels . push ( parcel ) ;
2021-06-01 21:20:38 +10:00
let candidate_transfers = reweight_vote ( & entry . num_votes , & entry . num_ballots , & surplus , is_weighted , & transfer_value , & transfer_denom , opts . round_tvs , opts . round_votes ) ;
2021-05-29 00:43:58 +10:00
count_card . transfer ( & candidate_transfers ) ;
checksum + = candidate_transfers ;
2021-05-28 19:58:40 +10:00
}
// Transfer exhausted votes
let parcel = result . exhausted . votes as Parcel < N > ;
state . exhausted . parcels . push ( parcel ) ;
2021-05-29 00:43:58 +10:00
2021-05-31 23:17:21 +10:00
let mut exhausted_transfers ;
if opts . transferable_only {
if transferable_votes > surplus {
// No ballots exhaust
exhausted_transfers = N ::new ( ) ;
} else {
exhausted_transfers = & surplus - & transferable_votes ;
}
} else {
2021-06-01 21:20:38 +10:00
exhausted_transfers = reweight_vote ( & result . exhausted . num_votes , & result . exhausted . num_ballots , & surplus , is_weighted , & transfer_value , & transfer_denom , opts . round_tvs , opts . round_votes ) ;
2021-05-31 23:17:21 +10:00
}
2021-05-29 17:51:45 +10:00
if let Some ( dps ) = opts . round_votes {
exhausted_transfers . floor_mut ( dps ) ;
}
2021-05-29 00:43:58 +10:00
state . exhausted . transfer ( & exhausted_transfers ) ;
checksum + = exhausted_transfers ;
2021-05-28 19:58:40 +10:00
// Finalise candidate votes
let count_card = state . candidates . get_mut ( elected_candidate ) . unwrap ( ) ;
2021-05-29 00:43:58 +10:00
count_card . transfers = - & surplus ;
2021-05-28 19:58:40 +10:00
count_card . votes . assign ( & state . quota ) ;
2021-05-29 00:43:58 +10:00
checksum - = surplus ;
// Update loss by fraction
state . loss_fraction . transfer ( & - checksum ) ;
2021-05-28 19:58:40 +10:00
}
2021-05-29 17:51:45 +10:00
fn bulk_elect < N : Number > ( state : & mut CountState < N > ) -> bool {
2021-05-28 19:58:40 +10:00
if state . election . candidates . len ( ) - state . num_excluded < = state . election . seats {
2021-05-29 01:22:46 +10:00
state . kind = None ;
state . title = " Bulk election " . to_string ( ) ;
2021-05-28 19:58:40 +10:00
// Bulk elect all remaining candidates
2021-05-28 22:37:18 +10:00
let mut hopefuls : Vec < ( & & Candidate , & mut CountCard < N > ) > = state . candidates . iter_mut ( )
. filter ( | ( _ , cc ) | cc . state = = CandidateState ::HOPEFUL )
. collect ( ) ;
2021-05-28 19:58:40 +10:00
// TODO: Handle ties
hopefuls . sort_unstable_by ( | a , b | a . 1. votes . partial_cmp ( & b . 1. votes ) . unwrap ( ) ) ;
2021-05-29 01:22:46 +10:00
for ( candidate , count_card ) in hopefuls . into_iter ( ) {
2021-05-28 19:58:40 +10:00
count_card . state = CandidateState ::ELECTED ;
state . num_elected + = 1 ;
count_card . order_elected = state . num_elected as isize ;
2021-05-29 01:22:46 +10:00
2021-05-29 02:13:47 +10:00
state . logger . log_smart (
" {} is elected to fill the remaining vacancy. " ,
" {} are elected to fill the remaining vacancies. " ,
vec! [ & candidate . name ]
) ;
2021-05-28 19:58:40 +10:00
}
return true ;
}
return false ;
}
2021-05-30 02:28:52 +10:00
fn exclude_hopefuls < N : Number > ( state : & mut CountState < N > , opts : & STVOptions ) -> bool
where
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
{
2021-05-28 22:37:18 +10:00
let mut hopefuls : Vec < ( & & Candidate , & CountCard < N > ) > = state . candidates . iter ( )
. filter ( | ( _ , cc ) | cc . state = = CandidateState ::HOPEFUL )
. collect ( ) ;
2021-05-28 19:58:40 +10:00
// Sort by votes
// TODO: Handle ties
hopefuls . sort_unstable_by ( | a , b | a . 1. votes . partial_cmp ( & b . 1. votes ) . unwrap ( ) ) ;
// Exclude lowest ranked candidate
let excluded_candidate = hopefuls . first ( ) . unwrap ( ) . 0 ;
2021-05-29 01:22:46 +10:00
state . kind = Some ( " Exclusion of " ) ;
state . title = String ::from ( & excluded_candidate . name ) ;
2021-05-29 02:13:47 +10:00
state . logger . log_smart (
" No surpluses to distribute, so {} is excluded. " ,
" No surpluses to distribute, so {} are excluded. " ,
vec! [ & excluded_candidate . name ]
) ;
2021-05-29 01:22:46 +10:00
2021-05-29 17:51:45 +10:00
exclude_candidate ( state , opts , excluded_candidate ) ;
2021-05-28 19:58:40 +10:00
return true ;
}
2021-05-30 02:28:52 +10:00
fn continue_exclusion < N : Number > ( state : & mut CountState < N > , opts : & STVOptions ) -> bool
where
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
{
// Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers
2021-05-28 22:37:18 +10:00
let mut excluded_with_votes : Vec < ( & & Candidate , & CountCard < N > ) > = state . candidates . iter ( )
2021-05-30 02:28:52 +10:00
//.filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && !cc.votes.is_zero())
. filter ( | ( _ , cc ) | cc . state = = CandidateState ::EXCLUDED & & cc . parcels . iter ( ) . any ( | p | p . len ( ) > 0 ) )
2021-05-28 22:37:18 +10:00
. collect ( ) ;
2021-05-28 19:58:40 +10:00
if excluded_with_votes . len ( ) > 0 {
excluded_with_votes . sort_unstable_by ( | a , b | a . 1. order_elected . partial_cmp ( & b . 1. order_elected ) . unwrap ( ) ) ;
let excluded_candidate = excluded_with_votes . first ( ) . unwrap ( ) . 0 ;
2021-05-29 01:22:46 +10:00
state . kind = Some ( " Exclusion of " ) ;
state . title = String ::from ( & excluded_candidate . name ) ;
2021-05-29 02:13:47 +10:00
state . logger . log_smart (
" Continuing exclusion of {}. " ,
" Continuing exclusion of {}. " ,
vec! [ & excluded_candidate . name ]
) ;
2021-05-29 01:22:46 +10:00
2021-05-29 17:51:45 +10:00
exclude_candidate ( state , opts , excluded_candidate ) ;
2021-05-28 19:58:40 +10:00
return true ;
}
return false ;
}
2021-05-30 02:28:52 +10:00
fn exclude_candidate < N : Number > ( state : & mut CountState < N > , opts : & STVOptions , excluded_candidate : & Candidate )
where
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
{
2021-05-28 19:58:40 +10:00
let count_card = state . candidates . get_mut ( excluded_candidate ) . unwrap ( ) ;
2021-05-30 02:28:52 +10:00
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
if count_card . state ! = CandidateState ::EXCLUDED {
count_card . state = CandidateState ::EXCLUDED ;
state . num_excluded + = 1 ;
count_card . order_elected = - ( state . num_excluded as isize ) ;
}
// Determine votes to transfer in this stage
let mut votes ;
2021-05-31 22:25:53 +10:00
let votes_remain ;
2021-05-30 02:28:52 +10:00
2021-05-31 22:25:53 +10:00
match opts . exclusion {
ExclusionMethod ::SingleStage = > {
// Exclude in one round
votes = count_card . parcels . concat ( ) ;
votes_remain = false ;
}
ExclusionMethod ::ByValue = > {
// Exclude by value
let all_votes = count_card . parcels . concat ( ) ;
// TODO: Write a multiple min/max function
let min_value = all_votes . iter ( ) . map ( | v | & v . value / & v . ballot . orig_value ) . max ( ) . unwrap ( ) ;
votes = Vec ::new ( ) ;
let mut remaining_votes = Vec ::new ( ) ;
// This could be implemented using Vec.drain_filter, but that is experimental currently
for vote in all_votes . into_iter ( ) {
if & vote . value / & vote . ballot . orig_value = = min_value {
votes . push ( vote ) ;
} else {
remaining_votes . push ( vote ) ;
}
2021-05-30 02:28:52 +10:00
}
2021-05-31 22:25:53 +10:00
votes_remain = remaining_votes . len ( ) > 0 ;
// Leave remaining votes with candidate (as one parcel)
count_card . parcels = vec! [ remaining_votes ] ;
}
ExclusionMethod ::ParcelsByOrder = > {
// Exclude by parcel by order
votes = count_card . parcels . remove ( 0 ) ;
votes_remain = count_card . parcels . len ( ) > 0 ;
2021-05-30 02:28:52 +10:00
}
}
2021-05-29 00:43:58 +10:00
let mut checksum = N ::new ( ) ;
2021-05-30 18:27:28 +10:00
if votes . len ( ) > 0 {
let value = & votes [ 0 ] . value / & votes [ 0 ] . ballot . orig_value ;
2021-05-28 19:58:40 +10:00
2021-05-30 18:27:28 +10:00
// Count next preferences
let result = next_preferences ( state , votes ) ;
2021-05-31 22:25:53 +10:00
if let ExclusionMethod ::SingleStage = opts . exclusion {
state . logger . log_literal ( format! ( " Transferring {:.0} ballot papers, totalling {:.dps$} votes. " , result . total_ballots , result . total_votes , dps = opts . pp_decimals ) ) ;
} else {
state . logger . log_literal ( format! ( " Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps$} . " , result . total_ballots , result . total_votes , value , dps = opts . pp_decimals ) ) ;
2021-05-30 18:27:28 +10:00
}
// Transfer candidate votes
for ( candidate , entry ) in result . candidates . into_iter ( ) {
let parcel = entry . votes as Parcel < N > ;
let count_card = state . candidates . get_mut ( candidate ) . unwrap ( ) ;
count_card . parcels . push ( parcel ) ;
// Round transfers
let mut candidate_transfers = entry . num_votes ;
if let Some ( dps ) = opts . round_votes {
candidate_transfers . floor_mut ( dps ) ;
}
count_card . transfer ( & candidate_transfers ) ;
checksum + = candidate_transfers ;
}
// Transfer exhausted votes
let parcel = result . exhausted . votes as Parcel < N > ;
state . exhausted . parcels . push ( parcel ) ;
let mut exhausted_transfers = result . exhausted . num_votes ;
2021-05-29 17:51:45 +10:00
if let Some ( dps ) = opts . round_votes {
2021-05-30 18:27:28 +10:00
exhausted_transfers . floor_mut ( dps ) ;
}
state . exhausted . transfer ( & exhausted_transfers ) ;
checksum + = exhausted_transfers ;
2021-05-31 22:25:53 +10:00
if votes_remain {
2021-05-30 18:27:28 +10:00
// Subtract from candidate tally
let count_card = state . candidates . get_mut ( excluded_candidate ) . unwrap ( ) ;
checksum - = & result . total_votes ;
count_card . transfer ( & - result . total_votes ) ;
// By definition, there is no loss by fraction
2021-05-29 17:51:45 +10:00
}
}
2021-05-28 19:58:40 +10:00
2021-05-31 22:25:53 +10:00
if ! votes_remain {
2021-05-30 02:28:52 +10:00
// Finalise candidate votes
let count_card = state . candidates . get_mut ( excluded_candidate ) . unwrap ( ) ;
checksum - = & count_card . votes ;
count_card . transfers = - count_card . votes . clone ( ) ;
count_card . votes = N ::new ( ) ;
// Update loss by fraction
state . loss_fraction . transfer ( & - checksum ) ;
2021-05-31 22:25:53 +10:00
if let ExclusionMethod ::SingleStage = opts . exclusion {
} else {
2021-05-30 02:28:52 +10:00
state . logger . log_literal ( " Exclusion complete. " . to_string ( ) ) ;
}
}
2021-05-28 19:58:40 +10:00
}
2021-05-29 17:51:45 +10:00
fn finished_before_stage < N : Number > ( state : & CountState < N > ) -> bool {
2021-05-28 19:58:40 +10:00
if state . num_elected > = state . election . seats {
return true ;
}
return false ;
}