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="wright">Wright STV</option>-->
|
||||
<option value="prsa77">PRSA 1977</option>
|
||||
<!--<option value="ers97">ERS97</option>-->
|
||||
<option value="ers97">ERS97</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="btnAdvancedOptions" onclick="clickAdvancedOptions()">Show advanced options</button>
|
||||
@ -70,13 +70,13 @@
|
||||
<option value="hare_exact">Hare (exact)</option>
|
||||
</select>
|
||||
</label>
|
||||
<!--<label>
|
||||
<label>
|
||||
<select id="selQuotaMode">
|
||||
<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>
|
||||
</select>
|
||||
</label>-->
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
|
@ -97,6 +97,7 @@ async function clickCount() {
|
||||
document.getElementById('chkRoundQuota').checked ? parseInt(document.getElementById('txtRoundQuota').value) : null,
|
||||
document.getElementById('selQuota').value,
|
||||
document.getElementById('selQuotaCriterion').value,
|
||||
document.getElementById('selQuotaMode').value,
|
||||
document.getElementById('selTransfers').value,
|
||||
document.getElementById('selSurplus').value,
|
||||
document.getElementById('selPapers').value == 'transferable',
|
||||
@ -291,7 +292,7 @@ function changePreset() {
|
||||
if (document.getElementById('selPreset').value === 'scottish') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
//document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
//document.getElementById('chkBulkElection').checked = true;
|
||||
//document.getElementById('chkBulkExclusion').checked = false;
|
||||
//document.getElementById('chkDeferSurpluses').checked = false;
|
||||
@ -311,7 +312,7 @@ function changePreset() {
|
||||
} else if (document.getElementById('selPreset').value === 'senate') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
//document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
//document.getElementById('chkBulkElection').checked = true;
|
||||
//document.getElementById('chkBulkExclusion').checked = true;
|
||||
//document.getElementById('chkDeferSurpluses').checked = false;
|
||||
@ -332,7 +333,7 @@ function changePreset() {
|
||||
} else if (document.getElementById('selPreset').value === 'prsa77') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
//document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
//document.getElementById('chkBulkElection').checked = true;
|
||||
//document.getElementById('chkBulkExclusion').checked = false;
|
||||
//document.getElementById('chkDeferSurpluses').checked = true;
|
||||
@ -352,5 +353,28 @@ function changePreset() {
|
||||
document.getElementById('selPapers').value = 'transferable';
|
||||
document.getElementById('selExclusion').value = 'parcels_by_order';
|
||||
//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
|
||||
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
|
||||
let opts = wasm.STVOptions.new.apply(null, evt.data.optsStr);
|
||||
|
||||
// Describe count
|
||||
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
|
||||
let state = wasm['CountState' + numbers].new(election);
|
||||
wasm['count_init_' + numbers](state, opts);
|
||||
|
@ -102,7 +102,8 @@ pub struct CountState<'a, N> {
|
||||
pub exhausted: 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_excluded: usize,
|
||||
@ -119,7 +120,8 @@ impl<'a, N: Number> CountState<'a, N> {
|
||||
candidates: HashMap::new(),
|
||||
exhausted: CountCard::new(),
|
||||
loss_fraction: CountCard::new(),
|
||||
quota: N::new(),
|
||||
quota: None,
|
||||
vote_required_election: None,
|
||||
num_elected: 0,
|
||||
num_excluded: 0,
|
||||
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")]
|
||||
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 --
|
||||
|
||||
@ -160,6 +164,7 @@ where
|
||||
cmd_opts.round_quota,
|
||||
&cmd_opts.quota,
|
||||
&cmd_opts.quota_criterion,
|
||||
&cmd_opts.quota_mode,
|
||||
&cmd_opts.surplus,
|
||||
&cmd_opts.surplus_order,
|
||||
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;
|
||||
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!("");
|
||||
}
|
||||
|
@ -192,9 +192,7 @@ impl ops::Add<&Self> for Fixed {
|
||||
|
||||
impl ops::Sub<&Self> for Fixed {
|
||||
type Output = Self;
|
||||
fn sub(self, _rhs: &Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn sub(self, rhs: &Self) -> Self::Output { Self(self.0 - &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Mul<&Self> for Fixed {
|
||||
|
@ -136,9 +136,7 @@ impl ops::Add<&NativeFloat64> for NativeFloat64 {
|
||||
|
||||
impl ops::Sub<&NativeFloat64> for NativeFloat64 {
|
||||
type Output = NativeFloat64;
|
||||
fn sub(self, _rhs: &NativeFloat64) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn sub(self, rhs: &NativeFloat64) -> Self::Output { Self(self.0 - &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Mul<&NativeFloat64> for NativeFloat64 {
|
||||
|
@ -183,9 +183,7 @@ impl ops::Add<&Rational> for Rational {
|
||||
|
||||
impl ops::Sub<&Rational> for Rational {
|
||||
type Output = Rational;
|
||||
fn sub(self, _rhs: &Rational) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn sub(self, rhs: &Rational) -> Self::Output { Self(self.0 - &rhs.0) }
|
||||
}
|
||||
|
||||
impl ops::Mul<&Rational> for Rational {
|
||||
|
@ -181,9 +181,7 @@ impl ops::Add<&Self> for Rational {
|
||||
|
||||
impl ops::Sub<&Self> for Rational {
|
||||
type Output = Self;
|
||||
fn sub(self, _rhs: &Self) -> Self::Output {
|
||||
todo!()
|
||||
}
|
||||
fn sub(self, rhs: &Self) -> Self::Output { Self(self.0 - &rhs.0) }
|
||||
}
|
||||
|
||||
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 quota: QuotaType,
|
||||
pub quota_criterion: QuotaCriterion,
|
||||
pub quota_mode: QuotaMode,
|
||||
pub surplus: SurplusMethod,
|
||||
pub surplus_order: SurplusOrder,
|
||||
pub transferable_only: bool,
|
||||
@ -53,6 +54,7 @@ impl STVOptions {
|
||||
round_quota: Option<usize>,
|
||||
quota: &str,
|
||||
quota_criterion: &str,
|
||||
quota_mode: &str,
|
||||
surplus: &str,
|
||||
surplus_order: &str,
|
||||
transferable_only: bool,
|
||||
@ -76,6 +78,11 @@ impl STVOptions {
|
||||
"gt" => QuotaCriterion::Greater,
|
||||
_ => panic!("Invalid --quota-criterion"),
|
||||
},
|
||||
quota_mode: match quota_mode {
|
||||
"static" => QuotaMode::Static,
|
||||
"ers97" => QuotaMode::ERS97,
|
||||
_ => panic!("Invalid --quota-mode"),
|
||||
},
|
||||
surplus: match surplus {
|
||||
"wig" => SurplusMethod::WIG,
|
||||
"uig" => SurplusMethod::UIG,
|
||||
@ -111,6 +118,7 @@ impl STVOptions {
|
||||
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_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_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); }
|
||||
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]
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(PartialEq)]
|
||||
@ -210,7 +235,7 @@ impl ExclusionMethod {
|
||||
match self {
|
||||
ExclusionMethod::SingleStage => "--exclusion single_stage",
|
||||
ExclusionMethod::ByValue => "--exclusion by_value",
|
||||
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_value",
|
||||
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
@ -237,24 +262,26 @@ where
|
||||
|
||||
// Continue exclusions
|
||||
if continue_exclusion(&mut state, &opts) {
|
||||
calculate_quota(&mut state, opts);
|
||||
elect_meeting_quota(&mut state, opts);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Distribute surpluses
|
||||
if distribute_surpluses(&mut state, &opts) {
|
||||
calculate_quota(&mut state, opts);
|
||||
elect_meeting_quota(&mut state, opts);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attempt bulk election
|
||||
if bulk_elect(&mut state) {
|
||||
elect_meeting_quota(&mut state, opts);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exclude lowest hopeful
|
||||
if exclude_hopefuls(&mut state, &opts) {
|
||||
calculate_quota(&mut state, opts);
|
||||
elect_meeting_quota(&mut state, opts);
|
||||
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());
|
||||
}
|
||||
|
||||
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||
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());
|
||||
|
||||
fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N {
|
||||
match opts.quota {
|
||||
QuotaType::Droop | QuotaType::DroopExact => {
|
||||
state.quota /= N::from(state.election.seats + 1);
|
||||
total /= N::from(seats + 1);
|
||||
}
|
||||
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
|
||||
let mut factor = N::from(10);
|
||||
factor.pow_assign(dps as i32);
|
||||
state.quota *= &factor;
|
||||
state.quota.floor_mut(0);
|
||||
state.quota += N::one();
|
||||
state.quota /= factor;
|
||||
total *= &factor;
|
||||
total.floor_mut(0);
|
||||
total += N::one();
|
||||
total /= factor;
|
||||
}
|
||||
QuotaType::DroopExact | QuotaType::HareExact => {
|
||||
// Round up to next available increment if necessary
|
||||
let mut factor = N::from(10);
|
||||
factor.pow_assign(dps as i32);
|
||||
state.quota *= &factor;
|
||||
state.quota.ceil_mut(0);
|
||||
state.quota /= factor;
|
||||
total.ceil_mut(dps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -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) {
|
||||
let quota = &state.quota; // 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 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> = state.election.candidates.iter()
|
||||
.filter(|c| { let cc = state.candidates.get(c).unwrap(); cc.state == CandidateState::HOPEFUL && meets_quota(vote_req, cc, opts) })
|
||||
.collect();
|
||||
|
||||
if cands_meeting_quota.len() > 0 {
|
||||
// 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
|
||||
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;
|
||||
state.num_elected += 1;
|
||||
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.",
|
||||
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::Neg<Output=N>
|
||||
{
|
||||
let quota = state.quota.as_ref().unwrap();
|
||||
let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
||||
.filter(|(_, cc)| cc.votes > state.quota)
|
||||
.filter(|(_, cc)| &cc.votes > quota)
|
||||
.collect();
|
||||
|
||||
if has_surplus.len() > 0 {
|
||||
@ -538,7 +647,7 @@ where
|
||||
for<'r> &'r N: ops::Neg<Output=N>
|
||||
{
|
||||
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;
|
||||
match opts.surplus {
|
||||
@ -633,7 +742,7 @@ where
|
||||
// Finalise candidate votes
|
||||
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||
count_card.transfers = -&surplus;
|
||||
count_card.votes.assign(&state.quota);
|
||||
count_card.votes.assign(state.quota.as_ref().unwrap());
|
||||
checksum -= surplus;
|
||||
|
||||
// Update loss by fraction
|
||||
|
@ -63,8 +63,8 @@ macro_rules! impl_type {
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<init_results_table_$type>](election: &[<Election$type>]) -> String {
|
||||
return init_results_table(&election.0);
|
||||
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &stv::STVOptions) -> String {
|
||||
return init_results_table(&election.0, opts);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
@ -132,12 +132,15 @@ impl_type!(Rational);
|
||||
|
||||
// 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>"#);
|
||||
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(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;
|
||||
}
|
||||
|
||||
@ -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 ");
|
||||
result.push_str(crate::VERSION);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -182,7 +185,10 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
|
||||
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(&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;
|
||||
}
|
||||
|
@ -61,6 +61,7 @@ fn aec_tas19_rational() {
|
||||
round_quota: Some(0),
|
||||
quota: stv::QuotaType::Droop,
|
||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||
quota_mode: stv::QuotaMode::Static,
|
||||
surplus: stv::SurplusMethod::UIG,
|
||||
surplus_order: stv::SurplusOrder::ByOrder,
|
||||
transferable_only: false,
|
||||
|
@ -29,6 +29,7 @@ fn prsa1_rational() {
|
||||
round_quota: Some(3),
|
||||
quota: stv::QuotaType::Droop,
|
||||
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
|
||||
quota_mode: stv::QuotaMode::Static,
|
||||
surplus: stv::SurplusMethod::EG,
|
||||
surplus_order: stv::SurplusOrder::ByOrder,
|
||||
transferable_only: true,
|
||||
|
Loading…
Reference in New Issue
Block a user