diff --git a/docs/options.md b/docs/options.md
index 3e41fd3..3f46ce9 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -137,6 +137,14 @@ When ballots are normalised, a set of preferences with weight *n* > 1 is instead
## Count optimisations
+## Early bulk election (--no-early-bulk-elect)
+
+When early bulk election is enabled (default), all remaining candidates are declared elected in a single stage as soon as the number of not-excluded candidates exactly equals the number of vacancies to fill. Further surplus distributions are not performed, and outstanding exclusions, if any, are not completed. This is typical of most STV rules.
+
+When early bulk election is disabled, surpluses continue to be distributed, and outstanding exclusions continue to be completed, even once the number of not-excluded candidates exactly equals the number of vacancies to fill. Bulk election is performed only once there are no more surpluses to distribute, and no exclusions to complete.
+
+In either case, candidates are declared elected in descending order of votes. This ensures that only one candidate is ever elected at a time and the order of election is well-defined, which is required e.g. for some affirmative action rules.
+
### Bulk exclusion (--bulk-exclude)
When bulk exclusion is disabled (default), only one candidate is ever excluded per stage.
diff --git a/html/index.html b/html/index.html
index 6825018..50ab513 100644
--- a/html/index.html
+++ b/html/index.html
@@ -179,7 +179,7 @@
Count optimisations:
-
+
Early bulk election
diff --git a/html/index.js b/html/index.js
index 398d187..609f641 100644
--- a/html/index.js
+++ b/html/index.js
@@ -115,6 +115,7 @@ async function clickCount() {
document.getElementById('selPapers').value == 'transferable',
document.getElementById('selExclusion').value,
document.getElementById('chkMeekNZExclusion').checked,
+ document.getElementById('chkBulkElection').checked,
document.getElementById('chkBulkExclusion').checked,
document.getElementById('chkDeferSurpluses').checked,
document.getElementById('chkMeekImmediateElect').checked,
@@ -318,7 +319,7 @@ function changePreset() {
document.getElementById('selQuotaCriterion').value = 'gt';
document.getElementById('selQuota').value = 'droop_exact';
document.getElementById('selQuotaMode').value = 'static';
- //document.getElementById('chkBulkElection').checked = true;
+ document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'rational';
@@ -338,7 +339,7 @@ function changePreset() {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
- //document.getElementById('chkBulkElection').checked = true;
+ document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'fixed';
@@ -361,7 +362,7 @@ function changePreset() {
document.getElementById('selQuotaCriterion').value = 'gt';
document.getElementById('selQuota').value = 'droop_exact';
document.getElementById('selQuotaMode').value = 'static';
- //document.getElementById('chkBulkElection').checked = true;
+ document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('chkMeekImmediateElect').checked = false;
@@ -385,7 +386,7 @@ function changePreset() {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
- //document.getElementById('chkBulkElection').checked = true;
+ document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('chkDeferSurpluses').checked = true;
document.getElementById('chkMeekImmediateElect').checked = true;
@@ -413,7 +414,7 @@ function changePreset() {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
- //document.getElementById('chkBulkElection').checked = true;
+ document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('chkDeferSurpluses').checked = true;
document.getElementById('chkMeekImmediateElect').checked = true;
@@ -441,7 +442,7 @@ function changePreset() {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
- //document.getElementById('chkBulkElection').checked = true;
+ document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = true;
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'fixed';
@@ -464,7 +465,7 @@ function changePreset() {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
- //document.getElementById('chkBulkElection').checked = true;
+ document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = true;
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'fixed';
@@ -486,7 +487,7 @@ function changePreset() {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
- //document.getElementById('chkBulkElection').checked = true;
+ document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('chkDeferSurpluses').checked = true;
document.getElementById('selNumbers').value = 'fixed';
@@ -511,7 +512,7 @@ function changePreset() {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop_exact';
document.getElementById('selQuotaMode').value = 'ers97';
- //document.getElementById('chkBulkElection').checked = true;
+ document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = true;
document.getElementById('chkDeferSurpluses').checked = true;
document.getElementById('selNumbers').value = 'fixed';
diff --git a/src/main.rs b/src/main.rs
index 1a25dc6..0fcdc08 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -140,6 +140,10 @@ struct STV {
// -------------------------
// -- Count optimisations --
+ /// Continue count even if continuing candidates fill all remaining vacancies
+ #[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
+ no_early_bulk_elect: bool,
+
/// Use bulk exclusion
#[clap(help_heading=Some("COUNT OPTIMISATIONS"), long)]
bulk_exclude: bool,
@@ -221,6 +225,7 @@ where
cmd_opts.transferable_only,
&cmd_opts.exclusion,
cmd_opts.meek_nz_exclusion,
+ !cmd_opts.no_early_bulk_elect,
cmd_opts.bulk_exclude,
cmd_opts.defer_surpluses,
cmd_opts.meek_immediate_elect,
diff --git a/src/stv/mod.rs b/src/stv/mod.rs
index 4a3edc3..73fcc55 100644
--- a/src/stv/mod.rs
+++ b/src/stv/mod.rs
@@ -71,6 +71,8 @@ pub struct STVOptions {
pub exclusion: ExclusionMethod,
/// (Meek STV) NZ Meek STV behaviour: Iterate keep values one round before candidate exclusion
pub meek_nz_exclusion: bool,
+ /// Bulk elect as soon as continuing candidates fill all remaining vacancies
+ pub early_bulk_elect: bool,
/// Use bulk exclusion
pub bulk_exclude: bool,
/// Defer surplus distributions if possible
@@ -101,6 +103,7 @@ impl STVOptions {
transferable_only: bool,
exclusion: &str,
meek_nz_exclusion: bool,
+ early_bulk_elect: bool,
bulk_exclude: bool,
defer_surpluses: bool,
meek_immediate_elect: bool,
@@ -164,6 +167,7 @@ impl STVOptions {
_ => panic!("Invalid --exclusion"),
},
meek_nz_exclusion,
+ early_bulk_elect,
bulk_exclude,
defer_surpluses,
meek_immediate_elect,
@@ -193,6 +197,7 @@ impl STVOptions {
if self.surplus != SurplusMethod::Meek && self.transferable_only { flags.push("--transferable-only".to_string()); }
if self.surplus != SurplusMethod::Meek && self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); }
if self.surplus == SurplusMethod::Meek && self.meek_nz_exclusion { flags.push("--meek-nz-exclusion".to_string()); }
+ if !self.early_bulk_elect { flags.push("--no-early-bulk-elect".to_string()); }
if self.bulk_exclude { flags.push("--bulk-exclude".to_string()); }
if self.defer_surpluses { flags.push("--defer-surpluses".to_string()); }
if self.surplus == SurplusMethod::Meek && self.meek_immediate_elect { flags.push("--meek-immediate-elect".to_string()); }
@@ -422,6 +427,13 @@ where
return Ok(true);
}
+ // Attempt early bulk election
+ if opts.early_bulk_elect {
+ if bulk_elect(&mut state, &opts)? {
+ return Ok(false);
+ }
+ }
+
// Continue exclusions
if continue_exclusion(&mut state, &opts) {
calculate_quota(&mut state, opts);
@@ -438,7 +450,7 @@ where
return Ok(false);
}
- // Attempt bulk election
+ // Attempt late bulk election
if bulk_elect(&mut state, &opts)? {
return Ok(false);
}
diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs
index 89573a9..1e96fdb 100644
--- a/src/stv/wasm.rs
+++ b/src/stv/wasm.rs
@@ -200,6 +200,7 @@ impl STVOptions {
transferable_only: bool,
exclusion: &str,
meek_nz_exclusion: bool,
+ early_bulk_elect: bool,
bulk_exclude: bool,
defer_surpluses: bool,
meek_immediate_elect: bool,
@@ -223,6 +224,7 @@ impl STVOptions {
transferable_only,
exclusion,
meek_nz_exclusion,
+ early_bulk_elect,
bulk_exclude,
defer_surpluses,
meek_immediate_elect,
diff --git a/tests/aec.rs b/tests/aec.rs
index 8ac4b09..3add705 100644
--- a/tests/aec.rs
+++ b/tests/aec.rs
@@ -71,6 +71,7 @@ fn aec_tas19_rational() {
transferable_only: false,
exclusion: stv::ExclusionMethod::ByValue,
meek_nz_exclusion: false,
+ early_bulk_elect: true,
bulk_exclude: true,
defer_surpluses: false,
meek_immediate_elect: false,
diff --git a/tests/csm.rs b/tests/csm.rs
index cf3f6ca..b67c18b 100644
--- a/tests/csm.rs
+++ b/tests/csm.rs
@@ -39,6 +39,7 @@ fn csm15_float64() {
transferable_only: false,
exclusion: stv::ExclusionMethod::Wright,
meek_nz_exclusion: false,
+ early_bulk_elect: true,
bulk_exclude: true,
defer_surpluses: false,
meek_immediate_elect: false,
diff --git a/tests/ers97.rs b/tests/ers97.rs
index 053570e..054c2cd 100644
--- a/tests/ers97.rs
+++ b/tests/ers97.rs
@@ -39,6 +39,7 @@ fn ers97_rational() {
transferable_only: true,
exclusion: stv::ExclusionMethod::ByValue,
meek_nz_exclusion: false,
+ early_bulk_elect: true,
bulk_exclude: true,
defer_surpluses: true,
meek_immediate_elect: false,
diff --git a/tests/meek.rs b/tests/meek.rs
index a763a29..275a759 100644
--- a/tests/meek.rs
+++ b/tests/meek.rs
@@ -44,6 +44,7 @@ fn meek87_ers97_float64() {
transferable_only: false,
exclusion: stv::ExclusionMethod::SingleStage,
meek_nz_exclusion: false,
+ early_bulk_elect: true,
bulk_exclude: false,
defer_surpluses: false,
meek_immediate_elect: false,
@@ -72,6 +73,7 @@ fn meek06_ers97_fixed12() {
transferable_only: false,
exclusion: stv::ExclusionMethod::SingleStage,
meek_nz_exclusion: false,
+ early_bulk_elect: true,
bulk_exclude: false,
defer_surpluses: true,
meek_immediate_elect: true,
@@ -145,6 +147,7 @@ fn meeknz_ers97_fixed12() {
transferable_only: false,
exclusion: stv::ExclusionMethod::SingleStage,
meek_nz_exclusion: true,
+ early_bulk_elect: true,
bulk_exclude: false,
defer_surpluses: true,
meek_immediate_elect: true,
diff --git a/tests/prsa.rs b/tests/prsa.rs
index 751c32e..70088e2 100644
--- a/tests/prsa.rs
+++ b/tests/prsa.rs
@@ -39,6 +39,7 @@ fn prsa1_rational() {
transferable_only: true,
exclusion: stv::ExclusionMethod::ParcelsByOrder,
meek_nz_exclusion: false,
+ early_bulk_elect: false,
bulk_exclude: false,
defer_surpluses: false,
meek_immediate_elect: false,
diff --git a/tests/scotland.rs b/tests/scotland.rs
index 9acaef0..717aadb 100644
--- a/tests/scotland.rs
+++ b/tests/scotland.rs
@@ -46,6 +46,7 @@ fn scotland_linn07_fixed5() {
transferable_only: false,
exclusion: stv::ExclusionMethod::SingleStage,
meek_nz_exclusion: false,
+ early_bulk_elect: true,
bulk_exclude: false,
defer_surpluses: false,
meek_immediate_elect: false,
@@ -74,6 +75,7 @@ fn scotland_linn07_gfixed5() {
transferable_only: false,
exclusion: stv::ExclusionMethod::SingleStage,
meek_nz_exclusion: false,
+ early_bulk_elect: true,
bulk_exclude: false,
defer_surpluses: false,
meek_immediate_elect: false,