Implement --quota-mode ers97
This commit is contained in:
parent
0fbe2d562e
commit
d50af1161e
|
@ -40,7 +40,7 @@
|
||||||
<!--<option value="meek">Meek 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>
|
||||||
</label>
|
</label>
|
||||||
<button id="btnAdvancedOptions" onclick="clickAdvancedOptions()">Show advanced options</button>
|
<button id="btnAdvancedOptions" onclick="clickAdvancedOptions()">Show advanced options</button>
|
||||||
|
@ -70,13 +70,13 @@
|
||||||
<option value="hare_exact">Hare (exact)</option>
|
<option value="hare_exact">Hare (exact)</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<!--<label>
|
<label>
|
||||||
<select id="selQuotaMode">
|
<select id="selQuotaMode">
|
||||||
<option value="static" selected>Static quota</option>
|
<option value="static" selected>Static quota</option>
|
||||||
<option value="progressive">Progressive quota</option>
|
<!--<option value="progressive">Progressive quota</option>-->
|
||||||
<option value="ers97">Static with ERS97 rules</option>
|
<option value="ers97">Static with ERS97 rules</option>
|
||||||
</select>
|
</select>
|
||||||
</label>-->
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
|
|
|
@ -97,6 +97,7 @@ async function clickCount() {
|
||||||
document.getElementById('chkRoundQuota').checked ? parseInt(document.getElementById('txtRoundQuota').value) : null,
|
document.getElementById('chkRoundQuota').checked ? parseInt(document.getElementById('txtRoundQuota').value) : null,
|
||||||
document.getElementById('selQuota').value,
|
document.getElementById('selQuota').value,
|
||||||
document.getElementById('selQuotaCriterion').value,
|
document.getElementById('selQuotaCriterion').value,
|
||||||
|
document.getElementById('selQuotaMode').value,
|
||||||
document.getElementById('selTransfers').value,
|
document.getElementById('selTransfers').value,
|
||||||
document.getElementById('selSurplus').value,
|
document.getElementById('selSurplus').value,
|
||||||
document.getElementById('selPapers').value == 'transferable',
|
document.getElementById('selPapers').value == 'transferable',
|
||||||
|
@ -291,7 +292,7 @@ function changePreset() {
|
||||||
if (document.getElementById('selPreset').value === 'scottish') {
|
if (document.getElementById('selPreset').value === 'scottish') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
document.getElementById('selQuota').value = 'droop';
|
document.getElementById('selQuota').value = 'droop';
|
||||||
//document.getElementById('selQuotaMode').value = 'static';
|
document.getElementById('selQuotaMode').value = 'static';
|
||||||
//document.getElementById('chkBulkElection').checked = true;
|
//document.getElementById('chkBulkElection').checked = true;
|
||||||
//document.getElementById('chkBulkExclusion').checked = false;
|
//document.getElementById('chkBulkExclusion').checked = false;
|
||||||
//document.getElementById('chkDeferSurpluses').checked = false;
|
//document.getElementById('chkDeferSurpluses').checked = false;
|
||||||
|
@ -311,7 +312,7 @@ function changePreset() {
|
||||||
} 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';
|
||||||
//document.getElementById('selQuotaMode').value = 'static';
|
document.getElementById('selQuotaMode').value = 'static';
|
||||||
//document.getElementById('chkBulkElection').checked = true;
|
//document.getElementById('chkBulkElection').checked = true;
|
||||||
//document.getElementById('chkBulkExclusion').checked = true;
|
//document.getElementById('chkBulkExclusion').checked = true;
|
||||||
//document.getElementById('chkDeferSurpluses').checked = false;
|
//document.getElementById('chkDeferSurpluses').checked = false;
|
||||||
|
@ -332,7 +333,7 @@ function changePreset() {
|
||||||
} else if (document.getElementById('selPreset').value === 'prsa77') {
|
} else if (document.getElementById('selPreset').value === 'prsa77') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
document.getElementById('selQuota').value = 'droop';
|
document.getElementById('selQuota').value = 'droop';
|
||||||
//document.getElementById('selQuotaMode').value = 'static';
|
document.getElementById('selQuotaMode').value = 'static';
|
||||||
//document.getElementById('chkBulkElection').checked = true;
|
//document.getElementById('chkBulkElection').checked = true;
|
||||||
//document.getElementById('chkBulkExclusion').checked = false;
|
//document.getElementById('chkBulkExclusion').checked = false;
|
||||||
//document.getElementById('chkDeferSurpluses').checked = true;
|
//document.getElementById('chkDeferSurpluses').checked = true;
|
||||||
|
@ -352,5 +353,28 @@ function changePreset() {
|
||||||
document.getElementById('selPapers').value = 'transferable';
|
document.getElementById('selPapers').value = 'transferable';
|
||||||
document.getElementById('selExclusion').value = 'parcels_by_order';
|
document.getElementById('selExclusion').value = 'parcels_by_order';
|
||||||
//document.getElementById('selTies').value = 'backwards_random';
|
//document.getElementById('selTies').value = 'backwards_random';
|
||||||
|
} else if (document.getElementById('selPreset').value === 'ers97') {
|
||||||
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
|
document.getElementById('selQuota').value = 'droop_exact';
|
||||||
|
document.getElementById('selQuotaMode').value = 'ers97';
|
||||||
|
//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('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('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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,15 +25,15 @@ onmessage = function(evt) {
|
||||||
// Init election
|
// Init election
|
||||||
let election = wasm['election_from_blt_' + numbers](evt.data.electionData);
|
let election = wasm['election_from_blt_' + numbers](evt.data.electionData);
|
||||||
|
|
||||||
// Init results table
|
|
||||||
postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election)});
|
|
||||||
|
|
||||||
// Init STV options
|
// Init STV options
|
||||||
let opts = wasm.STVOptions.new.apply(null, evt.data.optsStr);
|
let opts = wasm.STVOptions.new.apply(null, evt.data.optsStr);
|
||||||
|
|
||||||
// Describe count
|
// Describe count
|
||||||
postMessage({'type': 'describeCount', 'content': wasm['describe_count_' + numbers](evt.data.filePath, election, opts)});
|
postMessage({'type': 'describeCount', 'content': wasm['describe_count_' + numbers](evt.data.filePath, election, opts)});
|
||||||
|
|
||||||
|
// Init results table
|
||||||
|
postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election, opts)});
|
||||||
|
|
||||||
// Step election
|
// Step election
|
||||||
let state = wasm['CountState' + numbers].new(election);
|
let state = wasm['CountState' + numbers].new(election);
|
||||||
wasm['count_init_' + numbers](state, opts);
|
wasm['count_init_' + numbers](state, opts);
|
||||||
|
|
|
@ -102,7 +102,8 @@ pub struct CountState<'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 quota: N,
|
pub quota: Option<N>,
|
||||||
|
pub vote_required_election: Option<N>,
|
||||||
|
|
||||||
pub num_elected: usize,
|
pub num_elected: usize,
|
||||||
pub num_excluded: usize,
|
pub num_excluded: usize,
|
||||||
|
@ -119,7 +120,8 @@ 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(),
|
||||||
quota: N::new(),
|
quota: None,
|
||||||
|
vote_required_election: None,
|
||||||
num_elected: 0,
|
num_elected: 0,
|
||||||
num_excluded: 0,
|
num_excluded: 0,
|
||||||
kind: None,
|
kind: None,
|
||||||
|
|
10
src/main.rs
10
src/main.rs
|
@ -89,6 +89,10 @@ struct STV {
|
||||||
#[clap(help_heading=Some("QUOTA"), short='c', long, possible_values=&["geq", "gt"], default_value="gt", value_name="criterion")]
|
#[clap(help_heading=Some("QUOTA"), short='c', long, possible_values=&["geq", "gt"], default_value="gt", value_name="criterion")]
|
||||||
quota_criterion: String,
|
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")]
|
||||||
|
quota_mode: String,
|
||||||
|
|
||||||
// ------------------
|
// ------------------
|
||||||
// -- STV variants --
|
// -- STV variants --
|
||||||
|
|
||||||
|
@ -160,6 +164,7 @@ where
|
||||||
cmd_opts.round_quota,
|
cmd_opts.round_quota,
|
||||||
&cmd_opts.quota,
|
&cmd_opts.quota,
|
||||||
&cmd_opts.quota_criterion,
|
&cmd_opts.quota_criterion,
|
||||||
|
&cmd_opts.quota_mode,
|
||||||
&cmd_opts.surplus,
|
&cmd_opts.surplus,
|
||||||
&cmd_opts.surplus_order,
|
&cmd_opts.surplus_order,
|
||||||
cmd_opts.transferable_only,
|
cmd_opts.transferable_only,
|
||||||
|
@ -249,7 +254,10 @@ fn print_stage<N: Number>(stage_num: usize, result: &StageResult<N>, cmd_opts: &
|
||||||
total_vote += &state.loss_fraction.votes;
|
total_vote += &state.loss_fraction.votes;
|
||||||
println!("Total votes: {:.dps$}", total_vote, dps=cmd_opts.pp_decimals);
|
println!("Total votes: {:.dps$}", total_vote, dps=cmd_opts.pp_decimals);
|
||||||
|
|
||||||
println!("Quota: {:.dps$}", state.quota, dps=cmd_opts.pp_decimals);
|
println!("Quota: {:.dps$}", state.quota.as_ref().unwrap(), dps=cmd_opts.pp_decimals);
|
||||||
|
if cmd_opts.quota_mode == "ers97" {
|
||||||
|
println!("Vote required for election: {:.dps$}", state.vote_required_election.as_ref().unwrap(), dps=cmd_opts.pp_decimals);
|
||||||
|
}
|
||||||
|
|
||||||
println!("");
|
println!("");
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,9 +192,7 @@ impl ops::Add<&Self> for Fixed {
|
||||||
|
|
||||||
impl ops::Sub<&Self> for Fixed {
|
impl ops::Sub<&Self> for Fixed {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn sub(self, _rhs: &Self) -> Self::Output {
|
fn sub(self, rhs: &Self) -> Self::Output { Self(self.0 - &rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Mul<&Self> for Fixed {
|
impl ops::Mul<&Self> for Fixed {
|
||||||
|
|
|
@ -136,9 +136,7 @@ impl ops::Add<&NativeFloat64> for NativeFloat64 {
|
||||||
|
|
||||||
impl ops::Sub<&NativeFloat64> for NativeFloat64 {
|
impl ops::Sub<&NativeFloat64> for NativeFloat64 {
|
||||||
type Output = NativeFloat64;
|
type Output = NativeFloat64;
|
||||||
fn sub(self, _rhs: &NativeFloat64) -> Self::Output {
|
fn sub(self, rhs: &NativeFloat64) -> Self::Output { Self(self.0 - &rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Mul<&NativeFloat64> for NativeFloat64 {
|
impl ops::Mul<&NativeFloat64> for NativeFloat64 {
|
||||||
|
|
|
@ -183,9 +183,7 @@ impl ops::Add<&Rational> for Rational {
|
||||||
|
|
||||||
impl ops::Sub<&Rational> for Rational {
|
impl ops::Sub<&Rational> for Rational {
|
||||||
type Output = Rational;
|
type Output = Rational;
|
||||||
fn sub(self, _rhs: &Rational) -> Self::Output {
|
fn sub(self, rhs: &Rational) -> Self::Output { Self(self.0 - &rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Mul<&Rational> for Rational {
|
impl ops::Mul<&Rational> for Rational {
|
||||||
|
|
|
@ -181,9 +181,7 @@ impl ops::Add<&Self> for Rational {
|
||||||
|
|
||||||
impl ops::Sub<&Self> for Rational {
|
impl ops::Sub<&Self> for Rational {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn sub(self, _rhs: &Self) -> Self::Output {
|
fn sub(self, rhs: &Self) -> Self::Output { Self(self.0 - &rhs.0) }
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Mul<&Self> for Rational {
|
impl ops::Mul<&Self> for Rational {
|
||||||
|
|
169
src/stv/mod.rs
169
src/stv/mod.rs
|
@ -37,6 +37,7 @@ pub struct STVOptions {
|
||||||
pub round_quota: Option<usize>,
|
pub round_quota: Option<usize>,
|
||||||
pub quota: QuotaType,
|
pub quota: QuotaType,
|
||||||
pub quota_criterion: QuotaCriterion,
|
pub quota_criterion: QuotaCriterion,
|
||||||
|
pub quota_mode: QuotaMode,
|
||||||
pub surplus: SurplusMethod,
|
pub surplus: SurplusMethod,
|
||||||
pub surplus_order: SurplusOrder,
|
pub surplus_order: SurplusOrder,
|
||||||
pub transferable_only: bool,
|
pub transferable_only: bool,
|
||||||
|
@ -53,6 +54,7 @@ impl STVOptions {
|
||||||
round_quota: Option<usize>,
|
round_quota: Option<usize>,
|
||||||
quota: &str,
|
quota: &str,
|
||||||
quota_criterion: &str,
|
quota_criterion: &str,
|
||||||
|
quota_mode: &str,
|
||||||
surplus: &str,
|
surplus: &str,
|
||||||
surplus_order: &str,
|
surplus_order: &str,
|
||||||
transferable_only: bool,
|
transferable_only: bool,
|
||||||
|
@ -76,6 +78,11 @@ impl STVOptions {
|
||||||
"gt" => QuotaCriterion::Greater,
|
"gt" => QuotaCriterion::Greater,
|
||||||
_ => panic!("Invalid --quota-criterion"),
|
_ => panic!("Invalid --quota-criterion"),
|
||||||
},
|
},
|
||||||
|
quota_mode: match quota_mode {
|
||||||
|
"static" => QuotaMode::Static,
|
||||||
|
"ers97" => QuotaMode::ERS97,
|
||||||
|
_ => panic!("Invalid --quota-mode"),
|
||||||
|
},
|
||||||
surplus: match surplus {
|
surplus: match surplus {
|
||||||
"wig" => SurplusMethod::WIG,
|
"wig" => SurplusMethod::WIG,
|
||||||
"uig" => SurplusMethod::UIG,
|
"uig" => SurplusMethod::UIG,
|
||||||
|
@ -111,6 +118,7 @@ impl STVOptions {
|
||||||
if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); }
|
if let Some(dps) = self.round_quota { flags.push(format!("--round-quota {}", dps)); }
|
||||||
if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); }
|
if self.quota != QuotaType::Droop { flags.push(self.quota.describe()); }
|
||||||
if self.quota_criterion != QuotaCriterion::GreaterOrEqual { flags.push(self.quota_criterion.describe()); }
|
if self.quota_criterion != QuotaCriterion::GreaterOrEqual { flags.push(self.quota_criterion.describe()); }
|
||||||
|
if self.quota_mode != QuotaMode::Static { flags.push(self.quota_mode.describe()); }
|
||||||
if self.surplus != SurplusMethod::WIG { flags.push(self.surplus.describe()); }
|
if self.surplus != SurplusMethod::WIG { flags.push(self.surplus.describe()); }
|
||||||
if self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); }
|
if self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); }
|
||||||
if self.transferable_only { flags.push("--transferable-only".to_string()); }
|
if self.transferable_only { flags.push("--transferable-only".to_string()); }
|
||||||
|
@ -158,6 +166,23 @@ impl QuotaCriterion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum QuotaMode {
|
||||||
|
Static,
|
||||||
|
ERS97,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuotaMode {
|
||||||
|
fn describe(self) -> String {
|
||||||
|
match self {
|
||||||
|
QuotaMode::Static => "--quota-mode static",
|
||||||
|
QuotaMode::ERS97 => "--quota-mode ers97",
|
||||||
|
}.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
@ -210,7 +235,7 @@ impl ExclusionMethod {
|
||||||
match self {
|
match self {
|
||||||
ExclusionMethod::SingleStage => "--exclusion single_stage",
|
ExclusionMethod::SingleStage => "--exclusion single_stage",
|
||||||
ExclusionMethod::ByValue => "--exclusion by_value",
|
ExclusionMethod::ByValue => "--exclusion by_value",
|
||||||
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_value",
|
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
|
||||||
}.to_string()
|
}.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,24 +262,26 @@ where
|
||||||
|
|
||||||
// Continue exclusions
|
// Continue exclusions
|
||||||
if continue_exclusion(&mut state, &opts) {
|
if continue_exclusion(&mut state, &opts) {
|
||||||
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_quota(&mut state, opts);
|
elect_meeting_quota(&mut state, opts);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Distribute surpluses
|
// Distribute surpluses
|
||||||
if distribute_surpluses(&mut state, &opts) {
|
if distribute_surpluses(&mut state, &opts) {
|
||||||
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_quota(&mut state, opts);
|
elect_meeting_quota(&mut state, opts);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt bulk election
|
// Attempt bulk election
|
||||||
if bulk_elect(&mut state) {
|
if bulk_elect(&mut state) {
|
||||||
elect_meeting_quota(&mut state, opts);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclude lowest hopeful
|
// Exclude lowest hopeful
|
||||||
if exclude_hopefuls(&mut state, &opts) {
|
if exclude_hopefuls(&mut state, &opts) {
|
||||||
|
calculate_quota(&mut state, opts);
|
||||||
elect_meeting_quota(&mut state, opts);
|
elect_meeting_quota(&mut state, opts);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -357,19 +384,13 @@ fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
||||||
state.logger.log_literal("First preferences distributed.".to_string());
|
state.logger.log_literal("First preferences distributed.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N {
|
||||||
let mut log = String::new();
|
|
||||||
|
|
||||||
// Calculate the total vote
|
|
||||||
state.quota = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
|
|
||||||
log.push_str(format!("{:.dps$} usable votes, so the quota is ", state.quota, dps=opts.pp_decimals).as_str());
|
|
||||||
|
|
||||||
match opts.quota {
|
match opts.quota {
|
||||||
QuotaType::Droop | QuotaType::DroopExact => {
|
QuotaType::Droop | QuotaType::DroopExact => {
|
||||||
state.quota /= N::from(state.election.seats + 1);
|
total /= N::from(seats + 1);
|
||||||
}
|
}
|
||||||
QuotaType::Hare | QuotaType::HareExact => {
|
QuotaType::Hare | QuotaType::HareExact => {
|
||||||
state.quota /= N::from(state.election.seats);
|
total /= N::from(seats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -379,25 +400,100 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||||
// Increment to next available increment
|
// Increment to next available increment
|
||||||
let mut factor = N::from(10);
|
let mut factor = N::from(10);
|
||||||
factor.pow_assign(dps as i32);
|
factor.pow_assign(dps as i32);
|
||||||
state.quota *= &factor;
|
total *= &factor;
|
||||||
state.quota.floor_mut(0);
|
total.floor_mut(0);
|
||||||
state.quota += N::one();
|
total += N::one();
|
||||||
state.quota /= factor;
|
total /= factor;
|
||||||
}
|
}
|
||||||
QuotaType::DroopExact | QuotaType::HareExact => {
|
QuotaType::DroopExact | QuotaType::HareExact => {
|
||||||
// Round up to next available increment if necessary
|
// Round up to next available increment if necessary
|
||||||
let mut factor = N::from(10);
|
total.ceil_mut(dps);
|
||||||
factor.pow_assign(dps as i32);
|
|
||||||
state.quota *= &factor;
|
|
||||||
state.quota.ceil_mut(0);
|
|
||||||
state.quota /= factor;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.push_str(format!("{:.dps$}.", state.quota, dps=opts.pp_decimals).as_str());
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||||
|
// Calculate quota
|
||||||
|
if let None = state.quota {
|
||||||
|
let mut log = String::new();
|
||||||
|
|
||||||
|
// Calculate the total vote
|
||||||
|
let total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
|
||||||
|
log.push_str(format!("{:.dps$} usable votes, so the quota is ", total_vote, dps=opts.pp_decimals).as_str());
|
||||||
|
|
||||||
|
let quota = total_to_quota(total_vote, state.election.seats, opts);
|
||||||
|
|
||||||
|
log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
|
||||||
|
state.quota = Some(quota);
|
||||||
|
state.logger.log_literal(log);
|
||||||
|
}
|
||||||
|
|
||||||
state.logger.log_literal(log);
|
if let QuotaMode::ERS97 = opts.quota_mode {
|
||||||
|
// ERS97 rules
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Reduce quota if allowable
|
||||||
|
|
||||||
|
if state.num_elected == 0 {
|
||||||
|
let mut log = String::new();
|
||||||
|
|
||||||
|
// Calculate the total vote
|
||||||
|
let total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
|
||||||
|
log.push_str(format!("{:.dps$} usable votes, so the quota is reduced to ", total_vote, dps=opts.pp_decimals).as_str());
|
||||||
|
|
||||||
|
let quota = total_to_quota(total_vote, state.election.seats, opts);
|
||||||
|
|
||||||
|
if "a < state.quota.as_ref().unwrap() {
|
||||||
|
log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
|
||||||
|
state.quota = Some(quota);
|
||||||
|
state.logger.log_literal(log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
// Calculate vote required for election
|
||||||
|
|
||||||
|
if state.num_elected < state.election.seats {
|
||||||
|
let mut log = String::new();
|
||||||
|
|
||||||
|
// Calculate total active vote
|
||||||
|
let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
|
||||||
|
match cc.state {
|
||||||
|
CandidateState::ELECTED => { acc + &cc.votes - state.quota.as_ref().unwrap() }
|
||||||
|
_ => { acc + &cc.votes }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.push_str(format!("Total active vote is {:.dps$}, so the vote required for election is ", total_active_vote, dps=opts.pp_decimals).as_str());
|
||||||
|
|
||||||
|
let vote_req = total_to_quota(total_active_vote, state.election.seats - state.num_elected, opts);
|
||||||
|
|
||||||
|
if &vote_req < state.quota.as_ref().unwrap() {
|
||||||
|
// VRE is less than the quota
|
||||||
|
if let Some(v) = &state.vote_required_election {
|
||||||
|
if &vote_req != v {
|
||||||
|
log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
|
||||||
|
state.vote_required_election = Some(vote_req);
|
||||||
|
state.logger.log_literal(log);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.push_str(format!("{:.dps$}.", vote_req, dps=opts.pp_decimals).as_str());
|
||||||
|
state.vote_required_election = Some(vote_req);
|
||||||
|
state.logger.log_literal(log);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// VRE is not less than the quota, so use the quota
|
||||||
|
state.vote_required_election = state.quota.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No ERS97 rules
|
||||||
|
if let None = state.vote_required_election {
|
||||||
|
state.vote_required_election = state.quota.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
|
fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
|
||||||
|
@ -412,17 +508,19 @@ fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOption
|
||||||
}
|
}
|
||||||
|
|
||||||
fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||||
let quota = &state.quota; // Have to do this or else the borrow checker gets confused
|
let vote_req = state.vote_required_election.as_ref().unwrap(); // Have to do this or else the borrow checker gets confused
|
||||||
let mut cands_meeting_quota: Vec<(&&Candidate, &mut CountCard<N>)> = state.candidates.iter_mut()
|
|
||||||
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL && meets_quota(quota, cc, opts))
|
let mut cands_meeting_quota: Vec<&Candidate> = state.election.candidates.iter()
|
||||||
|
.filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::HOPEFUL && meets_quota(vote_req, cc, opts) })
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if cands_meeting_quota.len() > 0 {
|
if cands_meeting_quota.len() > 0 {
|
||||||
// Sort by votes
|
// Sort by votes
|
||||||
cands_meeting_quota.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap());
|
cands_meeting_quota.sort_unstable_by(|a, b| state.candidates.get(a).unwrap().votes.partial_cmp(&state.candidates.get(b).unwrap().votes).unwrap());
|
||||||
|
|
||||||
// Declare elected in descending order of votes
|
// Declare elected in descending order of votes
|
||||||
for (candidate, count_card) in cands_meeting_quota.into_iter().rev() {
|
for candidate in cands_meeting_quota.into_iter().rev() {
|
||||||
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||||
count_card.state = CandidateState::ELECTED;
|
count_card.state = CandidateState::ELECTED;
|
||||||
state.num_elected += 1;
|
state.num_elected += 1;
|
||||||
count_card.order_elected = state.num_elected as isize;
|
count_card.order_elected = state.num_elected as isize;
|
||||||
|
@ -431,6 +529,16 @@ fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions)
|
||||||
"{} meet the quota and are elected.",
|
"{} meet the quota and are elected.",
|
||||||
vec![&candidate.name]
|
vec![&candidate.name]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if opts.quota_mode == QuotaMode::ERS97 {
|
||||||
|
// Vote required for election may have changed
|
||||||
|
calculate_quota(state, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.quota_mode == QuotaMode::ERS97 {
|
||||||
|
// Repeat in case vote required for election has changed
|
||||||
|
elect_meeting_quota(state, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -440,8 +548,9 @@ 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::Neg<Output=N>
|
for<'r> &'r N: ops::Neg<Output=N>
|
||||||
{
|
{
|
||||||
|
let quota = state.quota.as_ref().unwrap();
|
||||||
let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||||||
.filter(|(_, cc)| cc.votes > state.quota)
|
.filter(|(_, cc)| &cc.votes > quota)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if has_surplus.len() > 0 {
|
if has_surplus.len() > 0 {
|
||||||
|
@ -538,7 +647,7 @@ where
|
||||||
for<'r> &'r N: ops::Neg<Output=N>
|
for<'r> &'r N: ops::Neg<Output=N>
|
||||||
{
|
{
|
||||||
let count_card = state.candidates.get(elected_candidate).unwrap();
|
let count_card = state.candidates.get(elected_candidate).unwrap();
|
||||||
let surplus = &count_card.votes - &state.quota;
|
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
|
||||||
|
|
||||||
let votes;
|
let votes;
|
||||||
match opts.surplus {
|
match opts.surplus {
|
||||||
|
@ -633,7 +742,7 @@ where
|
||||||
// Finalise candidate votes
|
// Finalise candidate votes
|
||||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||||
count_card.transfers = -&surplus;
|
count_card.transfers = -&surplus;
|
||||||
count_card.votes.assign(&state.quota);
|
count_card.votes.assign(state.quota.as_ref().unwrap());
|
||||||
checksum -= surplus;
|
checksum -= surplus;
|
||||||
|
|
||||||
// Update loss by fraction
|
// Update loss by fraction
|
||||||
|
|
|
@ -63,8 +63,8 @@ macro_rules! impl_type {
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<init_results_table_$type>](election: &[<Election$type>]) -> String {
|
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &stv::STVOptions) -> String {
|
||||||
return init_results_table(&election.0);
|
return init_results_table(&election.0, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
@ -132,12 +132,15 @@ impl_type!(Rational);
|
||||||
|
|
||||||
// Reporting
|
// Reporting
|
||||||
|
|
||||||
fn init_results_table<N: Number>(election: &Election<N>) -> String {
|
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String {
|
||||||
let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
|
let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
|
||||||
for candidate in election.candidates.iter() {
|
for candidate in election.candidates.iter() {
|
||||||
result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
|
result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
|
||||||
}
|
}
|
||||||
result.push_str(r#"<tr class="info transfers"><td rowspan="2">Exhausted</td></tr><tr class="info votes"></tr><tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr><tr class="info votes"></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"#);
|
result.push_str(r#"<tr class="info transfers"><td rowspan="2">Exhausted</td></tr><tr class="info votes"></tr><tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr><tr class="info votes"></tr><tr class="info transfers"><td>Total</td></tr><tr class="info transfers"><td>Quota</td></tr>"#);
|
||||||
|
if opts.quota_mode == stv::QuotaMode::ERS97 {
|
||||||
|
result.push_str(r#"<tr class="info transfers"><td>Vote required for election</td></tr>"#);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +148,7 @@ fn describe_count<N: Number>(filename: String, election: &Election<N>, opts: &st
|
||||||
let mut result = String::from("<p>Count computed by OpenTally (revision ");
|
let mut result = String::from("<p>Count computed by OpenTally (revision ");
|
||||||
result.push_str(crate::VERSION);
|
result.push_str(crate::VERSION);
|
||||||
let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value });
|
let total_ballots = election.ballots.iter().fold(N::zero(), |acc, b| { acc + &b.orig_value });
|
||||||
result.push_str(&format!(r#"). Read {:.dps$} ballots from ‘{}’ for election ‘{}’. There are {} candidates for {} vacancies. Counting using options <span style="font-family: monospace;">{}</span>.</p>"#, total_ballots, filename, election.name, election.candidates.len(), election.seats, opts.describe::<N>(), dps=opts.pp_decimals));
|
result.push_str(&format!(r#"). Read {:.0} ballots from ‘{}’ for election ‘{}’. There are {} candidates for {} vacancies. Counting using options <span style="font-family: monospace;">{}</span>.</p>"#, total_ballots, filename, election.name, election.candidates.len(), election.seats, opts.describe::<N>()));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,7 +185,10 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
|
||||||
total_vote += &state.loss_fraction.votes;
|
total_vote += &state.loss_fraction.votes;
|
||||||
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&total_vote, opts.pp_decimals)).into());
|
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&total_vote, opts.pp_decimals)).into());
|
||||||
|
|
||||||
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.quota, opts.pp_decimals)).into());
|
result.push(&format!(r#"<td class="count">{}</td>"#, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||||
|
if opts.quota_mode == stv::QuotaMode::ERS97 {
|
||||||
|
result.push(&format!(r#"<td class="count">{}</td>"#, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ fn aec_tas19_rational() {
|
||||||
round_quota: Some(0),
|
round_quota: Some(0),
|
||||||
quota: stv::QuotaType::Droop,
|
quota: stv::QuotaType::Droop,
|
||||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||||
|
quota_mode: stv::QuotaMode::Static,
|
||||||
surplus: stv::SurplusMethod::UIG,
|
surplus: stv::SurplusMethod::UIG,
|
||||||
surplus_order: stv::SurplusOrder::ByOrder,
|
surplus_order: stv::SurplusOrder::ByOrder,
|
||||||
transferable_only: false,
|
transferable_only: false,
|
||||||
|
|
|
@ -29,6 +29,7 @@ fn prsa1_rational() {
|
||||||
round_quota: Some(3),
|
round_quota: Some(3),
|
||||||
quota: stv::QuotaType::Droop,
|
quota: stv::QuotaType::Droop,
|
||||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||||
|
quota_mode: stv::QuotaMode::Static,
|
||||||
surplus: stv::SurplusMethod::EG,
|
surplus: stv::SurplusMethod::EG,
|
||||||
surplus_order: stv::SurplusOrder::ByOrder,
|
surplus_order: stv::SurplusOrder::ByOrder,
|
||||||
transferable_only: true,
|
transferable_only: true,
|
||||||
|
|
Loading…
Reference in New Issue