Implement Meek STV
This commit is contained in:
parent
f395e6f064
commit
4ebb6474fd
@ -37,9 +37,9 @@
|
|||||||
<select id="selPreset" onchange="changePreset()">
|
<select id="selPreset" onchange="changePreset()">
|
||||||
<option value="wigm" selected>Recommended WIGM</option>
|
<option value="wigm" selected>Recommended WIGM</option>
|
||||||
<option value="scottish">Scottish STV</option>
|
<option value="scottish">Scottish STV</option>
|
||||||
|
<option value="meek">Meek STV</option>
|
||||||
<option value="senate">Australian Senate STV</option>
|
<option value="senate">Australian Senate STV</option>
|
||||||
<!--<option value="meek">Meek STV</option>
|
<!--<option value="wright">Wright STV</option>-->
|
||||||
<option value="wright">Wright STV</option>-->
|
|
||||||
<option value="prsa77">PRSA 1977</option>
|
<option value="prsa77">PRSA 1977</option>
|
||||||
<option value="ers97">ERS97</option>
|
<option value="ers97">ERS97</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -353,6 +353,27 @@ function changePreset() {
|
|||||||
document.getElementById('selPapers').value = 'both';
|
document.getElementById('selPapers').value = 'both';
|
||||||
document.getElementById('selExclusion').value = 'single_stage';
|
document.getElementById('selExclusion').value = 'single_stage';
|
||||||
document.getElementById('selTies').value = 'backwards,random';
|
document.getElementById('selTies').value = 'backwards,random';
|
||||||
|
} else if (document.getElementById('selPreset').value === 'meek') {
|
||||||
|
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||||
|
document.getElementById('selQuota').value = 'droop_exact';
|
||||||
|
document.getElementById('selQuotaMode').value = 'static';
|
||||||
|
document.getElementById('chkBulkElection').checked = true;
|
||||||
|
document.getElementById('chkBulkExclusion').checked = false;
|
||||||
|
document.getElementById('chkDeferSurpluses').checked = false;
|
||||||
|
document.getElementById('selNumbers').value = 'fixed';
|
||||||
|
document.getElementById('txtDP').value = '5';
|
||||||
|
document.getElementById('txtPPDP').value = '2';
|
||||||
|
document.getElementById('chkNormaliseBallots').checked = false;
|
||||||
|
document.getElementById('chkRoundQuota').checked = false;
|
||||||
|
document.getElementById('chkRoundVotes').checked = false;
|
||||||
|
document.getElementById('chkRoundTVs').checked = false;
|
||||||
|
document.getElementById('chkRoundWeights').checked = false;
|
||||||
|
document.getElementById('selSumTransfers').value = 'single_step';
|
||||||
|
document.getElementById('selSurplus').value = 'by_size';
|
||||||
|
document.getElementById('selTransfers').value = 'meek';
|
||||||
|
document.getElementById('selPapers').value = 'both';
|
||||||
|
document.getElementById('selExclusion').value = 'single_stage';
|
||||||
|
document.getElementById('selTies').value = 'backwards,random';
|
||||||
} else if (document.getElementById('selPreset').value === 'senate') {
|
} else if (document.getElementById('selPreset').value === 'senate') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
document.getElementById('selQuota').value = 'droop';
|
document.getElementById('selQuota').value = 'droop';
|
||||||
|
@ -79,7 +79,7 @@ function resume_count() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state)});
|
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state)});
|
||||||
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state)});
|
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state, opts)});
|
||||||
}
|
}
|
||||||
|
|
||||||
var user_input_buffer = null;
|
var user_input_buffer = null;
|
||||||
|
@ -129,14 +129,16 @@ pub struct Candidate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The current state of counting an [Election]
|
/// The current state of counting an [Election]
|
||||||
#[derive(Clone)]
|
//#[derive(Clone)]
|
||||||
pub struct CountState<'a, N> {
|
pub struct CountState<'a, N: Number> {
|
||||||
pub election: &'a Election<N>,
|
pub election: &'a Election<N>,
|
||||||
|
|
||||||
pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>,
|
pub candidates: HashMap<&'a Candidate, CountCard<'a, N>>,
|
||||||
pub exhausted: CountCard<'a, N>,
|
pub exhausted: CountCard<'a, N>,
|
||||||
pub loss_fraction: CountCard<'a, N>,
|
pub loss_fraction: CountCard<'a, N>,
|
||||||
|
|
||||||
|
pub ballot_tree: Option<crate::stv::meek::BallotTree<'a, N>>,
|
||||||
|
|
||||||
pub forwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
pub forwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
||||||
pub backwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
pub backwards_tiebreak: Option<HashMap<&'a Candidate, usize>>,
|
||||||
pub random: Option<SHARandom<'a>>,
|
pub random: Option<SHARandom<'a>>,
|
||||||
@ -160,6 +162,7 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||||||
candidates: HashMap::new(),
|
candidates: HashMap::new(),
|
||||||
exhausted: CountCard::new(),
|
exhausted: CountCard::new(),
|
||||||
loss_fraction: CountCard::new(),
|
loss_fraction: CountCard::new(),
|
||||||
|
ballot_tree: None,
|
||||||
forwards_tiebreak: None,
|
forwards_tiebreak: None,
|
||||||
backwards_tiebreak: None,
|
backwards_tiebreak: None,
|
||||||
random: None,
|
random: None,
|
||||||
@ -195,12 +198,12 @@ impl<'a, N: Number> CountState<'a, N> {
|
|||||||
|
|
||||||
/// Represents either a reference to a [CountState] or a clone
|
/// Represents either a reference to a [CountState] or a clone
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum CountStateOrRef<'a, N> {
|
pub enum CountStateOrRef<'a, N: Number> {
|
||||||
State(CountState<'a, N>), // NYI: May be used e.g. for tie-breaking or rollback-based constraints
|
State(CountState<'a, N>), // NYI: May be used e.g. for tie-breaking or rollback-based constraints
|
||||||
Ref(&'a CountState<'a, N>),
|
Ref(&'a CountState<'a, N>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, N> CountStateOrRef<'a, N> {
|
impl<'a, N: Number> CountStateOrRef<'a, N> {
|
||||||
/// Construct a [CountStateOrRef] as a reference to a [CountState]
|
/// Construct a [CountStateOrRef] as a reference to a [CountState]
|
||||||
pub fn from(state: &'a CountState<N>) -> Self {
|
pub fn from(state: &'a CountState<N>) -> Self {
|
||||||
return Self::Ref(state);
|
return Self::Ref(state);
|
||||||
@ -216,7 +219,7 @@ impl<'a, N> CountStateOrRef<'a, N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Result of a stage of counting
|
/// Result of a stage of counting
|
||||||
pub struct StageResult<'a, N> {
|
pub struct StageResult<'a, N: Number> {
|
||||||
pub kind: Option<&'a str>,
|
pub kind: Option<&'a str>,
|
||||||
pub title: &'a String,
|
pub title: &'a String,
|
||||||
pub logs: Vec<String>,
|
pub logs: Vec<String>,
|
||||||
@ -229,11 +232,14 @@ pub struct CountCard<'a, N> {
|
|||||||
pub state: CandidateState,
|
pub state: CandidateState,
|
||||||
pub order_elected: isize,
|
pub order_elected: isize,
|
||||||
|
|
||||||
pub orig_votes: N,
|
//pub orig_votes: N,
|
||||||
pub transfers: N,
|
pub transfers: N,
|
||||||
pub votes: N,
|
pub votes: N,
|
||||||
|
|
||||||
pub parcels: Vec<Parcel<'a, N>>,
|
pub parcels: Vec<Parcel<'a, N>>,
|
||||||
|
|
||||||
|
/// Candidate's keep value (Meek STV)
|
||||||
|
pub keep_value: Option<N>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, N: Number> CountCard<'a, N> {
|
impl<'a, N: Number> CountCard<'a, N> {
|
||||||
@ -242,10 +248,11 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||||||
return CountCard {
|
return CountCard {
|
||||||
state: CandidateState::Hopeful,
|
state: CandidateState::Hopeful,
|
||||||
order_elected: 0,
|
order_elected: 0,
|
||||||
orig_votes: N::new(),
|
//orig_votes: N::new(),
|
||||||
transfers: N::new(),
|
transfers: N::new(),
|
||||||
votes: N::new(),
|
votes: N::new(),
|
||||||
parcels: Vec::new(),
|
parcels: Vec::new(),
|
||||||
|
keep_value: None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,9 +262,9 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||||||
self.votes += transfer;
|
self.votes += transfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set [orig_votes](CountCard::orig_votes) to [votes](CountCard::votes), and set [transfers](CountCard::transfers) to 0
|
/// Set [transfers](CountCard::transfers) to 0
|
||||||
pub fn step(&mut self) {
|
pub fn step(&mut self) {
|
||||||
self.orig_votes = self.votes.clone();
|
//self.orig_votes = self.votes.clone();
|
||||||
self.transfers = N::new();
|
self.transfers = N::new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -270,6 +277,7 @@ pub type Parcel<'a, N> = Vec<Vote<'a, N>>;
|
|||||||
pub struct Vote<'a, N> {
|
pub struct Vote<'a, N> {
|
||||||
pub ballot: &'a Ballot<N>,
|
pub ballot: &'a Ballot<N>,
|
||||||
pub value: N,
|
pub value: N,
|
||||||
|
/// Index of the next preference to examine
|
||||||
pub up_to_pref: usize,
|
pub up_to_pref: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ use opentally::numbers::{Fixed, GuardedFixed, NativeFloat64, Number, Rational};
|
|||||||
|
|
||||||
use clap::{AppSettings, Clap};
|
use clap::{AppSettings, Clap};
|
||||||
|
|
||||||
|
use std::cmp::max;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, BufRead};
|
use std::io::{self, BufRead};
|
||||||
use std::ops;
|
use std::ops;
|
||||||
@ -185,6 +186,7 @@ fn main() {
|
|||||||
fn count_election<N: Number>(mut election: Election<N>, cmd_opts: STV)
|
fn count_election<N: Number>(mut election: Election<N>, cmd_opts: STV)
|
||||||
where
|
where
|
||||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Neg<Output=N>
|
for<'r> &'r N: ops::Neg<Output=N>
|
||||||
{
|
{
|
||||||
@ -261,7 +263,11 @@ where
|
|||||||
fn print_candidates<'a, N: 'a + Number, I: Iterator<Item=(&'a Candidate, &'a CountCard<'a, N>)>>(candidates: I, cmd_opts: &STV) {
|
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 {
|
for (candidate, count_card) in candidates {
|
||||||
if count_card.state == CandidateState::Elected {
|
if count_card.state == CandidateState::Elected {
|
||||||
|
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);
|
println!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=cmd_opts.pp_decimals);
|
||||||
|
}
|
||||||
} else if count_card.state == CandidateState::Excluded {
|
} else if count_card.state == CandidateState::Excluded {
|
||||||
// If --hide-excluded, hide unless nonzero votes or nonzero transfers
|
// 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() {
|
if !cmd_opts.hide_excluded || !count_card.votes.is_zero() || !count_card.transfers.is_zero() {
|
||||||
|
@ -153,9 +153,7 @@ impl ops::Neg for Fixed {
|
|||||||
|
|
||||||
impl ops::Add for Fixed {
|
impl ops::Add for Fixed {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn add(self, _rhs: Self) -> Self::Output {
|
fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Sub for Fixed {
|
impl ops::Sub for Fixed {
|
||||||
@ -174,9 +172,7 @@ impl ops::Mul for Fixed {
|
|||||||
|
|
||||||
impl ops::Div for Fixed {
|
impl ops::Div for Fixed {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn div(self, _rhs: Self) -> Self::Output {
|
fn div(self, rhs: Self) -> Self::Output { Self(self.0 * get_factor() / rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Rem for Fixed {
|
impl ops::Rem for Fixed {
|
||||||
@ -285,9 +281,7 @@ impl ops::Sub<Self> for &Fixed {
|
|||||||
|
|
||||||
impl ops::Mul<Self> for &Fixed {
|
impl ops::Mul<Self> for &Fixed {
|
||||||
type Output = Fixed;
|
type Output = Fixed;
|
||||||
fn mul(self, _rhs: Self) -> Self::Output {
|
fn mul(self, rhs: Self) -> Self::Output { Fixed(&self.0 * &rhs.0 / get_factor()) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Div<Self> for &Fixed {
|
impl ops::Div<Self> for &Fixed {
|
||||||
|
@ -186,9 +186,7 @@ impl ops::Neg for GuardedFixed {
|
|||||||
|
|
||||||
impl ops::Add for GuardedFixed {
|
impl ops::Add for GuardedFixed {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn add(self, _rhs: Self) -> Self::Output {
|
fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Sub for GuardedFixed {
|
impl ops::Sub for GuardedFixed {
|
||||||
@ -207,9 +205,7 @@ impl ops::Mul for GuardedFixed {
|
|||||||
|
|
||||||
impl ops::Div for GuardedFixed {
|
impl ops::Div for GuardedFixed {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn div(self, _rhs: Self) -> Self::Output {
|
fn div(self, rhs: Self) -> Self::Output { Self(self.0 * get_factor() / rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Rem for GuardedFixed {
|
impl ops::Rem for GuardedFixed {
|
||||||
@ -318,9 +314,7 @@ impl ops::Sub<Self> for &GuardedFixed {
|
|||||||
|
|
||||||
impl ops::Mul<Self> for &GuardedFixed {
|
impl ops::Mul<Self> for &GuardedFixed {
|
||||||
type Output = GuardedFixed;
|
type Output = GuardedFixed;
|
||||||
fn mul(self, _rhs: Self) -> Self::Output {
|
fn mul(self, rhs: Self) -> Self::Output { GuardedFixed(&self.0 * &rhs.0 / get_factor()) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Div<Self> for &GuardedFixed {
|
impl ops::Div<Self> for &GuardedFixed {
|
||||||
|
@ -97,9 +97,7 @@ impl ops::Neg for NativeFloat64 {
|
|||||||
|
|
||||||
impl ops::Add for NativeFloat64 {
|
impl ops::Add for NativeFloat64 {
|
||||||
type Output = NativeFloat64;
|
type Output = NativeFloat64;
|
||||||
fn add(self, _rhs: Self) -> Self::Output {
|
fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Sub for NativeFloat64 {
|
impl ops::Sub for NativeFloat64 {
|
||||||
@ -118,9 +116,7 @@ impl ops::Mul for NativeFloat64 {
|
|||||||
|
|
||||||
impl ops::Div for NativeFloat64 {
|
impl ops::Div for NativeFloat64 {
|
||||||
type Output = NativeFloat64;
|
type Output = NativeFloat64;
|
||||||
fn div(self, _rhs: Self) -> Self::Output {
|
fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Rem for NativeFloat64 {
|
impl ops::Rem for NativeFloat64 {
|
||||||
@ -220,9 +216,7 @@ impl ops::Sub<Self> for &NativeFloat64 {
|
|||||||
|
|
||||||
impl ops::Mul<Self> for &NativeFloat64 {
|
impl ops::Mul<Self> for &NativeFloat64 {
|
||||||
type Output = NativeFloat64;
|
type Output = NativeFloat64;
|
||||||
fn mul(self, _rhs: &NativeFloat64) -> Self::Output {
|
fn mul(self, rhs: &NativeFloat64) -> Self::Output { NativeFloat64(&self.0 * &rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Div<Self> for &NativeFloat64 {
|
impl ops::Div<Self> for &NativeFloat64 {
|
||||||
|
@ -144,9 +144,7 @@ impl ops::Neg for Rational {
|
|||||||
|
|
||||||
impl ops::Add for Rational {
|
impl ops::Add for Rational {
|
||||||
type Output = Rational;
|
type Output = Rational;
|
||||||
fn add(self, _rhs: Self) -> Self::Output {
|
fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Sub for Rational {
|
impl ops::Sub for Rational {
|
||||||
@ -165,9 +163,7 @@ impl ops::Mul for Rational {
|
|||||||
|
|
||||||
impl ops::Div for Rational {
|
impl ops::Div for Rational {
|
||||||
type Output = Rational;
|
type Output = Rational;
|
||||||
fn div(self, _rhs: Self) -> Self::Output {
|
fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Rem for Rational {
|
impl ops::Rem for Rational {
|
||||||
@ -267,9 +263,7 @@ impl ops::Sub<Self> for &Rational {
|
|||||||
|
|
||||||
impl ops::Mul<Self> for &Rational {
|
impl ops::Mul<Self> for &Rational {
|
||||||
type Output = Rational;
|
type Output = Rational;
|
||||||
fn mul(self, _rhs: &Rational) -> Self::Output {
|
fn mul(self, rhs: &Rational) -> Self::Output { Rational(&self.0 * &rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Div<Self> for &Rational {
|
impl ops::Div<Self> for &Rational {
|
||||||
|
@ -143,9 +143,7 @@ impl ops::Neg for Rational {
|
|||||||
|
|
||||||
impl ops::Add for Rational {
|
impl ops::Add for Rational {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn add(self, _rhs: Self) -> Self::Output {
|
fn add(self, rhs: Self) -> Self::Output { Self(self.0 + rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Sub for Rational {
|
impl ops::Sub for Rational {
|
||||||
@ -164,9 +162,7 @@ impl ops::Mul for Rational {
|
|||||||
|
|
||||||
impl ops::Div for Rational {
|
impl ops::Div for Rational {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn div(self, _rhs: Self) -> Self::Output {
|
fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Rem for Rational {
|
impl ops::Rem for Rational {
|
||||||
@ -266,9 +262,7 @@ impl ops::Sub<Self> for &Rational {
|
|||||||
|
|
||||||
impl ops::Mul<Self> for &Rational {
|
impl ops::Mul<Self> for &Rational {
|
||||||
type Output = Rational;
|
type Output = Rational;
|
||||||
fn mul(self, _rhs: Self) -> Self::Output {
|
fn mul(self, rhs: Self) -> Self::Output { Rational(rug::Rational::from(&self.0 * &rhs.0)) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Div<Self> for &Rational {
|
impl ops::Div<Self> for &Rational {
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use super::{NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder};
|
use super::{ExclusionMethod, NextPreferencesEntry, NextPreferencesResult, STVError, STVOptions, SumSurplusTransfersMode, SurplusMethod, SurplusOrder};
|
||||||
|
|
||||||
use crate::election::{Candidate, CountCard, CountState, Parcel, Vote};
|
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
|
||||||
use crate::numbers::Number;
|
use crate::numbers::Number;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@ -25,6 +25,34 @@ use itertools::Itertools;
|
|||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
|
||||||
|
/// Distribute first preference votes according to the Gregory method
|
||||||
|
pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
||||||
|
let votes = state.election.ballots.iter().map(|b| Vote {
|
||||||
|
ballot: b,
|
||||||
|
value: b.orig_value.clone(),
|
||||||
|
up_to_pref: 0,
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let result = super::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);
|
||||||
|
|
||||||
|
state.kind = None;
|
||||||
|
state.title = "First preferences".to_string();
|
||||||
|
state.logger.log_literal("First preferences distributed.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
/// Distribute the largest surplus according to the Gregory method, based on [STVOptions::surplus]
|
/// Distribute the largest surplus according to the Gregory method, based on [STVOptions::surplus]
|
||||||
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||||
where
|
where
|
||||||
@ -37,10 +65,11 @@ where
|
|||||||
.map(|c| (c, state.candidates.get(c).unwrap()))
|
.map(|c| (c, state.candidates.get(c).unwrap()))
|
||||||
.filter(|(_, cc)| &cc.votes > quota)
|
.filter(|(_, cc)| &cc.votes > quota)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
if !has_surplus.is_empty() {
|
||||||
let total_surpluses = has_surplus.iter()
|
let total_surpluses = has_surplus.iter()
|
||||||
.fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota);
|
.fold(N::new(), |acc, (_, cc)| acc + &cc.votes - quota);
|
||||||
|
|
||||||
if !has_surplus.is_empty() {
|
|
||||||
// Determine if surplues can be deferred
|
// Determine if surplues can be deferred
|
||||||
if opts.defer_surpluses {
|
if opts.defer_surpluses {
|
||||||
if super::can_defer_surpluses(state, opts, &total_surpluses) {
|
if super::can_defer_surpluses(state, opts, &total_surpluses) {
|
||||||
@ -311,3 +340,157 @@ where
|
|||||||
// Update loss by fraction
|
// Update loss by fraction
|
||||||
state.loss_fraction.transfer(&-checksum);
|
state.loss_fraction.transfer(&-checksum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform one stage of a candidate exclusion according to the Gregory method, based on [STVOptions::exclusion]
|
||||||
|
pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
||||||
|
where
|
||||||
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
|
{
|
||||||
|
// Used to give bulk excluded candidate the same order_elected
|
||||||
|
let order_excluded = state.num_excluded + 1;
|
||||||
|
|
||||||
|
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 ??!
|
||||||
|
if count_card.state != CandidateState::Excluded {
|
||||||
|
count_card.state = CandidateState::Excluded;
|
||||||
|
state.num_excluded += 1;
|
||||||
|
count_card.order_elected = -(order_excluded as isize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine votes to transfer in this stage
|
||||||
|
let mut votes = Vec::new();
|
||||||
|
let mut votes_remain;
|
||||||
|
let mut checksum = N::new();
|
||||||
|
|
||||||
|
match opts.exclusion {
|
||||||
|
ExclusionMethod::SingleStage => {
|
||||||
|
// Exclude in one round
|
||||||
|
for excluded_candidate in excluded_candidates.iter() {
|
||||||
|
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||||
|
votes.append(&mut count_card.parcels.concat());
|
||||||
|
count_card.parcels.clear();
|
||||||
|
|
||||||
|
// Update votes
|
||||||
|
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value);
|
||||||
|
checksum -= &votes_transferred;
|
||||||
|
count_card.transfer(&-votes_transferred);
|
||||||
|
}
|
||||||
|
votes_remain = false;
|
||||||
|
}
|
||||||
|
ExclusionMethod::ByValue => {
|
||||||
|
// Exclude by value
|
||||||
|
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();
|
||||||
|
|
||||||
|
votes_remain = false;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
let mut votes_transferred = N::new();
|
||||||
|
for vote in cand_votes.into_iter() {
|
||||||
|
if &vote.value / &vote.ballot.orig_value == max_value {
|
||||||
|
votes_transferred += &vote.value;
|
||||||
|
votes.push(vote);
|
||||||
|
} else {
|
||||||
|
remaining_votes.push(vote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !remaining_votes.is_empty() {
|
||||||
|
votes_remain = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave remaining votes with candidate (as one parcel)
|
||||||
|
count_card.parcels = vec![remaining_votes];
|
||||||
|
|
||||||
|
// Update votes
|
||||||
|
checksum -= &votes_transferred;
|
||||||
|
count_card.transfer(&-votes_transferred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExclusionMethod::ParcelsByOrder => {
|
||||||
|
// Exclude by parcel by order
|
||||||
|
if excluded_candidates.len() > 1 {
|
||||||
|
panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude");
|
||||||
|
}
|
||||||
|
|
||||||
|
let count_card = state.candidates.get_mut(excluded_candidates[0]).unwrap();
|
||||||
|
votes = count_card.parcels.remove(0);
|
||||||
|
votes_remain = !count_card.parcels.is_empty();
|
||||||
|
|
||||||
|
// Update votes
|
||||||
|
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value);
|
||||||
|
checksum -= &votes_transferred;
|
||||||
|
count_card.transfer(&-votes_transferred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !votes.is_empty() {
|
||||||
|
let value = &votes[0].value / &votes[0].ballot.orig_value;
|
||||||
|
|
||||||
|
// Count next preferences
|
||||||
|
let result = super::next_preferences(state, votes);
|
||||||
|
|
||||||
|
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 {:.dps2$}.", result.total_ballots, result.total_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
if let Some(dps) = opts.round_votes {
|
||||||
|
exhausted_transfers.floor_mut(dps);
|
||||||
|
}
|
||||||
|
state.exhausted.transfer(&exhausted_transfers);
|
||||||
|
checksum += exhausted_transfers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !votes_remain {
|
||||||
|
// Finalise candidate votes
|
||||||
|
for excluded_candidate in excluded_candidates.into_iter() {
|
||||||
|
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||||
|
checksum -= &count_card.votes;
|
||||||
|
count_card.transfers -= &count_card.votes;
|
||||||
|
count_card.votes = N::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ExclusionMethod::SingleStage = opts.exclusion {
|
||||||
|
} else {
|
||||||
|
state.logger.log_literal("Exclusion complete.".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update loss by fraction
|
||||||
|
state.loss_fraction.transfer(&-checksum);
|
||||||
|
}
|
||||||
|
301
src/stv/meek.rs
Normal file
301
src/stv/meek.rs
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
/* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use super::{STVError, STVOptions};
|
||||||
|
|
||||||
|
use crate::election::{Ballot, Candidate, CandidateState, CountCard, CountState, Election};
|
||||||
|
use crate::numbers::Number;
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
use std::cmp::max;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ops;
|
||||||
|
|
||||||
|
/// Ballot in a [BallotTree]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct BallotInTree<'b, N: Number> {
|
||||||
|
ballot: &'b Ballot<N>,
|
||||||
|
/// Index of the next preference to examine
|
||||||
|
up_to_pref: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tree-packed ballot representation
|
||||||
|
pub struct BallotTree<'t, N: Number> {
|
||||||
|
num_ballots: N,
|
||||||
|
ballots: Vec<BallotInTree<'t, N>>,
|
||||||
|
next_preferences: Option<Box<HashMap<&'t Candidate, BallotTree<'t, N>>>>,
|
||||||
|
next_exhausted: Option<Box<BallotTree<'t, N>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t, N: Number> BallotTree<'t, N> {
|
||||||
|
/// Construct a new empty [BallotTree]
|
||||||
|
fn new() -> Self {
|
||||||
|
BallotTree {
|
||||||
|
num_ballots: N::new(),
|
||||||
|
ballots: Vec::new(),
|
||||||
|
next_preferences: None,
|
||||||
|
next_exhausted: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Descend one level of the [BallotTree]
|
||||||
|
fn descend_tree(&mut self, candidates: &'t Vec<Candidate>) {
|
||||||
|
let mut next_preferences: HashMap<&Candidate, BallotTree<N>> = HashMap::new();
|
||||||
|
let mut next_exhausted = BallotTree::new();
|
||||||
|
|
||||||
|
for bit in self.ballots.iter() {
|
||||||
|
if bit.up_to_pref < bit.ballot.preferences.len() {
|
||||||
|
let candidate = &candidates[bit.ballot.preferences[bit.up_to_pref]];
|
||||||
|
|
||||||
|
if next_preferences.contains_key(candidate) {
|
||||||
|
let np_bt = next_preferences.get_mut(candidate).unwrap();
|
||||||
|
np_bt.num_ballots += &bit.ballot.orig_value;
|
||||||
|
np_bt.ballots.push(BallotInTree {
|
||||||
|
ballot: bit.ballot,
|
||||||
|
up_to_pref: bit.up_to_pref + 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let mut np_bt = BallotTree::new();
|
||||||
|
np_bt.num_ballots += &bit.ballot.orig_value;
|
||||||
|
np_bt.ballots.push(BallotInTree {
|
||||||
|
ballot: bit.ballot,
|
||||||
|
up_to_pref: bit.up_to_pref + 1,
|
||||||
|
});
|
||||||
|
next_preferences.insert(candidate, np_bt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Exhausted
|
||||||
|
next_exhausted.num_ballots += &bit.ballot.orig_value;
|
||||||
|
next_exhausted.ballots.push(bit.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.next_preferences = Some(Box::new(next_preferences));
|
||||||
|
self.next_exhausted = Some(Box::new(next_exhausted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialise keep values, ballot tree and distribute preferences
|
||||||
|
pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>)
|
||||||
|
where
|
||||||
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
|
{
|
||||||
|
// Initialise keep values
|
||||||
|
for (_, count_card) in state.candidates.iter_mut() {
|
||||||
|
count_card.keep_value = Some(N::one());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise ballot tree
|
||||||
|
let mut ballot_tree = BallotTree::new();
|
||||||
|
for ballot in state.election.ballots.iter() {
|
||||||
|
ballot_tree.ballots.push(BallotInTree {
|
||||||
|
ballot: ballot,
|
||||||
|
up_to_pref: 0,
|
||||||
|
});
|
||||||
|
ballot_tree.num_ballots += &ballot.orig_value;
|
||||||
|
}
|
||||||
|
state.ballot_tree = Some(ballot_tree);
|
||||||
|
|
||||||
|
// Distribute preferences
|
||||||
|
distribute_preferences(state);
|
||||||
|
|
||||||
|
// Recalculate transfers
|
||||||
|
for (_, count_card) in state.candidates.iter_mut() {
|
||||||
|
count_card.transfers.assign(&count_card.votes);
|
||||||
|
}
|
||||||
|
state.exhausted.transfers.assign(&state.exhausted.votes);
|
||||||
|
|
||||||
|
state.kind = None;
|
||||||
|
state.title = "First preferences".to_string();
|
||||||
|
state.logger.log_literal("First preferences distributed.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (Re)distribute preferences according to candidate keep values
|
||||||
|
pub fn distribute_preferences<N: Number>(state: &mut CountState<N>)
|
||||||
|
where
|
||||||
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
|
{
|
||||||
|
// Reset votes
|
||||||
|
for (_, count_card) in state.candidates.iter_mut() {
|
||||||
|
//count_card.orig_votes = N::new();
|
||||||
|
//count_card.transfers = N::new();
|
||||||
|
count_card.votes = N::new();
|
||||||
|
}
|
||||||
|
state.exhausted.votes = N::new();
|
||||||
|
|
||||||
|
distribute_recursively(&mut state.candidates, &mut state.exhausted, state.ballot_tree.as_mut().unwrap(), N::one(), &state.election);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Distribute preferences recursively
|
||||||
|
///
|
||||||
|
/// Called by [distribute_preferences]
|
||||||
|
fn distribute_recursively<'t, N: Number>(candidates: &mut HashMap<&'t Candidate, CountCard<N>>, exhausted: &mut CountCard<N>, tree: &mut BallotTree<'t, N>, remaining_multiplier: N, election: &'t Election<N>)
|
||||||
|
where
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
|
{
|
||||||
|
// Descend tree if required
|
||||||
|
if let None = tree.next_exhausted {
|
||||||
|
tree.descend_tree(&election.candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Possibility of infinite loop if malformed inputs?
|
||||||
|
|
||||||
|
// TODO: Round transfers?
|
||||||
|
|
||||||
|
// Credit votes at this level
|
||||||
|
for (candidate, cand_tree) in tree.next_preferences.as_mut().unwrap().as_mut().iter_mut() {
|
||||||
|
let count_card = candidates.get_mut(candidate).unwrap();
|
||||||
|
match count_card.state {
|
||||||
|
CandidateState::Hopeful | CandidateState::Guarded | CandidateState::Doomed => {
|
||||||
|
// Hopeful candidate has keep value 1, so transfer entire remaining value
|
||||||
|
count_card.votes += &remaining_multiplier * &cand_tree.num_ballots;
|
||||||
|
}
|
||||||
|
CandidateState::Elected => {
|
||||||
|
// Transfer according to elected candidate's keep value
|
||||||
|
count_card.votes += &remaining_multiplier * &cand_tree.num_ballots * count_card.keep_value.as_ref().unwrap();
|
||||||
|
let new_remaining_multiplier = &remaining_multiplier * &(N::one() - count_card.keep_value.as_ref().unwrap());
|
||||||
|
// Recurse
|
||||||
|
distribute_recursively(candidates, exhausted, cand_tree, new_remaining_multiplier, election);
|
||||||
|
}
|
||||||
|
CandidateState::Excluded | CandidateState::Withdrawn => {
|
||||||
|
// Excluded candidate has keep value 0, so skip over this candidate
|
||||||
|
// Recurse
|
||||||
|
distribute_recursively(candidates, exhausted, cand_tree, remaining_multiplier.clone(), election);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credit exhausted votes at this level
|
||||||
|
exhausted.votes += &remaining_multiplier * &tree.next_exhausted.as_ref().unwrap().as_ref().num_ballots;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recalculate all candidate keep factors to distribute all surpluses according to the Meek method
|
||||||
|
pub fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||||
|
where
|
||||||
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
|
{
|
||||||
|
// TODO: Make configurable
|
||||||
|
let quota_tolerance = N::one() / N::from(100000) + N::one();
|
||||||
|
|
||||||
|
let quota = state.quota.as_ref().unwrap();
|
||||||
|
let mut has_surplus: Vec<&Candidate> = state.election.candidates.iter() // Present in order in case of tie
|
||||||
|
.filter(|c| {
|
||||||
|
let count_card = state.candidates.get(c).unwrap();
|
||||||
|
return count_card.state == CandidateState::Elected && (&count_card.votes / quota > quota_tolerance);
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !has_surplus.is_empty() {
|
||||||
|
// TODO: Defer surpluses?
|
||||||
|
|
||||||
|
let orig_candidates = state.candidates.clone();
|
||||||
|
let orig_exhausted = state.exhausted.clone();
|
||||||
|
|
||||||
|
let mut num_iterations: u32 = 0;
|
||||||
|
|
||||||
|
while !has_surplus.is_empty() {
|
||||||
|
num_iterations += 1;
|
||||||
|
|
||||||
|
// Recompute keep values
|
||||||
|
for candidate in has_surplus.into_iter() {
|
||||||
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||||
|
count_card.keep_value = Some(count_card.keep_value.take().unwrap() * state.quota.as_ref().unwrap() / &count_card.votes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redistribute votes
|
||||||
|
distribute_preferences(state);
|
||||||
|
|
||||||
|
// Recompute quota if more ballots have become exhausted
|
||||||
|
super::calculate_quota(state, opts);
|
||||||
|
|
||||||
|
//println!("Debug {}", num_iterations);
|
||||||
|
|
||||||
|
let quota = state.quota.as_ref().unwrap();
|
||||||
|
has_surplus = state.election.candidates.iter()
|
||||||
|
.filter(|c| {
|
||||||
|
let count_card = state.candidates.get(c).unwrap();
|
||||||
|
return count_card.state == CandidateState::Elected && (&count_card.votes / quota > quota_tolerance);
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate transfers
|
||||||
|
for (candidate, count_card) in state.candidates.iter_mut() {
|
||||||
|
count_card.transfers = &count_card.votes - &orig_candidates.get(candidate).unwrap().votes;
|
||||||
|
}
|
||||||
|
state.exhausted.transfers = &state.exhausted.votes - &orig_exhausted.votes;
|
||||||
|
|
||||||
|
// Remove intermediate logs on quota calculation
|
||||||
|
state.logger.entries.clear();
|
||||||
|
|
||||||
|
state.kind = None;
|
||||||
|
state.title = "Surpluses distributed".to_string();
|
||||||
|
if num_iterations == 1 {
|
||||||
|
state.logger.log_literal("Surpluses distributed, requiring 1 iteration.".to_string());
|
||||||
|
} else {
|
||||||
|
state.logger.log_literal(format!("Surpluses distributed, requiring {} iterations.", num_iterations));
|
||||||
|
}
|
||||||
|
|
||||||
|
let kv_str = state.election.candidates.iter()
|
||||||
|
.map(|c| (c, state.candidates.get(c).unwrap()))
|
||||||
|
.filter(|(_, cc)| cc.state == CandidateState::Elected)
|
||||||
|
.sorted_unstable_by(|a, b| a.1.order_elected.cmp(&b.1.order_elected))
|
||||||
|
.map(|(c, cc)| format!("{} ({:.dps2$})", c.name, cc.keep_value.as_ref().unwrap(), dps2=max(opts.pp_decimals, 2)))
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
state.logger.log_literal(format!("Keep values of elected candidates are: {}.", kv_str));
|
||||||
|
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exclude the given candidates according to the Meek method
|
||||||
|
pub fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, _opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
||||||
|
where
|
||||||
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
|
{
|
||||||
|
// Used to give bulk excluded candidate the same order_elected
|
||||||
|
let order_excluded = state.num_excluded + 1;
|
||||||
|
|
||||||
|
for excluded_candidate in excluded_candidates.into_iter() {
|
||||||
|
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||||
|
|
||||||
|
if count_card.state != CandidateState::Excluded {
|
||||||
|
count_card.state = CandidateState::Excluded;
|
||||||
|
state.num_excluded += 1;
|
||||||
|
count_card.order_elected = -(order_excluded as isize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let orig_candidates = state.candidates.clone();
|
||||||
|
let orig_exhausted = state.exhausted.clone();
|
||||||
|
|
||||||
|
distribute_preferences(state);
|
||||||
|
|
||||||
|
// Recalculate transfers
|
||||||
|
for (candidate, count_card) in state.candidates.iter_mut() {
|
||||||
|
count_card.transfers = &count_card.votes - &orig_candidates.get(candidate).unwrap().votes;
|
||||||
|
}
|
||||||
|
state.exhausted.transfers = &state.exhausted.votes - &orig_exhausted.votes;
|
||||||
|
}
|
217
src/stv/mod.rs
217
src/stv/mod.rs
@ -17,20 +17,22 @@
|
|||||||
|
|
||||||
#![allow(mutable_borrow_reservation_conflict)]
|
#![allow(mutable_borrow_reservation_conflict)]
|
||||||
|
|
||||||
|
/// Gregory method of surplus distributions
|
||||||
pub mod gregory;
|
pub mod gregory;
|
||||||
|
/// Meek method of surplus distributions, etc.
|
||||||
|
pub mod meek;
|
||||||
|
|
||||||
//#[cfg(target_arch = "wasm32")]
|
//#[cfg(target_arch = "wasm32")]
|
||||||
pub mod wasm;
|
pub mod wasm;
|
||||||
|
|
||||||
use crate::numbers::Number;
|
use crate::numbers::Number;
|
||||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
|
use crate::election::{Candidate, CandidateState, CountCard, CountState, Vote};
|
||||||
use crate::sharandom::SHARandom;
|
use crate::sharandom::SHARandom;
|
||||||
use crate::ties::TieStrategy;
|
use crate::ties::TieStrategy;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
||||||
use std::cmp::max;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
|
||||||
@ -319,7 +321,11 @@ pub enum STVError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Distribute first preferences, and initialise other states such as the random number generator and tie-breaking rules
|
/// Distribute first preferences, and initialise other states such as the random number generator and tie-breaking rules
|
||||||
pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a STVOptions) {
|
pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a STVOptions)
|
||||||
|
where
|
||||||
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
|
{
|
||||||
// Initialise RNG
|
// Initialise RNG
|
||||||
for t in opts.ties.iter() {
|
for t in opts.ties.iter() {
|
||||||
if let TieStrategy::Random(seed) = t {
|
if let TieStrategy::Random(seed) = t {
|
||||||
@ -327,7 +333,7 @@ pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a ST
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
distribute_first_preferences(&mut state);
|
distribute_first_preferences(&mut state, opts);
|
||||||
calculate_quota(&mut state, opts);
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_quota(&mut state, opts);
|
elect_meeting_quota(&mut state, opts);
|
||||||
init_tiebreaks(&mut state, opts);
|
init_tiebreaks(&mut state, opts);
|
||||||
@ -337,6 +343,7 @@ pub fn count_init<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &'a ST
|
|||||||
pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
pub fn count_one_stage<'a, N: Number>(mut state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||||
where
|
where
|
||||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Neg<Output=N>,
|
for<'r> &'r N: ops::Neg<Output=N>,
|
||||||
{
|
{
|
||||||
@ -452,31 +459,19 @@ fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Distribute first preference votes
|
/// Distribute first preference votes
|
||||||
fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
fn distribute_first_preferences<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
|
||||||
let votes = state.election.ballots.iter().map(|b| Vote {
|
where
|
||||||
ballot: b,
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
value: b.orig_value.clone(),
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
up_to_pref: 0,
|
{
|
||||||
}).collect();
|
match opts.surplus {
|
||||||
|
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
||||||
let result = next_preferences(state, votes);
|
gregory::distribute_first_preferences(state);
|
||||||
|
}
|
||||||
// Transfer candidate votes
|
SurplusMethod::Meek => {
|
||||||
for (candidate, entry) in result.candidates.into_iter() {
|
meek::distribute_first_preferences(state);
|
||||||
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);
|
|
||||||
|
|
||||||
state.kind = None;
|
|
||||||
state.title = "First preferences".to_string();
|
|
||||||
state.logger.log_literal("First preferences distributed.".to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the quota, given the total vote, according to [STVOptions::quota]
|
/// Calculate the quota, given the total vote, according to [STVOptions::quota]
|
||||||
@ -514,7 +509,7 @@ fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N
|
|||||||
/// Calculate the quota according to [STVOptions::quota]
|
/// Calculate the quota according to [STVOptions::quota]
|
||||||
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||||
// Calculate quota
|
// Calculate quota
|
||||||
if let None = state.quota {
|
if state.quota.is_none() || opts.surplus == SurplusMethod::Meek {
|
||||||
let mut log = String::new();
|
let mut log = String::new();
|
||||||
|
|
||||||
// Calculate the total vote
|
// Calculate the total vote
|
||||||
@ -587,7 +582,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No ERS97 rules
|
// No ERS97 rules
|
||||||
if let None = state.vote_required_election {
|
if state.vote_required_election.is_none() || opts.surplus == SurplusMethod::Meek {
|
||||||
state.vote_required_election = state.quota.clone();
|
state.vote_required_election = state.quota.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -678,15 +673,16 @@ where
|
|||||||
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
fn distribute_surpluses<N: Number>(state: &mut CountState<N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||||
where
|
where
|
||||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Neg<Output=N>
|
for<'r> &'r N: ops::Neg<Output=N>,
|
||||||
{
|
{
|
||||||
match opts.surplus {
|
match opts.surplus {
|
||||||
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
||||||
return gregory::distribute_surpluses(state, opts);
|
return gregory::distribute_surpluses(state, opts);
|
||||||
}
|
}
|
||||||
SurplusMethod::Meek => {
|
SurplusMethod::Meek => {
|
||||||
todo!();
|
return meek::distribute_surpluses(state, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -784,6 +780,8 @@ fn hopefuls_to_bulk_exclude<'a, N: Number>(state: &CountState<'a, N>, _opts: &ST
|
|||||||
/// Exclude the lowest-ranked hopeful candidate(s)
|
/// Exclude the lowest-ranked hopeful candidate(s)
|
||||||
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
fn exclude_hopefuls<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> Result<bool, STVError>
|
||||||
where
|
where
|
||||||
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
{
|
{
|
||||||
let mut excluded_candidates: Vec<&Candidate> = Vec::new();
|
let mut excluded_candidates: Vec<&Candidate> = Vec::new();
|
||||||
@ -831,6 +829,8 @@ where
|
|||||||
/// Continue the exclusion of a candidate who is being excluded
|
/// Continue the exclusion of a candidate who is being excluded
|
||||||
fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
|
fn continue_exclusion<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions) -> bool
|
||||||
where
|
where
|
||||||
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
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
|
// Cannot filter by raw vote count, as candidates may have 0.00 votes but still have recorded ballot papers
|
||||||
@ -867,155 +867,18 @@ where
|
|||||||
/// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion]
|
/// Perform one stage of a candidate exclusion, according to [STVOptions::exclusion]
|
||||||
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
fn exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
|
||||||
where
|
where
|
||||||
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
{
|
{
|
||||||
// Used to give bulk excluded candidate the same order_elected
|
match opts.surplus {
|
||||||
let order_excluded = state.num_excluded + 1;
|
SurplusMethod::WIG | SurplusMethod::UIG | SurplusMethod::EG => {
|
||||||
|
gregory::exclude_candidates(state, opts, excluded_candidates);
|
||||||
for excluded_candidate in excluded_candidates.iter() {
|
}
|
||||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
SurplusMethod::Meek => {
|
||||||
|
meek::exclude_candidates(state, opts, excluded_candidates);
|
||||||
// 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 = -(order_excluded as isize);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine votes to transfer in this stage
|
|
||||||
let mut votes = Vec::new();
|
|
||||||
let mut votes_remain;
|
|
||||||
let mut checksum = N::new();
|
|
||||||
|
|
||||||
match opts.exclusion {
|
|
||||||
ExclusionMethod::SingleStage => {
|
|
||||||
// Exclude in one round
|
|
||||||
for excluded_candidate in excluded_candidates.iter() {
|
|
||||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
|
||||||
votes.append(&mut count_card.parcels.concat());
|
|
||||||
count_card.parcels.clear();
|
|
||||||
|
|
||||||
// Update votes
|
|
||||||
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value);
|
|
||||||
checksum -= &votes_transferred;
|
|
||||||
count_card.transfer(&-votes_transferred);
|
|
||||||
}
|
|
||||||
votes_remain = false;
|
|
||||||
}
|
|
||||||
ExclusionMethod::ByValue => {
|
|
||||||
// Exclude by value
|
|
||||||
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();
|
|
||||||
|
|
||||||
votes_remain = false;
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
let mut votes_transferred = N::new();
|
|
||||||
for vote in cand_votes.into_iter() {
|
|
||||||
if &vote.value / &vote.ballot.orig_value == max_value {
|
|
||||||
votes_transferred += &vote.value;
|
|
||||||
votes.push(vote);
|
|
||||||
} else {
|
|
||||||
remaining_votes.push(vote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !remaining_votes.is_empty() {
|
|
||||||
votes_remain = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leave remaining votes with candidate (as one parcel)
|
|
||||||
count_card.parcels = vec![remaining_votes];
|
|
||||||
|
|
||||||
// Update votes
|
|
||||||
checksum -= &votes_transferred;
|
|
||||||
count_card.transfer(&-votes_transferred);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ExclusionMethod::ParcelsByOrder => {
|
|
||||||
// Exclude by parcel by order
|
|
||||||
if excluded_candidates.len() > 1 {
|
|
||||||
panic!("--exclusion parcels_by_order is incompatible with --bulk-exclude");
|
|
||||||
}
|
|
||||||
|
|
||||||
let count_card = state.candidates.get_mut(excluded_candidates[0]).unwrap();
|
|
||||||
votes = count_card.parcels.remove(0);
|
|
||||||
votes_remain = !count_card.parcels.is_empty();
|
|
||||||
|
|
||||||
// Update votes
|
|
||||||
let votes_transferred = votes.iter().fold(N::new(), |acc, v| acc + &v.value);
|
|
||||||
checksum -= &votes_transferred;
|
|
||||||
count_card.transfer(&-votes_transferred);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !votes.is_empty() {
|
|
||||||
let value = &votes[0].value / &votes[0].ballot.orig_value;
|
|
||||||
|
|
||||||
// Count next preferences
|
|
||||||
let result = next_preferences(state, votes);
|
|
||||||
|
|
||||||
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 {:.dps2$}.", result.total_ballots, result.total_votes, value, dps=opts.pp_decimals, dps2=max(opts.pp_decimals, 2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
if let Some(dps) = opts.round_votes {
|
|
||||||
exhausted_transfers.floor_mut(dps);
|
|
||||||
}
|
|
||||||
state.exhausted.transfer(&exhausted_transfers);
|
|
||||||
checksum += exhausted_transfers;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !votes_remain {
|
|
||||||
// Finalise candidate votes
|
|
||||||
for excluded_candidate in excluded_candidates.into_iter() {
|
|
||||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
|
||||||
checksum -= &count_card.votes;
|
|
||||||
count_card.transfers -= &count_card.votes;
|
|
||||||
count_card.votes = N::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let ExclusionMethod::SingleStage = opts.exclusion {
|
|
||||||
} else {
|
|
||||||
state.logger.log_literal("Exclusion complete.".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update loss by fraction
|
|
||||||
state.loss_fraction.transfer(&-checksum);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine if the count is complete because the number of elected candidates equals the number of vacancies
|
/// Determine if the count is complete because the number of elected candidates equals the number of vacancies
|
||||||
|
@ -26,6 +26,8 @@ extern crate console_error_panic_hook;
|
|||||||
use js_sys::Array;
|
use js_sys::Array;
|
||||||
use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
|
use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
|
||||||
|
|
||||||
|
use std::cmp::max;
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
|
|
||||||
/// Wrapper for [Fixed::set_dps]
|
/// Wrapper for [Fixed::set_dps]
|
||||||
@ -122,8 +124,8 @@ macro_rules! impl_type {
|
|||||||
/// Wrapper for [final_result_summary]
|
/// Wrapper for [final_result_summary]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<final_result_summary_$type>](state: &[<CountState$type>]) -> String {
|
pub fn [<final_result_summary_$type>](state: &[<CountState$type>], opts: &STVOptions) -> String {
|
||||||
return final_result_summary(&state.0);
|
return final_result_summary(&state.0, &opts.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper structs
|
// Wrapper structs
|
||||||
@ -336,20 +338,24 @@ fn finalise_results_table<N: Number>(state: &CountState<N>) -> Array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the final lead-out text summarising the result of the election
|
/// Generate the final lead-out text summarising the result of the election
|
||||||
fn final_result_summary<N: Number>(state: &CountState<N>) -> String {
|
fn final_result_summary<N: Number>(state: &CountState<N>, opts: &stv::STVOptions) -> String {
|
||||||
let mut result = String::from("<p>Count complete. The winning candidates are, in order of election:</p><ol>");
|
let mut result = String::from("<p>Count complete. The winning candidates are, in order of election:</p><ol>");
|
||||||
|
|
||||||
let mut winners = Vec::new();
|
let mut winners = Vec::new();
|
||||||
for (candidate, count_card) in state.candidates.iter() {
|
for (candidate, count_card) in state.candidates.iter() {
|
||||||
if count_card.state == CandidateState::Elected {
|
if count_card.state == CandidateState::Elected {
|
||||||
winners.push((candidate, count_card.order_elected));
|
winners.push((candidate, count_card.order_elected, &count_card.keep_value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
winners.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
winners.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||||
|
|
||||||
for (winner, _) in winners.into_iter() {
|
for (winner, _, kv_opt) in winners.into_iter() {
|
||||||
|
if let Some(kv) = kv_opt {
|
||||||
|
result.push_str(&format!("<li>{} (<i>kv</i> = {:.dps2$})</li>", winner.name, kv, dps2=max(opts.pp_decimals, 2)));
|
||||||
|
} else {
|
||||||
result.push_str(&format!("<li>{}</li>", winner.name));
|
result.push_str(&format!("<li>{}</li>", winner.name));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result.push_str("</ol>");
|
result.push_str("</ol>");
|
||||||
return result;
|
return result;
|
||||||
|
@ -55,6 +55,7 @@ pub fn read_validate_election<N: Number>(csv_file: &str, blt_file: &str, stv_opt
|
|||||||
pub fn validate_election<N: Number>(stages: Vec<usize>, records: Vec<StringRecord>, election: Election<N>, stv_opts: stv::STVOptions)
|
pub fn validate_election<N: Number>(stages: Vec<usize>, records: Vec<StringRecord>, election: Election<N>, stv_opts: stv::STVOptions)
|
||||||
where
|
where
|
||||||
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
|
||||||
|
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
for<'r> &'r N: ops::Div<&'r N, Output=N>,
|
||||||
for<'r> &'r N: ops::Neg<Output=N>,
|
for<'r> &'r N: ops::Neg<Output=N>,
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user