diff --git a/src/main.rs b/src/main.rs index f63da0c..2598d14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,7 +96,7 @@ struct STV { // ------------------ // -- STV variants -- - /// Method of surplus transfers + /// Method of surplus distributions #[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")] surplus: String, @@ -114,9 +114,14 @@ struct STV { // ------------------------- // -- Count optimisations -- + /// Use bulk exclusion #[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)] bulk_exclude: bool, + /// Defer surplus distributions if possible + #[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)] + defer_surpluses: bool, + // ---------------------- // -- Display settings -- @@ -176,6 +181,7 @@ where cmd_opts.transferable_only, &cmd_opts.exclusion, cmd_opts.bulk_exclude, + cmd_opts.defer_surpluses, cmd_opts.pp_decimals, ); diff --git a/src/stv/mod.rs b/src/stv/mod.rs index b9b4888..0c2d323 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -43,6 +43,7 @@ pub struct STVOptions { pub transferable_only: bool, pub exclusion: ExclusionMethod, pub bulk_exclude: bool, + pub defer_surpluses: bool, pub pp_decimals: usize, } @@ -61,6 +62,7 @@ impl STVOptions { transferable_only: bool, exclusion: &str, bulk_exclude: bool, + defer_surpluses: bool, pp_decimals: usize, ) -> Self { return STVOptions { @@ -105,6 +107,7 @@ impl STVOptions { _ => panic!("Invalid --exclusion"), }, bulk_exclude, + defer_surpluses, pp_decimals, }; } @@ -465,7 +468,7 @@ fn calculate_quota(state: &mut CountState, opts: &STVOptions) { // Calculate total active vote let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| { match cc.state { - CandidateState::ELECTED => { acc + &cc.votes - state.quota.as_ref().unwrap() } + CandidateState::ELECTED => { if &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } } _ => { acc + &cc.votes } } }); @@ -541,11 +544,40 @@ fn elect_meeting_quota(state: &mut CountState, opts: &STVOptions) if opts.quota_mode == QuotaMode::ERS97 { // Repeat in case vote required for election has changed + //calculate_quota(state, opts); elect_meeting_quota(state, opts); } } } +fn can_defer_surpluses(state: &CountState, opts: &STVOptions, has_surplus: &Vec<(&&Candidate, &CountCard)>, total_surpluses: &N) -> bool +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)> = state.candidates.iter() + .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL || cc.state == CandidateState::GUARDED) + .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; +} + fn distribute_surpluses(state: &mut CountState, opts: &STVOptions) -> bool where for<'r> &'r N: ops::Sub<&'r N, Output=N>, @@ -555,8 +587,18 @@ where let mut has_surplus: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() .filter(|(_, cc)| &cc.votes > quota) .collect(); - + let total_surpluses = has_surplus.iter() + .fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota); + if has_surplus.len() > 0 { + // Determine if surplues can be deferred + if opts.defer_surpluses { + if can_defer_surpluses(state, opts, &has_surplus, &total_surpluses) { + state.logger.log_literal(format!("Distribution of surpluses totalling {:.2} votes will be deferred.", total_surpluses)); + return false; + } + } + match opts.surplus_order { SurplusOrder::BySize => { // Compare b with a to sort high-low @@ -782,10 +824,9 @@ fn bulk_elect(state: &mut CountState) -> bool { return false; } -fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool -where - for<'r> &'r N: ops::Div<&'r N, Output=N>, -{ +fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &STVOptions) -> Vec<&'a Candidate> { + let mut excluded_candidates = Vec::new(); + let mut hopefuls: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) .collect(); @@ -794,43 +835,60 @@ where // TODO: Handle ties hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); + 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; +} + +fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool +where + for<'r> &'r N: ops::Div<&'r N, Output=N>, +{ let mut excluded_candidates: Vec<&Candidate> = Vec::new(); // Attempt a bulk exclusion if opts.bulk_exclude { - 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; - } + excluded_candidates = hopefuls_to_bulk_exclude(state, opts); } // Exclude lowest ranked candidate if excluded_candidates.len() == 0 { + let mut hopefuls: Vec<(&&Candidate, &CountCard)> = state.candidates.iter() + .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL) + .collect(); + + // Sort by votes + // TODO: Handle ties + hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); + excluded_candidates = vec![&hopefuls.first().unwrap().0]; } diff --git a/tests/aec.rs b/tests/aec.rs index fa7d94b..84d6b9e 100644 --- a/tests/aec.rs +++ b/tests/aec.rs @@ -67,6 +67,7 @@ fn aec_tas19_rational() { transferable_only: false, exclusion: stv::ExclusionMethod::ByValue, bulk_exclude: true, + defer_surpluses: false, pp_decimals: 2, }; diff --git a/tests/prsa.rs b/tests/prsa.rs index 9b2dddb..965a1a8 100644 --- a/tests/prsa.rs +++ b/tests/prsa.rs @@ -35,6 +35,7 @@ fn prsa1_rational() { transferable_only: true, exclusion: stv::ExclusionMethod::ParcelsByOrder, bulk_exclude: false, + defer_surpluses: false, pp_decimals: 2, }; utils::read_validate_election::("tests/data/prsa1.csv", "tests/data/prsa1.blt", stv_opts);