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-06-12 02:09:26 +10:00
use crate ::ties ::TieStrategy ;
2021-05-28 19:58:40 +10:00
2021-06-11 21:22:28 +10:00
use itertools ::Itertools ;
2021-05-31 22:25:53 +10:00
use wasm_bindgen ::prelude ::wasm_bindgen ;
2021-06-06 00:53:52 +10:00
use std ::cmp ::max ;
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-06-11 21:22:28 +10:00
pub sum_surplus_transfers : SumSurplusTransfersMode ,
2021-06-02 18:07:05 +10:00
pub quota : QuotaType ,
pub quota_criterion : QuotaCriterion ,
2021-06-07 20:52:18 +10:00
pub quota_mode : QuotaMode ,
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 ,
2021-06-08 22:22:43 +10:00
pub bulk_exclude : bool ,
2021-06-09 12:16:25 +10:00
pub defer_surpluses : bool ,
2021-05-31 22:25:53 +10:00
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-11 21:22:28 +10:00
sum_transfers : & str ,
2021-06-02 18:07:05 +10:00
quota : & str ,
quota_criterion : & str ,
2021-06-07 20:52:18 +10:00
quota_mode : & str ,
2021-06-01 19:04:03 +10:00
surplus : & str ,
surplus_order : & str ,
transferable_only : bool ,
exclusion : & str ,
2021-06-08 22:22:43 +10:00
bulk_exclude : bool ,
2021-06-09 12:16:25 +10:00
defer_surpluses : bool ,
2021-06-01 19:04:03 +10:00
pp_decimals : usize ,
) -> Self {
return STVOptions {
2021-06-01 21:20:38 +10:00
round_tvs ,
round_weights ,
round_votes ,
round_quota ,
2021-06-11 21:22:28 +10:00
sum_surplus_transfers : match sum_transfers {
" single_step " = > SumSurplusTransfersMode ::SingleStep ,
" by_value " = > SumSurplusTransfersMode ::ByValue ,
" per_ballot " = > SumSurplusTransfersMode ::PerBallot ,
_ = > panic! ( " Invalid --sum-transfers " ) ,
} ,
2021-06-02 18:07:05 +10:00
quota : match quota {
" droop " = > QuotaType ::Droop ,
" hare " = > QuotaType ::Hare ,
" droop_exact " = > QuotaType ::DroopExact ,
" hare_exact " = > QuotaType ::HareExact ,
_ = > panic! ( " Invalid --quota " ) ,
} ,
quota_criterion : match quota_criterion {
" geq " = > QuotaCriterion ::GreaterOrEqual ,
" gt " = > QuotaCriterion ::Greater ,
_ = > panic! ( " Invalid --quota-criterion " ) ,
} ,
2021-06-07 20:52:18 +10:00
quota_mode : match quota_mode {
" static " = > QuotaMode ::Static ,
" ers97 " = > QuotaMode ::ERS97 ,
_ = > panic! ( " Invalid --quota-mode " ) ,
} ,
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-08 22:22:43 +10:00
bulk_exclude ,
2021-06-09 12:16:25 +10:00
defer_surpluses ,
2021-06-01 21:20:38 +10:00
pp_decimals ,
2021-06-01 19:04:03 +10:00
} ;
}
}
2021-06-03 21:35:25 +10:00
impl STVOptions {
// Not exported to WebAssembly
pub fn describe < N : Number > ( & self ) -> String {
let mut flags = Vec ::new ( ) ;
flags . push ( N ::describe ( ) ) ;
if let Some ( dps ) = self . round_tvs { flags . push ( format! ( " --round-tvs {} " , dps ) ) ; }
if let Some ( dps ) = self . round_weights { flags . push ( format! ( " --round-weights {} " , dps ) ) ; }
if let Some ( dps ) = self . round_votes { flags . push ( format! ( " --round-votes {} " , dps ) ) ; }
if let Some ( dps ) = self . round_quota { flags . push ( format! ( " --round-quota {} " , dps ) ) ; }
if self . quota ! = QuotaType ::Droop { flags . push ( self . quota . describe ( ) ) ; }
if self . quota_criterion ! = QuotaCriterion ::GreaterOrEqual { flags . push ( self . quota_criterion . describe ( ) ) ; }
2021-06-07 20:52:18 +10:00
if self . quota_mode ! = QuotaMode ::Static { flags . push ( self . quota_mode . describe ( ) ) ; }
2021-06-03 21:35:25 +10:00
if self . surplus ! = SurplusMethod ::WIG { flags . push ( self . surplus . describe ( ) ) ; }
if self . surplus_order ! = SurplusOrder ::BySize { flags . push ( self . surplus_order . describe ( ) ) ; }
if self . transferable_only { flags . push ( " --transferable-only " . to_string ( ) ) ; }
if self . exclusion ! = ExclusionMethod ::SingleStage { flags . push ( self . exclusion . describe ( ) ) ; }
if self . pp_decimals ! = 2 { flags . push ( format! ( " --pp-decimals {} " , self . pp_decimals ) ) ; }
return flags . join ( " " ) ;
}
}
2021-06-11 21:22:28 +10:00
#[ wasm_bindgen ]
#[ derive(Clone, Copy) ]
#[ derive(PartialEq) ]
pub enum SumSurplusTransfersMode {
SingleStep ,
ByValue ,
PerBallot ,
}
2021-06-02 18:07:05 +10:00
#[ wasm_bindgen ]
#[ derive(Clone, Copy) ]
2021-06-03 21:35:25 +10:00
#[ derive(PartialEq) ]
2021-06-02 18:07:05 +10:00
pub enum QuotaType {
Droop ,
Hare ,
DroopExact ,
HareExact ,
}
2021-06-03 21:35:25 +10:00
impl QuotaType {
fn describe ( self ) -> String {
match self {
QuotaType ::Droop = > " --quota droop " ,
QuotaType ::Hare = > " --quota hare " ,
QuotaType ::DroopExact = > " --quota droop_exact " ,
QuotaType ::HareExact = > " --quota hare_exact " ,
} . to_string ( )
}
}
2021-06-02 18:07:05 +10:00
#[ wasm_bindgen ]
#[ derive(Clone, Copy) ]
2021-06-03 21:35:25 +10:00
#[ derive(PartialEq) ]
2021-06-02 18:07:05 +10:00
pub enum QuotaCriterion {
GreaterOrEqual ,
Greater ,
}
2021-06-03 21:35:25 +10:00
impl QuotaCriterion {
fn describe ( self ) -> String {
match self {
QuotaCriterion ::GreaterOrEqual = > " --quota-criterion geq " ,
QuotaCriterion ::Greater = > " --quota-criterion gt " ,
} . to_string ( )
}
}
2021-06-07 20:52:18 +10:00
#[ wasm_bindgen ]
#[ derive(Clone, Copy) ]
#[ derive(PartialEq) ]
pub enum QuotaMode {
Static ,
ERS97 ,
}
impl QuotaMode {
fn describe ( self ) -> String {
match self {
QuotaMode ::Static = > " --quota-mode static " ,
QuotaMode ::ERS97 = > " --quota-mode ers97 " ,
} . to_string ( )
}
}
2021-05-31 22:25:53 +10:00
#[ wasm_bindgen ]
#[ derive(Clone, Copy) ]
2021-06-03 21:35:25 +10:00
#[ derive(PartialEq) ]
2021-05-31 22:25:53 +10:00
pub enum SurplusMethod {
WIG ,
UIG ,
EG ,
Meek ,
2021-05-29 17:51:45 +10:00
}
2021-06-03 21:35:25 +10:00
impl SurplusMethod {
fn describe ( self ) -> String {
match self {
SurplusMethod ::WIG = > " --surplus wig " ,
SurplusMethod ::UIG = > " --surplus uig " ,
SurplusMethod ::EG = > " --surplus eg " ,
SurplusMethod ::Meek = > " --surplus meek " ,
} . to_string ( )
}
}
2021-06-01 18:57:56 +10:00
#[ wasm_bindgen ]
#[ derive(Clone, Copy) ]
2021-06-03 21:35:25 +10:00
#[ derive(PartialEq) ]
2021-06-01 18:57:56 +10:00
pub enum SurplusOrder {
BySize ,
ByOrder ,
}
2021-06-03 21:35:25 +10:00
impl SurplusOrder {
fn describe ( self ) -> String {
match self {
SurplusOrder ::BySize = > " --surplus-order by_size " ,
SurplusOrder ::ByOrder = > " --surplus-order by_order " ,
} . to_string ( )
}
}
2021-05-31 22:25:53 +10:00
#[ wasm_bindgen ]
#[ derive(Clone, Copy) ]
2021-06-03 21:35:25 +10:00
#[ derive(PartialEq) ]
2021-05-31 22:25:53 +10:00
pub enum ExclusionMethod {
SingleStage ,
ByValue ,
ParcelsByOrder ,
}
2021-06-03 21:35:25 +10:00
impl ExclusionMethod {
fn describe ( self ) -> String {
match self {
ExclusionMethod ::SingleStage = > " --exclusion single_stage " ,
ExclusionMethod ::ByValue = > " --exclusion by_value " ,
2021-06-07 20:52:18 +10:00
ExclusionMethod ::ParcelsByOrder = > " --exclusion parcels_by_order " ,
2021-06-03 21:35:25 +10:00
} . to_string ( )
}
}
2021-06-12 02:09:26 +10:00
#[ wasm_bindgen ]
#[ derive(Debug) ]
pub enum STVError {
RequireInput ,
}
2021-05-31 22:25:53 +10:00
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-06-02 18:07:05 +10:00
elect_meeting_quota ( & mut state , opts ) ;
2021-05-29 17:51:45 +10:00
}
2021-06-12 02:09:26 +10:00
pub fn count_one_stage < ' a , N : Number > ( mut state : & mut CountState < ' a , N > , opts : & STVOptions ) -> Result < bool , STVError >
2021-05-29 17:51:45 +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 ::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 ) {
2021-06-12 02:09:26 +10:00
return Ok ( true ) ;
2021-05-29 17:51:45 +10:00
}
// Continue exclusions
if continue_exclusion ( & mut state , & opts ) {
2021-06-07 20:52:18 +10:00
calculate_quota ( & mut state , opts ) ;
2021-06-02 18:07:05 +10:00
elect_meeting_quota ( & mut state , opts ) ;
2021-06-12 02:09:26 +10:00
return Ok ( false ) ;
2021-05-29 17:51:45 +10:00
}
// Distribute surpluses
2021-06-12 02:09:26 +10:00
if distribute_surpluses ( & mut state , & opts ) ? {
2021-06-07 20:52:18 +10:00
calculate_quota ( & mut state , opts ) ;
2021-06-02 18:07:05 +10:00
elect_meeting_quota ( & mut state , opts ) ;
2021-06-12 02:09:26 +10:00
return Ok ( false ) ;
2021-05-29 17:51:45 +10:00
}
// Attempt bulk election
if bulk_elect ( & mut state ) {
2021-06-12 02:09:26 +10:00
return Ok ( false ) ;
2021-05-29 17:51:45 +10:00
}
// Exclude lowest hopeful
2021-06-12 02:09:26 +10:00
if exclude_hopefuls ( & mut state , & opts ) ? {
2021-06-07 20:52:18 +10:00
calculate_quota ( & mut state , opts ) ;
2021-06-02 18:07:05 +10:00
elect_meeting_quota ( & mut state , opts ) ;
2021-06-12 02:09:26 +10:00
return Ok ( false ) ;
2021-05-29 17:51:45 +10:00
}
2021-06-12 02:09:26 +10:00
panic! ( " Count incomplete but unable to proceed " ) ;
2021-05-29 17:51:45 +10:00
}
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 ( ) ;
2021-06-12 00:50:01 +10:00
if let CandidateState ::Hopeful | CandidateState ::Guarded = count_card . state {
2021-05-28 19:58:40 +10:00
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-06-07 20:52:18 +10:00
fn total_to_quota < N : Number > ( mut total : N , seats : usize , opts : & STVOptions ) -> N {
2021-06-02 18:07:05 +10:00
match opts . quota {
QuotaType ::Droop | QuotaType ::DroopExact = > {
2021-06-07 20:52:18 +10:00
total / = N ::from ( seats + 1 ) ;
2021-06-02 18:07:05 +10:00
}
QuotaType ::Hare | QuotaType ::HareExact = > {
2021-06-07 20:52:18 +10:00
total / = N ::from ( seats ) ;
2021-06-02 18:07:05 +10:00
}
}
2021-05-28 19:58:40 +10:00
2021-06-01 21:20:38 +10:00
if let Some ( dps ) = opts . round_quota {
2021-06-02 18:07:05 +10:00
match opts . quota {
QuotaType ::Droop | QuotaType ::Hare = > {
// Increment to next available increment
let mut factor = N ::from ( 10 ) ;
factor . pow_assign ( dps as i32 ) ;
2021-06-07 20:52:18 +10:00
total * = & factor ;
total . floor_mut ( 0 ) ;
total + = N ::one ( ) ;
total / = factor ;
2021-06-02 18:07:05 +10:00
}
QuotaType ::DroopExact | QuotaType ::HareExact = > {
// Round up to next available increment if necessary
2021-06-07 20:52:18 +10:00
total . ceil_mut ( dps ) ;
2021-06-02 18:07:05 +10:00
}
}
2021-06-01 21:20:38 +10:00
}
2021-06-07 20:52:18 +10:00
return total ;
}
fn calculate_quota < N : Number > ( state : & mut CountState < N > , opts : & STVOptions ) {
// Calculate quota
if let None = state . quota {
let mut log = String ::new ( ) ;
// Calculate the total vote
let total_vote = state . candidates . values ( ) . fold ( N ::zero ( ) , | acc , cc | { acc + & cc . votes } ) ;
log . push_str ( format! ( " {:.dps$} usable votes, so the quota is " , total_vote , dps = opts . pp_decimals ) . as_str ( ) ) ;
let quota = total_to_quota ( total_vote , state . election . seats , opts ) ;
log . push_str ( format! ( " {:.dps$} . " , quota , dps = opts . pp_decimals ) . as_str ( ) ) ;
state . quota = Some ( quota ) ;
state . logger . log_literal ( log ) ;
}
2021-05-29 01:22:46 +10:00
2021-06-07 20:52:18 +10:00
if let QuotaMode ::ERS97 = opts . quota_mode {
// ERS97 rules
// -------------------------
// Reduce quota if allowable
if state . num_elected = = 0 {
let mut log = String ::new ( ) ;
// Calculate the total vote
let total_vote = state . candidates . values ( ) . fold ( N ::zero ( ) , | acc , cc | { acc + & cc . votes } ) ;
log . push_str ( format! ( " {:.dps$} usable votes, so the quota is reduced to " , total_vote , dps = opts . pp_decimals ) . as_str ( ) ) ;
let quota = total_to_quota ( total_vote , state . election . seats , opts ) ;
if & quota < state . quota . as_ref ( ) . unwrap ( ) {
log . push_str ( format! ( " {:.dps$} . " , quota , dps = opts . pp_decimals ) . as_str ( ) ) ;
state . quota = Some ( quota ) ;
state . logger . log_literal ( log ) ;
}
}
// ------------------------------------
// Calculate vote required for election
if state . num_elected < state . election . seats {
let mut log = String ::new ( ) ;
// Calculate total active vote
let total_active_vote = state . candidates . values ( ) . fold ( N ::zero ( ) , | acc , cc | {
match cc . state {
2021-06-12 00:50:01 +10:00
CandidateState ::Elected = > { if & cc . votes > state . quota . as_ref ( ) . unwrap ( ) { acc + & cc . votes - state . quota . as_ref ( ) . unwrap ( ) } else { acc } }
2021-06-07 20:52:18 +10:00
_ = > { acc + & cc . votes }
}
} ) ;
log . push_str ( format! ( " Total active vote is {:.dps$} , so the vote required for election is " , total_active_vote , dps = opts . pp_decimals ) . as_str ( ) ) ;
let vote_req = total_to_quota ( total_active_vote , state . election . seats - state . num_elected , opts ) ;
if & vote_req < state . quota . as_ref ( ) . unwrap ( ) {
// VRE is less than the quota
if let Some ( v ) = & state . vote_required_election {
if & vote_req ! = v {
log . push_str ( format! ( " {:.dps$} . " , vote_req , dps = opts . pp_decimals ) . as_str ( ) ) ;
state . vote_required_election = Some ( vote_req ) ;
state . logger . log_literal ( log ) ;
}
} else {
log . push_str ( format! ( " {:.dps$} . " , vote_req , dps = opts . pp_decimals ) . as_str ( ) ) ;
state . vote_required_election = Some ( vote_req ) ;
state . logger . log_literal ( log ) ;
}
} else {
// VRE is not less than the quota, so use the quota
state . vote_required_election = state . quota . clone ( ) ;
}
}
} else {
// No ERS97 rules
if let None = state . vote_required_election {
state . vote_required_election = state . quota . clone ( ) ;
}
}
2021-05-28 19:58:40 +10:00
}
2021-06-02 18:07:05 +10:00
fn meets_quota < N : Number > ( quota : & N , count_card : & CountCard < N > , opts : & STVOptions ) -> bool {
match opts . quota_criterion {
QuotaCriterion ::GreaterOrEqual = > {
return count_card . votes > = * quota ;
}
QuotaCriterion ::Greater = > {
return count_card . votes > * quota ;
}
}
2021-05-28 19:58:40 +10:00
}
2021-06-02 18:07:05 +10:00
fn elect_meeting_quota < N : Number > ( state : & mut CountState < N > , opts : & STVOptions ) {
2021-06-07 20:52:18 +10:00
let vote_req = state . vote_required_election . as_ref ( ) . unwrap ( ) ; // Have to do this or else the borrow checker gets confused
let mut cands_meeting_quota : Vec < & Candidate > = state . election . candidates . iter ( )
2021-06-12 00:50:01 +10:00
. filter ( | c | { let cc = state . candidates . get ( c ) . unwrap ( ) ; cc . state = = CandidateState ::Hopeful & & meets_quota ( vote_req , cc , opts ) } )
2021-05-28 22:37:18 +10:00
. collect ( ) ;
2021-05-28 19:58:40 +10:00
if cands_meeting_quota . len ( ) > 0 {
// Sort by votes
2021-06-07 20:52:18 +10:00
cands_meeting_quota . sort_unstable_by ( | a , b | state . candidates . get ( a ) . unwrap ( ) . votes . partial_cmp ( & state . candidates . get ( b ) . unwrap ( ) . votes ) . unwrap ( ) ) ;
2021-05-28 19:58:40 +10:00
// Declare elected in descending order of votes
2021-06-07 20:52:18 +10:00
for candidate in cands_meeting_quota . into_iter ( ) . rev ( ) {
let count_card = state . candidates . get_mut ( candidate ) . unwrap ( ) ;
2021-06-12 00:50:01 +10:00
count_card . state = CandidateState ::Elected ;
2021-05-28 19:58:40 +10:00
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-06-07 20:52:18 +10:00
if opts . quota_mode = = QuotaMode ::ERS97 {
// Vote required for election may have changed
calculate_quota ( state , opts ) ;
}
}
if opts . quota_mode = = QuotaMode ::ERS97 {
// Repeat in case vote required for election has changed
2021-06-09 12:16:25 +10:00
//calculate_quota(state, opts);
2021-06-07 20:52:18 +10:00
elect_meeting_quota ( state , opts ) ;
2021-05-28 19:58:40 +10:00
}
}
}
2021-06-09 12:42:47 +10:00
fn can_defer_surpluses < N : Number > ( state : & CountState < N > , opts : & STVOptions , total_surpluses : & N ) -> bool
2021-06-09 12:16:25 +10:00
where
for < ' r > & ' r N : ops ::Sub < & ' r N , Output = N >
{
// Do not defer if this could change the last 2 candidates
let mut hopefuls : Vec < ( & & Candidate , & CountCard < N > ) > = state . candidates . iter ( )
2021-06-12 00:50:01 +10:00
. filter ( | ( _ , cc ) | cc . state = = CandidateState ::Hopeful | | cc . state = = CandidateState ::Guarded )
2021-06-09 12:16:25 +10:00
. collect ( ) ;
hopefuls . sort_unstable_by ( | ( _ , cc1 ) , ( _ , cc2 ) | cc1 . votes . cmp ( & cc2 . votes ) ) ;
if total_surpluses > & ( & hopefuls [ 1 ] . 1. votes - & hopefuls [ 0 ] . 1. votes ) {
return false ;
}
// Do not defer if this could affect a bulk exclusion
if opts . bulk_exclude {
let to_exclude = hopefuls_to_bulk_exclude ( state , opts ) ;
let num_to_exclude = to_exclude . len ( ) ;
if num_to_exclude > 0 {
let total_excluded = to_exclude . into_iter ( )
. fold ( N ::new ( ) , | acc , c | acc + & state . candidates . get ( c ) . unwrap ( ) . votes ) ;
if total_surpluses > & ( & hopefuls [ num_to_exclude + 1 ] . 1. votes - & total_excluded ) {
return false ;
}
}
}
return true ;
}
2021-06-12 02:09:26 +10:00
fn distribute_surpluses < N : Number > ( state : & mut CountState < N > , opts : & STVOptions ) -> Result < bool , STVError >
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 > ,
2021-06-11 21:22:28 +10:00
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
2021-05-30 02:28:52 +10:00
for < ' r > & ' r N : ops ::Neg < Output = N >
2021-05-29 00:43:58 +10:00
{
2021-06-07 20:52:18 +10:00
let quota = state . quota . as_ref ( ) . unwrap ( ) ;
2021-06-12 02:09:26 +10:00
let mut has_surplus : Vec < ( & Candidate , & CountCard < N > ) > = state . election . candidates . iter ( ) // Present in order in case of tie
. map ( | c | ( c , state . candidates . get ( c ) . unwrap ( ) ) )
2021-06-07 20:52:18 +10:00
. filter ( | ( _ , cc ) | & cc . votes > quota )
2021-05-28 22:37:18 +10:00
. collect ( ) ;
2021-06-09 12:16:25 +10:00
let total_surpluses = has_surplus . iter ( )
. fold ( N ::new ( ) , | acc , ( _ , cc ) | acc + & cc . votes - quota ) ;
2021-05-28 19:58:40 +10:00
if has_surplus . len ( ) > 0 {
2021-06-09 12:16:25 +10:00
// Determine if surplues can be deferred
if opts . defer_surpluses {
2021-06-09 12:42:47 +10:00
if can_defer_surpluses ( state , opts , & total_surpluses ) {
2021-06-12 00:50:01 +10:00
state . logger . log_literal ( format! ( " Distribution of surpluses totalling {:.dps$} votes will be deferred. " , total_surpluses , dps = opts . pp_decimals ) ) ;
2021-06-12 02:09:26 +10:00
return Ok ( false ) ;
2021-06-09 12:16:25 +10:00
}
}
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
2021-06-12 02:09:26 +10:00
has_surplus . sort_by ( | a , b | b . 1. votes . partial_cmp ( & a . 1. votes ) . unwrap ( ) ) ;
2021-06-01 18:57:56 +10:00
}
SurplusOrder ::ByOrder = > {
2021-06-12 02:09:26 +10:00
has_surplus . sort_by ( | a , b | a . 1. order_elected . partial_cmp ( & b . 1. order_elected ) . unwrap ( ) ) ;
2021-06-01 18:57:56 +10:00
}
}
2021-05-28 19:58:40 +10:00
// Distribute top candidate's surplus
2021-06-12 02:09:26 +10:00
let elected_candidate ;
// Handle ties
if has_surplus . len ( ) > 1 & & has_surplus [ 0 ] . 1. votes = = has_surplus [ 1 ] . 1. votes {
let max_votes = & has_surplus [ 0 ] . 1. votes ;
let has_surplus = has_surplus . into_iter ( ) . filter_map ( | ( c , cc ) | if & cc . votes = = max_votes { Some ( c ) } else { None } ) . collect ( ) ;
elected_candidate = TieStrategy ::Prompt . choose_highest ( has_surplus ) ? ;
} else {
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
2021-06-12 02:09:26 +10:00
return Ok ( true ) ;
2021-05-28 19:58:40 +10:00
}
2021-06-12 02:09:26 +10:00
return Ok ( false ) ;
2021-05-28 19:58:40 +10:00
}
2021-05-31 23:17:21 +10:00
/// Return the denominator of the transfer value
2021-06-11 21:22:28 +10:00
fn calculate_surplus_denom < N : Number > ( surplus : & N , result : & NextPreferencesResult < N > , transferable_votes : & N , weighted : bool , transferable_only : bool ) -> Option < N >
2021-05-31 23:17:21 +10:00
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 ,
2021-06-11 21:22:28 +10:00
surplus_fraction : & Option < N > ,
surplus_denom : & Option < N > ,
2021-06-01 21:20:38 +10:00
round_tvs : Option < usize > ,
rounding : Option < usize > ) -> N
{
let mut result ;
2021-06-11 21:22:28 +10:00
match surplus_denom {
2021-05-31 23:17:21 +10:00
Some ( v ) = > {
2021-06-01 21:20:38 +10:00
if let Some ( _ ) = round_tvs {
// Rounding requested: use the rounded transfer value
if weighted {
2021-06-11 21:22:28 +10:00
result = num_votes . clone ( ) * surplus_fraction . as_ref ( ) . unwrap ( ) ;
2021-06-01 21:20:38 +10:00
} else {
2021-06-11 21:22:28 +10:00
result = num_ballots . clone ( ) * surplus_fraction . as_ref ( ) . unwrap ( ) ;
2021-06-01 21:20:38 +10:00
}
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-06-11 21:22:28 +10:00
fn sum_surplus_transfers < N : Number > ( entry : & NextPreferencesEntry < N > , surplus : & N , is_weighted : bool , surplus_fraction : & Option < N > , surplus_denom : & Option < N > , _state : & mut CountState < N > , opts : & STVOptions ) -> N
where
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
{
match opts . sum_surplus_transfers {
SumSurplusTransfersMode ::SingleStep = > {
// Calculate transfer across all votes
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals));
return reweight_vote ( & entry . num_votes , & entry . num_ballots , surplus , is_weighted , surplus_fraction , surplus_denom , opts . round_tvs , opts . round_votes ) ;
}
SumSurplusTransfersMode ::ByValue = > {
// Sum transfers by value
let mut result = N ::new ( ) ;
// Sort into parcels by value
let mut votes : Vec < & Vote < N > > = entry . votes . iter ( ) . collect ( ) ;
votes . sort_unstable_by ( | a , b | ( & a . value / & a . ballot . orig_value ) . cmp ( & ( & b . value / & b . ballot . orig_value ) ) ) ;
for ( _value , parcel ) in & votes . into_iter ( ) . group_by ( | v | & v . value / & v . ballot . orig_value ) {
let mut num_votes = N ::new ( ) ;
let mut num_ballots = N ::new ( ) ;
for vote in parcel {
num_votes + = & vote . value ;
num_ballots + = & vote . ballot . orig_value ;
}
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$}.", num_ballots, num_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
result + = reweight_vote ( & num_votes , & num_ballots , surplus , is_weighted , surplus_fraction , surplus_denom , opts . round_tvs , opts . round_votes ) ;
}
return result ;
}
SumSurplusTransfersMode ::PerBallot = > {
// Sum transfer per each individual ballot
// TODO: This could be moved to distribute_surplus to avoid looping over the votes and calculating transfer values twice
let mut result = N ::new ( ) ;
for vote in entry . votes . iter ( ) {
result + = reweight_vote ( & vote . value , & vote . ballot . orig_value , surplus , is_weighted , surplus_fraction , surplus_denom , opts . round_tvs , opts . round_votes ) ;
}
//state.logger.log_literal(format!("Transferring {:.0} ballot papers, totalling {:.dps$} votes.", entry.num_ballots, entry.num_votes, dps=opts.pp_decimals));
return result ;
}
}
}
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 > ,
2021-06-11 21:22:28 +10:00
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
2021-05-30 02:28:52 +10:00
for < ' r > & ' r N : ops ::Neg < Output = N >
2021-05-29 00:43:58 +10:00
{
2021-06-11 21:22:28 +10:00
state . logger . log_literal ( format! ( " Surplus of {} distributed. " , elected_candidate . name ) ) ;
2021-05-28 19:58:40 +10:00
let count_card = state . candidates . get ( elected_candidate ) . unwrap ( ) ;
2021-06-07 20:52:18 +10:00
let surplus = & count_card . votes - state . quota . as_ref ( ) . unwrap ( ) ;
2021-05-28 19:58:40 +10:00
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 ;
2021-06-11 21:22:28 +10:00
let surplus_denom = calculate_surplus_denom ( & surplus , & result , & transferable_votes , is_weighted , opts . transferable_only ) ;
let mut surplus_fraction ;
match surplus_denom {
2021-05-31 23:17:21 +10:00
Some ( ref v ) = > {
2021-06-11 21:22:28 +10:00
surplus_fraction = Some ( surplus . clone ( ) / v ) ;
2021-06-01 21:20:38 +10:00
// Round down if requested
if let Some ( dps ) = opts . round_tvs {
2021-06-11 21:22:28 +10:00
surplus_fraction . as_mut ( ) . unwrap ( ) . floor_mut ( dps ) ;
2021-06-01 21:20:38 +10:00
}
2021-06-11 21:22:28 +10:00
if opts . transferable_only {
state . logger . log_literal ( format! ( " Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, with surplus fraction {:.dps2$} . " , & result . total_ballots - & result . exhausted . num_ballots , transferable_votes , surplus_fraction . as_ref ( ) . unwrap ( ) , dps = opts . pp_decimals , dps2 = max ( opts . pp_decimals , 2 ) ) ) ;
} else {
state . logger . log_literal ( format! ( " Transferring {:.0} ballots, totalling {:.dps$} votes, with surplus fraction {:.dps2$} . " , result . total_ballots , result . total_votes , surplus_fraction . as_ref ( ) . unwrap ( ) , dps = opts . pp_decimals , dps2 = max ( opts . pp_decimals , 2 ) ) ) ;
}
2021-05-31 22:25:53 +10:00
}
2021-05-31 23:17:21 +10:00
None = > {
2021-06-11 21:22:28 +10:00
surplus_fraction = None ;
if opts . transferable_only {
state . logger . log_literal ( format! ( " Transferring {:.0} transferable ballots, totalling {:.dps$} transferable votes, at values received. " , & result . total_ballots - & result . exhausted . num_ballots , transferable_votes , dps = opts . pp_decimals ) ) ;
} else {
state . logger . log_literal ( format! ( " Transferring {:.0} ballots, totalling {:.dps$} votes, at values received. " , result . total_ballots , result . total_votes , dps = opts . pp_decimals ) ) ;
}
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 ( ) {
2021-06-11 21:22:28 +10:00
// Credit transferred votes
let candidate_transfers = sum_surplus_transfers ( & entry , & surplus , is_weighted , & surplus_fraction , & surplus_denom , state , opts ) ;
let count_card = state . candidates . get_mut ( candidate ) . unwrap ( ) ;
count_card . transfer ( & candidate_transfers ) ;
checksum + = candidate_transfers ;
2021-05-28 19:58:40 +10:00
let mut parcel = entry . votes as Parcel < N > ;
// Reweight votes
for vote in parcel . iter_mut ( ) {
2021-06-11 21:22:28 +10:00
vote . value = reweight_vote ( & vote . value , & vote . ballot . orig_value , & surplus , is_weighted , & surplus_fraction , & surplus_denom , opts . round_tvs , opts . round_weights ) ;
2021-05-28 19:58:40 +10:00
}
count_card . parcels . push ( parcel ) ;
}
2021-06-11 21:22:28 +10:00
// Credit exhausted votes
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 ;
2021-06-11 21:22:28 +10:00
if let Some ( dps ) = opts . round_votes {
exhausted_transfers . floor_mut ( dps ) ;
}
2021-05-31 23:17:21 +10:00
}
} else {
2021-06-11 21:22:28 +10:00
exhausted_transfers = sum_surplus_transfers ( & result . exhausted , & surplus , is_weighted , & surplus_fraction , & surplus_denom , state , opts ) ;
2021-05-31 23:17:21 +10:00
}
2021-05-29 00:43:58 +10:00
state . exhausted . transfer ( & exhausted_transfers ) ;
checksum + = exhausted_transfers ;
2021-05-28 19:58:40 +10:00
2021-06-11 21:22:28 +10:00
// Transfer exhausted votes
let parcel = result . exhausted . votes as Parcel < N > ;
state . exhausted . parcels . push ( parcel ) ;
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-06-07 20:52:18 +10:00
count_card . votes . assign ( state . quota . as_ref ( ) . unwrap ( ) ) ;
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 ( )
2021-06-12 00:50:01 +10:00
. filter ( | ( _ , cc ) | cc . state = = CandidateState ::Hopeful )
2021-05-28 22:37:18 +10:00
. 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-06-12 00:50:01 +10:00
count_card . state = CandidateState ::Elected ;
2021-05-28 19:58:40 +10:00
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-06-09 12:16:25 +10:00
fn hopefuls_to_bulk_exclude < ' a , N : Number > ( state : & CountState < ' a , N > , _opts : & STVOptions ) -> Vec < & ' a Candidate > {
let mut excluded_candidates = Vec ::new ( ) ;
2021-05-28 22:37:18 +10:00
let mut hopefuls : Vec < ( & & Candidate , & CountCard < N > ) > = state . candidates . iter ( )
2021-06-12 00:50:01 +10:00
. filter ( | ( _ , cc ) | cc . state = = CandidateState ::Hopeful )
2021-05-28 22:37:18 +10:00
. 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 ( ) ) ;
2021-06-09 12:16:25 +10:00
let total_surpluses = state . candidates . iter ( )
. filter ( | ( _ , cc ) | & cc . votes > state . quota . as_ref ( ) . unwrap ( ) )
. fold ( N ::new ( ) , | agg , ( _ , cc ) | agg + & cc . votes - state . quota . as_ref ( ) . unwrap ( ) ) ;
// Attempt to exclude as many candidates as possible
for i in 0 .. hopefuls . len ( ) {
let try_exclude = & hopefuls [ 0 .. hopefuls . len ( ) - i ] ;
// Do not exclude if this splits tied candidates
if i ! = 0 & & try_exclude . last ( ) . unwrap ( ) . 1. votes = = hopefuls [ hopefuls . len ( ) - i ] . 1. votes {
continue ;
}
// Do not exclude if this leaves insufficient candidates
if state . num_elected + hopefuls . len ( ) - try_exclude . len ( ) < state . election . seats {
continue ;
}
// Do not exclude if this could change the order of exclusion
let total_votes = try_exclude . into_iter ( ) . fold ( N ::new ( ) , | agg , ( _ , cc ) | agg + & cc . votes ) ;
if i ! = 0 & & total_votes + & total_surpluses > hopefuls [ hopefuls . len ( ) - i ] . 1. votes {
continue ;
}
for ( c , _ ) in try_exclude . into_iter ( ) {
excluded_candidates . push ( * * c ) ;
}
break ;
}
return excluded_candidates ;
}
2021-06-12 02:09:26 +10:00
fn exclude_hopefuls < ' a , N : Number > ( state : & mut CountState < ' a , N > , opts : & STVOptions ) -> Result < bool , STVError >
2021-06-09 12:16:25 +10:00
where
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
{
2021-06-08 22:22:43 +10:00
let mut excluded_candidates : Vec < & Candidate > = Vec ::new ( ) ;
// Attempt a bulk exclusion
if opts . bulk_exclude {
2021-06-09 12:16:25 +10:00
excluded_candidates = hopefuls_to_bulk_exclude ( state , opts ) ;
2021-06-08 22:22:43 +10:00
}
2021-05-28 19:58:40 +10:00
// Exclude lowest ranked candidate
2021-06-08 22:22:43 +10:00
if excluded_candidates . len ( ) = = 0 {
2021-06-12 02:09:26 +10:00
let mut hopefuls : Vec < ( & Candidate , & CountCard < N > ) > = state . election . candidates . iter ( ) // Present in order in case of tie
. map ( | c | ( c , state . candidates . get ( c ) . unwrap ( ) ) )
2021-06-12 00:50:01 +10:00
. filter ( | ( _ , cc ) | cc . state = = CandidateState ::Hopeful )
. collect ( ) ;
2021-06-09 12:16:25 +10:00
// Sort by votes
2021-06-12 02:09:26 +10:00
hopefuls . sort_by ( | a , b | a . 1. votes . partial_cmp ( & b . 1. votes ) . unwrap ( ) ) ;
2021-06-09 12:16:25 +10:00
2021-06-12 02:09:26 +10:00
// Handle ties
if hopefuls . len ( ) > 1 & & hopefuls [ 0 ] . 1. votes = = hopefuls [ 1 ] . 1. votes {
let min_votes = & hopefuls [ 0 ] . 1. votes ;
let hopefuls = hopefuls . into_iter ( ) . filter_map ( | ( c , cc ) | if & cc . votes = = min_votes { Some ( c ) } else { None } ) . collect ( ) ;
excluded_candidates = vec! [ TieStrategy ::Prompt . choose_lowest ( hopefuls ) ? ] ;
} else {
excluded_candidates = vec! [ & hopefuls . first ( ) . unwrap ( ) . 0 ] ;
}
2021-06-08 22:22:43 +10:00
}
2021-05-29 01:22:46 +10:00
2021-06-08 22:22:43 +10:00
let mut names : Vec < & str > = excluded_candidates . iter ( ) . map ( | c | c . name . as_str ( ) ) . collect ( ) ;
names . sort ( ) ;
2021-05-29 01:22:46 +10:00
state . kind = Some ( " Exclusion of " ) ;
2021-06-08 22:22:43 +10:00
state . title = names . join ( " , " ) ;
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. " ,
2021-06-08 22:22:43 +10:00
names
2021-05-29 02:13:47 +10:00
) ;
2021-05-29 01:22:46 +10:00
2021-06-08 22:22:43 +10:00
exclude_candidates ( state , opts , excluded_candidates ) ;
2021-05-28 19:58:40 +10:00
2021-06-12 02:09:26 +10:00
return Ok ( true ) ;
2021-05-28 19:58:40 +10:00
}
2021-06-08 22:22:43 +10:00
fn continue_exclusion < ' a , N : Number > ( state : & mut CountState < ' a , N > , opts : & STVOptions ) -> bool
2021-05-30 02:28:52 +10:00
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-06-12 02:09:26 +10:00
//.filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && !cc.votes.is_zero())
2021-06-12 00:50:01 +10:00
. 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 ( ) ) ;
2021-05-29 01:22:46 +10:00
2021-06-08 22:22:43 +10:00
let order_excluded = excluded_with_votes . first ( ) . unwrap ( ) . 1. order_elected ;
let excluded_candidates : Vec < & Candidate > = excluded_with_votes . into_iter ( )
. filter ( | ( _ , cc ) | cc . order_elected = = order_excluded )
. map ( | ( c , _ ) | * c )
. collect ( ) ;
let mut names : Vec < & str > = excluded_candidates . iter ( ) . map ( | c | c . name . as_str ( ) ) . collect ( ) ;
names . sort ( ) ;
2021-05-29 01:22:46 +10:00
state . kind = Some ( " Exclusion of " ) ;
2021-06-08 22:22:43 +10:00
state . title = names . join ( " , " ) ;
2021-05-29 02:13:47 +10:00
state . logger . log_smart (
" Continuing exclusion of {}. " ,
" Continuing exclusion of {}. " ,
2021-06-08 22:22:43 +10:00
names
2021-05-29 02:13:47 +10:00
) ;
2021-05-29 01:22:46 +10:00
2021-06-08 22:22:43 +10:00
exclude_candidates ( state , opts , excluded_candidates ) ;
2021-05-28 19:58:40 +10:00
return true ;
}
return false ;
}
2021-06-08 22:22:43 +10:00
fn exclude_candidates < ' a , N : Number > ( state : & mut CountState < ' a , N > , opts : & STVOptions , excluded_candidates : Vec < & ' a Candidate > )
2021-05-30 02:28:52 +10:00
where
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
{
2021-06-08 22:22:43 +10:00
// Used to give bulk excluded candidate the same order_elected
let order_excluded = state . num_excluded + 1 ;
2021-05-28 19:58:40 +10:00
2021-06-08 22:22:43 +10:00
for excluded_candidate in excluded_candidates . iter ( ) {
let count_card = state . candidates . get_mut ( excluded_candidate ) . unwrap ( ) ;
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
2021-06-12 00:50:01 +10:00
if count_card . state ! = CandidateState ::Excluded {
count_card . state = CandidateState ::Excluded ;
2021-06-08 22:22:43 +10:00
state . num_excluded + = 1 ;
count_card . order_elected = - ( order_excluded as isize ) ;
}
2021-05-30 02:28:52 +10:00
}
// Determine votes to transfer in this stage
2021-06-08 22:22:43 +10:00
let mut votes = Vec ::new ( ) ;
let mut votes_remain ;
2021-06-09 19:43:11 +10:00
let mut checksum = N ::new ( ) ;
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
2021-06-08 22:22:43 +10:00
for excluded_candidate in excluded_candidates . iter ( ) {
let count_card = state . candidates . get_mut ( excluded_candidate ) . unwrap ( ) ;
votes . append ( & mut count_card . parcels . concat ( ) ) ;
2021-06-09 22:34:44 +10:00
count_card . parcels . clear ( ) ;
2021-06-09 19:43:11 +10:00
// Update votes
let votes_transferred = votes . iter ( ) . fold ( N ::new ( ) , | acc , v | acc + & v . value ) ;
checksum - = & votes_transferred ;
count_card . transfer ( & - votes_transferred ) ;
2021-06-08 22:22:43 +10:00
}
2021-05-31 22:25:53 +10:00
votes_remain = false ;
}
ExclusionMethod ::ByValue = > {
// Exclude by value
2021-06-08 22:22:43 +10:00
let max_value = excluded_candidates . iter ( )
. map ( | c | state . candidates . get ( c ) . unwrap ( ) . parcels . iter ( )
. map ( | p | p . iter ( ) . map ( | v | & v . value / & v . ballot . orig_value ) . max ( ) . unwrap ( ) )
. max ( ) . unwrap ( ) )
. max ( ) . unwrap ( ) ;
2021-05-31 22:25:53 +10:00
2021-06-08 22:22:43 +10:00
votes_remain = false ;
2021-05-31 22:25:53 +10:00
2021-06-08 22:22:43 +10:00
for excluded_candidate in excluded_candidates . iter ( ) {
let count_card = state . candidates . get_mut ( excluded_candidate ) . unwrap ( ) ;
// Filter out just those votes with max_value
let mut remaining_votes = Vec ::new ( ) ;
let cand_votes = count_card . parcels . concat ( ) ;
2021-06-09 19:43:11 +10:00
let mut votes_transferred = N ::new ( ) ;
2021-06-08 22:22:43 +10:00
for vote in cand_votes . into_iter ( ) {
if & vote . value / & vote . ballot . orig_value = = max_value {
2021-06-09 19:43:11 +10:00
votes_transferred + = & vote . value ;
2021-06-08 22:22:43 +10:00
votes . push ( vote ) ;
} else {
remaining_votes . push ( vote ) ;
}
}
if remaining_votes . len ( ) > 0 {
votes_remain = true ;
2021-05-31 22:25:53 +10:00
}
2021-06-08 22:22:43 +10:00
// Leave remaining votes with candidate (as one parcel)
count_card . parcels = vec! [ remaining_votes ] ;
2021-06-09 19:43:11 +10:00
// Update votes
checksum - = & votes_transferred ;
count_card . transfer ( & - votes_transferred ) ;
2021-05-30 02:28:52 +10:00
}
2021-05-31 22:25:53 +10:00
}
ExclusionMethod ::ParcelsByOrder = > {
// Exclude by parcel by order
2021-06-08 22:22:43 +10:00
if excluded_candidates . len ( ) > 1 {
panic! ( " --exclusion parcels_by_order is incompatible with --bulk-exclude " ) ;
}
let count_card = state . candidates . get_mut ( excluded_candidates . first ( ) . unwrap ( ) ) . unwrap ( ) ;
2021-05-31 22:25:53 +10:00
votes = count_card . parcels . remove ( 0 ) ;
votes_remain = count_card . parcels . len ( ) > 0 ;
2021-06-09 19:43:11 +10:00
// Update votes
let votes_transferred = votes . iter ( ) . fold ( N ::new ( ) , | acc , v | acc + & v . value ) ;
checksum - = & votes_transferred ;
count_card . transfer ( & - votes_transferred ) ;
2021-05-30 02:28:52 +10:00
}
}
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 {
2021-06-06 00:53:52 +10:00
state . logger . log_literal ( format! ( " Transferring {:.0} ballot papers, totalling {:.dps$} votes, received at value {:.dps2$} . " , result . total_ballots , result . total_votes , value , dps = opts . pp_decimals , dps2 = max ( opts . pp_decimals , 2 ) ) ) ;
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-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
2021-06-08 22:22:43 +10:00
for excluded_candidate in excluded_candidates . into_iter ( ) {
let count_card = state . candidates . get_mut ( excluded_candidate ) . unwrap ( ) ;
checksum - = & count_card . votes ;
2021-06-09 19:43:11 +10:00
count_card . transfers - = & count_card . votes ;
2021-06-08 22:22:43 +10:00
count_card . votes = N ::new ( ) ;
}
2021-05-30 02:28:52 +10:00
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-06-04 22:54:26 +10:00
// 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 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 ;
}