Implement Wright STV
This commit is contained in:
parent
a1c21cf2b4
commit
d46eb69f26
|
@ -41,7 +41,7 @@
|
|||
<option value="meek06">Meek STV (2006)</option>
|
||||
<option value="meeknz">Meek STV (New Zealand)</option>
|
||||
<option value="senate">Australian Senate STV</option>
|
||||
<!--<option value="wright">Wright STV</option>-->
|
||||
<option value="wright">Wright STV</option>
|
||||
<option value="prsa77">PRSA 1977</option>
|
||||
<option value="ers97">ERS97</option>
|
||||
</select>
|
||||
|
@ -112,7 +112,7 @@
|
|||
<option value="single_stage" selected>Single stage</option>
|
||||
<option value="by_value">By value</option>
|
||||
<option value="parcels_by_order">By parcel (by order)</option>
|
||||
<!--<option value="wright">Wright method (re-iterate)</option>-->
|
||||
<option value="wright">Wright method (re-iterate)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style="margin-left:1em;">
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -63,7 +63,8 @@ where
|
|||
let quota = state.quota.as_ref().unwrap();
|
||||
let mut has_surplus: Vec<(&Candidate, &CountCard<N>)> = 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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -285,43 +285,52 @@ fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions)
|
|||
/// Generate subsequent columns of the HTML results table
|
||||
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions) -> Array {
|
||||
let result = Array::new();
|
||||
result.push(&format!(r#"<td>{}</td>"#, stage_num).into());
|
||||
result.push(&format!(r#"<td>{}</td>"#, state.kind.unwrap_or("")).into());
|
||||
result.push(&format!(r#"<td>{}</td>"#, 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#"<td{}>{}</td>"#, tdclasses1, stage_num).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.kind.unwrap_or("")).into());
|
||||
result.push(&format!(r#"<td{}>{}</td>"#, 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#"<td class="count elected">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="count elected">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
} else if count_card.state == stv::CandidateState::Excluded {
|
||||
result.push(&format!(r#"<td class="count excluded">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
if count_card.votes.is_zero() {
|
||||
result.push(&r#"<td class="count excluded">Ex</td>"#.into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">Ex</td>"#, tdclasses2).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="count excluded">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
} else if count_card.state == stv::CandidateState::Withdrawn {
|
||||
result.push(&r#"<td class="count excluded"></td>"#.into());
|
||||
result.push(&r#"<td class="count excluded">WD</td>"#.into());
|
||||
result.push(&format!(r#"<td class="{}count excluded"></td>"#, tdclasses2).into());
|
||||
result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, tdclasses2).into());
|
||||
} else {
|
||||
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||
}
|
||||
}
|
||||
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.exhausted.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, 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#"<td class="count">{}</td>"#, pp(&total_vote, opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into());
|
||||
|
||||
result.push(&format!(r#"<td class="count">{}</td>"#, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, 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());
|
||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
Loading…
Reference in New Issue