From d144ab0cb416f5b5a5a826d9fc008456fe047b51 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 18 Jul 2021 20:01:35 +1000 Subject: [PATCH] Implement ERS76 rules --- docs/options.md | 3 ++- html/index.html | 2 ++ html/index.js | 25 +++++++++++++++++++++++++ src/main.rs | 2 +- src/stv/mod.rs | 15 +++++++++------ src/stv/wasm.rs | 4 ++-- 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/docs/options.md b/docs/options.md index 30de07f..1a84cc0 100644 --- a/docs/options.md +++ b/docs/options.md @@ -14,7 +14,8 @@ The preset dropdown allows you to choose from a hardcoded list of preloaded STV * [*Wright STV*](https://www.aph.gov.au/Parliamentary_Business/Committees/House_of_Representatives_Committees?url=em/elect07/subs/sub051.1.pdf): Rules proposed by Anthony van der Craats designed for computer counting, involving reset and re-iteration of the count after each candidate exclusion. Validated against the [EVE Online reference implementation](https://github.com/ccpgames/ccp-wright-stv) for the [CSM 15 election](https://www.eveonline.com/news/view/meet-the-new-council). * [*PRSA 1977*](https://www.prsa.org.au/rule1977.htm): Simple rules designed for hand counting, using the exclusive Gregory method, with counting automatically performed in thousandths of a vote. Validated against [example 1](https://www.prsa.org.au/example1.pdf) of the PRSA's [*Proportional Representation Manual*](https://www.prsa.org.au/publicat.htm#p2). * [*ERS97*](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/): More complex rules designed for hand counting, using the exclusive Gregory method. Validated against the ERS97 model election. - * *ERS73*: Former rules from the 1973 1st edition. The quota is calculated always to 2 decimal places – for full ERS73 compliance, set *Round quota to 0 d.p.* when the quota is 100 or more. + * *ERS76*: Former rules from the 1976 2nd edition. The quota is always calculated to 2 decimal places – for full ERS76 compliance, set *Round quota to 0 d.p.* when the quota is more than 100. + * *ERS73*: Former rules from the 1973 1st edition. The quota is always calculated to 2 decimal places – for full ERS73 compliance, set *Round quota to 0 d.p.* when the quota is 100 or more. This functionality is not available on the command line. diff --git a/html/index.html b/html/index.html index 5eb1c29..9112b9c 100644 --- a/html/index.html +++ b/html/index.html @@ -44,6 +44,7 @@ + @@ -79,6 +80,7 @@ + diff --git a/html/index.js b/html/index.js index e5e2209..8bf1507 100644 --- a/html/index.js +++ b/html/index.js @@ -548,6 +548,31 @@ function changePreset() { document.getElementById('selPapers').value = 'transferable'; document.getElementById('selExclusion').value = 'by_value'; document.getElementById('selTies').value = 'forwards,random'; + } else if (document.getElementById('selPreset').value === 'ers76') { + document.getElementById('selQuotaCriterion').value = 'geq'; + document.getElementById('selQuota').value = 'droop_exact'; + document.getElementById('selQuotaMode').value = 'ers76'; + document.getElementById('chkBulkElection').checked = true; + document.getElementById('chkBulkExclusion').checked = true; + document.getElementById('chkDeferSurpluses').checked = true; + 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 = '2'; + document.getElementById('chkRoundVotes').checked = true; + document.getElementById('txtRoundVotes').value = '2'; + document.getElementById('chkRoundTVs').checked = true; + document.getElementById('txtRoundTVs').value = '2'; + document.getElementById('chkRoundWeights').checked = true; + document.getElementById('txtRoundWeights').value = '2'; + document.getElementById('selSumTransfers').value = 'single_step'; + document.getElementById('selSurplus').value = 'by_size'; + document.getElementById('selTransfers').value = 'eg'; + document.getElementById('selPapers').value = 'transferable'; + document.getElementById('selExclusion').value = 'by_value'; + document.getElementById('selTies').value = 'forwards,random'; } else if (document.getElementById('selPreset').value === 'ers73') { document.getElementById('selQuotaCriterion').value = 'geq'; document.getElementById('selQuota').value = 'droop_exact'; diff --git a/src/main.rs b/src/main.rs index 8757afc..4b46438 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,7 +104,7 @@ struct STV { quota_criterion: String, /// Whether to apply a form of progressive quota - #[clap(help_heading=Some("QUOTA"), long, possible_values=&["static", "ers97"], default_value="static", value_name="mode")] + #[clap(help_heading=Some("QUOTA"), long, possible_values=&["static", "ers97", "ers76"], default_value="static", value_name="mode")] quota_mode: String, // ------------------ diff --git a/src/stv/mod.rs b/src/stv/mod.rs index 61701a1..3033c15 100644 --- a/src/stv/mod.rs +++ b/src/stv/mod.rs @@ -144,6 +144,7 @@ impl STVOptions { quota_mode: match quota_mode { "static" => QuotaMode::Static, "ers97" => QuotaMode::ERS97, + "ers76" => QuotaMode::ERS76, _ => panic!("Invalid --quota-mode"), }, ties: ties.into_iter().map(|t| match t.as_str() { @@ -315,6 +316,8 @@ pub enum QuotaMode { Static, /// Static quota with ERS97 rules ERS97, + /// Static quota with ERS76 rules + ERS76, } impl QuotaMode { @@ -323,6 +326,7 @@ impl QuotaMode { match self { QuotaMode::Static => "--quota-mode static", QuotaMode::ERS97 => "--quota-mode ers97", + QuotaMode::ERS76 => "--quota-mode ers76", }.to_string() } } @@ -664,8 +668,8 @@ fn calculate_quota(state: &mut CountState, opts: &STVOptions) { state.logger.log_literal(log); } - if let QuotaMode::ERS97 = opts.quota_mode { - // ERS97 rules + if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 { + // ERS97/ERS76 rules // ------------------------- // Reduce quota if allowable @@ -722,7 +726,7 @@ fn calculate_quota(state: &mut CountState, opts: &STVOptions) { } } } else { - // No ERS97 rules + // No ERS97/ERS76 rules if state.vote_required_election.is_none() || opts.surplus == SurplusMethod::Meek { state.vote_required_election = state.quota.clone(); } @@ -789,10 +793,9 @@ fn elect_meeting_quota<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVO if opts.quota_mode == QuotaMode::ERS97 { // Vote required for election may have changed + // This is not performed in ERS76 - TODO: Check this calculate_quota(state, opts); - } - - if opts.quota_mode == QuotaMode::ERS97 { + // Repeat in case vote required for election has changed match elect_meeting_quota(state, opts) { Ok(_) => {} diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs index 13a972c..9950003 100644 --- a/src/stv/wasm.rs +++ b/src/stv/wasm.rs @@ -292,7 +292,7 @@ fn init_results_table(election: &Election, opts: &stv::STVOptions) result.push_str(&format!(r#"{}"#, candidate.name)); } result.push_str(r#"ExhaustedLoss by fractionTotalQuota"#); - if opts.quota_mode == stv::QuotaMode::ERS97 { + if opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76 { result.push_str(r#"Vote required for election"#); } return result; @@ -354,7 +354,7 @@ fn update_results_table(stage_num: usize, state: &CountState, opts result.push(&format!(r#"{}"#, tdclasses2, pp(&total_vote, 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 { + if opts.quota_mode == stv::QuotaMode::ERS97 || opts.quota_mode == stv::QuotaMode::ERS76 { result.push(&format!(r#"{}"#, tdclasses2, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into()); }