diff --git a/html/index.html b/html/index.html index 23ad0f2..9d446b7 100644 --- a/html/index.html +++ b/html/index.html @@ -41,7 +41,7 @@ Meek STV (2006) Meek STV (New Zealand) Australian Senate STV - + Wright STV PRSA 1977 ERS97 @@ -112,7 +112,7 @@ Single stage By value By parcel (by order) - + Wright method (re-iterate) diff --git a/html/index.js b/html/index.js index 34de336..398d187 100644 --- a/html/index.js +++ b/html/index.js @@ -460,6 +460,28 @@ function changePreset() { document.getElementById('selPapers').value = 'both'; document.getElementById('selExclusion').value = 'by_value'; document.getElementById('selTies').value = 'backwards,random'; + } else if (document.getElementById('selPreset').value === 'wright') { + document.getElementById('selQuotaCriterion').value = 'geq'; + document.getElementById('selQuota').value = 'droop'; + document.getElementById('selQuotaMode').value = 'static'; + //document.getElementById('chkBulkElection').checked = true; + document.getElementById('chkBulkExclusion').checked = true; + 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 = true; + document.getElementById('txtRoundQuota').value = '0'; + 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 = 'wig'; + document.getElementById('selPapers').value = 'both'; + document.getElementById('selExclusion').value = 'wright'; + document.getElementById('selTies').value = 'random'; } else if (document.getElementById('selPreset').value === 'prsa77') { document.getElementById('selQuotaCriterion').value = 'geq'; document.getElementById('selQuota').value = 'droop'; diff --git a/html/main.css b/html/main.css index 9c337b5..a5621e0 100644 --- a/html/main.css +++ b/html/main.css @@ -106,6 +106,10 @@ tr.stage-no td:not(:empty), tr.transfers td { tr.info:last-child td, .bb { border-bottom: 1px solid #76858c; } +.blw { + /* Used to separate counts in Wright STV */ + border-left: 2px solid #76858c; +} /* Table stripes */ diff --git a/src/stv/gregory.rs b/src/stv/gregory.rs index 31b7ac1..095d182 100644 --- a/src/stv/gregory.rs +++ b/src/stv/gregory.rs @@ -63,7 +63,8 @@ where let quota = state.quota.as_ref().unwrap(); let mut has_surplus: Vec<(&Candidate, &CountCard)> = state.election.candidates.iter() // Present in order in case of tie .map(|c| (c, state.candidates.get(c).unwrap())) - .filter(|(_, cc)| &cc.votes > quota) + //.filter(|(_, cc)| &cc.votes > quota) + .filter(|(_, cc)| &cc.votes > quota && cc.parcels.iter().any(|p| !p.is_empty())) .collect(); if !has_surplus.is_empty() { @@ -337,6 +338,8 @@ where count_card.votes.assign(state.quota.as_ref().unwrap()); checksum -= surplus; + count_card.parcels.clear(); // Mark surpluses as done + // Update loss by fraction state.loss_fraction.transfer(&-checksum); } @@ -435,6 +438,7 @@ where checksum -= &votes_transferred; count_card.transfer(&-votes_transferred); } + _ => panic!() } if !votes.is_empty() { @@ -494,3 +498,59 @@ where // Update loss by fraction state.loss_fraction.transfer(&-checksum); } + +/// Perform one stage of a candidate exclusion according to the Wright method +pub fn wright_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>, + 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); + } + } + + // Reset count + for (_, count_card) in state.candidates.iter_mut() { + if count_card.order_elected > 0 { + count_card.order_elected = 0; + } + count_card.parcels.clear(); + count_card.votes = N::new(); + count_card.transfers = N::new(); + count_card.state = match count_card.state { + CandidateState::Withdrawn => CandidateState::Withdrawn, + CandidateState::Excluded => CandidateState::Excluded, + _ => CandidateState::Hopeful, + }; + } + + state.exhausted.votes = N::new(); + state.exhausted.transfers = N::new(); + state.loss_fraction.votes = N::new(); + state.loss_fraction.transfers = N::new(); + + state.num_elected = 0; + + let orig_title = state.title.clone(); + + // Redistribute first preferences + super::distribute_first_preferences(state, opts); + + state.kind = Some("Exclusion of"); + state.title = orig_title; + + // Trigger recalculation of quota within stv::count_one_stage + state.quota = None; + state.vote_required_election = None; +} diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 28e1913..4a3edc3 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -160,6 +160,7 @@ impl STVOptions { "single_stage" => ExclusionMethod::SingleStage, "by_value" => ExclusionMethod::ByValue, "parcels_by_order" => ExclusionMethod::ParcelsByOrder, + "wright" => ExclusionMethod::Wright, _ => panic!("Invalid --exclusion"), }, meek_nz_exclusion, @@ -360,6 +361,8 @@ pub enum ExclusionMethod { ByValue, /// Transfer the ballot papers of an excluded candidate parcel by parcel in the order received ParcelsByOrder, + /// Wright method (re-iterate) + Wright, } impl ExclusionMethod { @@ -369,6 +372,7 @@ impl ExclusionMethod { ExclusionMethod::SingleStage => "--exclusion single_stage", ExclusionMethod::ByValue => "--exclusion by_value", ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order", + ExclusionMethod::Wright => "--exclusion wright", }.to_string() } } @@ -952,6 +956,8 @@ where // Exclusion in parts compatible only with Gregory method gregory::exclude_candidates(state, opts, excluded_candidates); } + ExclusionMethod::Wright => { + gregory::wright_exclude_candidates(state, opts, excluded_candidates); } } } diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index e13b44b..89573a9 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -285,43 +285,52 @@ fn init_results_table(election: &Election, opts: &stv::STVOptions) /// Generate subsequent columns of the HTML results table fn update_results_table(stage_num: usize, state: &CountState, opts: &stv::STVOptions) -> Array { let result = Array::new(); - result.push(&format!(r#"{}"#, stage_num).into()); - result.push(&format!(r#"{}"#, state.kind.unwrap_or("")).into()); - result.push(&format!(r#"{}"#, state.title).into()); + + // Insert borders to left of new exclusions in Wright STV + let mut tdclasses1 = ""; + let mut tdclasses2 = ""; + if opts.exclusion == stv::ExclusionMethod::Wright && state.kind == Some("Exclusion of") { + tdclasses1 = r#" class="blw""#; + tdclasses2 = r#"blw "#; + } + + result.push(&format!(r#"{}"#, tdclasses1, stage_num).into()); + result.push(&format!(r#"{}"#, tdclasses1, state.kind.unwrap_or("")).into()); + result.push(&format!(r#"{}"#, tdclasses1, state.title).into()); for candidate in state.election.candidates.iter() { let count_card = state.candidates.get(candidate).unwrap(); if count_card.state == stv::CandidateState::Elected { - result.push(&format!(r#"{}"#, pp(&count_card.transfers, opts.pp_decimals)).into()); - result.push(&format!(r#"{}"#, pp(&count_card.votes, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into()); } else if count_card.state == stv::CandidateState::Excluded { - result.push(&format!(r#"{}"#, pp(&count_card.transfers, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into()); if count_card.votes.is_zero() { - result.push(&r#"Ex"#.into()); + result.push(&format!(r#"Ex"#, tdclasses2).into()); } else { - result.push(&format!(r#"{}"#, pp(&count_card.votes, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into()); } } else if count_card.state == stv::CandidateState::Withdrawn { - result.push(&r#""#.into()); - result.push(&r#"WD"#.into()); + result.push(&format!(r#""#, tdclasses2).into()); + result.push(&format!(r#"WD"#, tdclasses2).into()); } else { - result.push(&format!(r#"{}"#, pp(&count_card.transfers, opts.pp_decimals)).into()); - result.push(&format!(r#"{}"#, pp(&count_card.votes, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into()); } } - result.push(&format!(r#"{}"#, pp(&state.exhausted.transfers, opts.pp_decimals)).into()); - result.push(&format!(r#"{}"#, pp(&state.exhausted.votes, opts.pp_decimals)).into()); - result.push(&format!(r#"{}"#, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into()); - result.push(&format!(r#"{}"#, pp(&state.loss_fraction.votes, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(&state.exhausted.transfers, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(&state.exhausted.votes, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(&state.loss_fraction.votes, opts.pp_decimals)).into()); // Calculate total votes let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); total_vote += &state.exhausted.votes; total_vote += &state.loss_fraction.votes; - result.push(&format!(r#"{}"#, pp(&total_vote, opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into()); - result.push(&format!(r#"{}"#, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into()); if opts.quota_mode == stv::QuotaMode::ERS97 { - result.push(&format!(r#"{}"#, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into()); + result.push(&format!(r#"{}"#, tdclasses2, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into()); } return result;