Implement papers+votes report

This commit is contained in:
RunasSudo 2021-08-16 18:48:49 +10:00
parent c9faa2ef01
commit 8a3361f20d
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
8 changed files with 257 additions and 74 deletions

View File

@ -166,6 +166,19 @@ OpenTally applies constraints using the Grey–Fitzgerald method. Whenever a can
Multiple constraints are supported using the method described by Hill ([*Voting Matters* 1998;(9):2–4](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;(13):4–7](http://www.votingmatters.org.uk/ISSUE13/P3.HTM)). Multiple constraints are supported using the method described by Hill ([*Voting Matters* 1998;(9):2–4](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;(13):4–7](http://www.votingmatters.org.uk/ISSUE13/P3.HTM)).
## Report options
### Report style
* *Votes only*: The result sheet displays the number of votes held by each candidate at each stage of the count.
* *Ballots and votes*: The result sheet displays the number of votes *and ballot papers* held by each candidate at each stage of the count.
This functionality is not available on the command line.
### Display up to [n] d.p. (--pp-decimals)
This option allows you to specify to how many decimal places votes will be reported in the results report. It does not affect the internal precision of calculations.
## Numeric representation ## Numeric representation
### Numbers (-n/--numbers), Decimal places (--decimals) ### Numbers (-n/--numbers), Decimal places (--decimals)
@ -177,10 +190,6 @@ This dropdown allows you to select how numbers (vote totals, etc.) are represent
* *Rational* (default): Numbers are represented exactly as fractions, resulting in the elimination of rounding error, but increasing computational complexity when the number of surplus transfers is very large. * *Rational* (default): Numbers are represented exactly as fractions, resulting in the elimination of rounding error, but increasing computational complexity when the number of surplus transfers is very large.
* *Float (64-bit)*: Numbers are represented as native 64-bit floating-point numbers. This is fast, but not recommended as unexpectedly large rounding errors may be introduced in some circumstances. * *Float (64-bit)*: Numbers are represented as native 64-bit floating-point numbers. This is fast, but not recommended as unexpectedly large rounding errors may be introduced in some circumstances.
### Display up to [n] d.p. (--pp-decimals)
This option allows you to specify to how many decimal places votes will be reported in the results report. It does not affect the internal precision of calculations.
### Normalise ballots (--normalise-ballots) ### Normalise ballots (--normalise-ballots)
In the BLT file format, each set of preferences can have a specified weight – this is typically used to indicate multiple voters who had the same preferences. In the BLT file format, each set of preferences can have a specified weight – this is typically used to indicate multiple voters who had the same preferences.

View File

@ -182,6 +182,27 @@
<div> <div>
<input type="file" id="conFile"> <input type="file" id="conFile">
</div> </div>
<div class="subheading">
Report options:
</div>
<div class="col-12">
<label style="margin-right:1em;">
Report style:
<select id="selReport">
<option value="votes" selected>Votes only</option>
<option value="ballots_votes">Ballots and votes</option>
</select>
</label>
<label style="display:none;">
<input type="checkbox" id="chkReportTranspose" disabled>
Transpose transfers/<wbr>totals
</label>
</div>
<label class="col-12">
Display up to
<input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;">
d.p.
</label>
</div> </div>
<div class="col-6 cols-12" style="align-self: start;"> <div class="col-6 cols-12" style="align-self: start;">
<div class="col-12 subheading"> <div class="col-12 subheading">
@ -203,11 +224,6 @@
<input type="number" id="txtDP" value="5" min="0" style="width: 3em;"> <input type="number" id="txtDP" value="5" min="0" style="width: 3em;">
</label> </label>
</div> </div>
<label class="col-12">
Display up to
<input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;">
d.p.
</label>
<label class="col-12"> <label class="col-12">
<input type="checkbox" id="chkNormaliseBallots"> <input type="checkbox" id="chkNormaliseBallots">
Normalise ballots Normalise ballots

View File

@ -58,16 +58,19 @@ worker.onmessage = function(evt) {
document.getElementById('resultLogs1').innerHTML = evt.data.content; document.getElementById('resultLogs1').innerHTML = evt.data.content;
} else if (evt.data.type === 'updateResultsTable') { } else if (evt.data.type === 'updateResultsTable') {
for (let i = 0; i < evt.data.result.length; i++) { for (let row = 0; row < evt.data.result.length; row++) {
if (evt.data.result[i]) { if (evt.data.result[row]) {
tblResult.rows[i].insertAdjacentHTML('beforeend', evt.data.result[i]); tblResult.rows[row].insertAdjacentHTML('beforeend', evt.data.result[row]);
// Update candidate status // Update candidate status
if (i >= 3 && i % 2 == 1) { if (
if (tblResult.rows[i].lastElementChild.classList.contains('elected')) { (document.getElementById('selReport').value == 'votes' && row >= 3 && row % 2 == 1) ||
tblResult.rows[i].cells[0].classList.add('elected'); (document.getElementById('selReport').value == 'ballots_votes' && row >= 4 && row % 2 == 0)
) {
if (tblResult.rows[row].lastElementChild.classList.contains('elected')) {
tblResult.rows[row].cells[0].classList.add('elected');
} else { } else {
tblResult.rows[i].cells[0].classList.remove('elected'); tblResult.rows[row].cells[0].classList.remove('elected');
} }
} }
} }
@ -170,14 +173,18 @@ async function clickCount() {
// Dispatch to worker // Dispatch to worker
worker.postMessage({ worker.postMessage({
'type': 'countElection', 'type': 'countElection',
// Data
'bltData': bltData, 'bltData': bltData,
'conData': conData, 'conData': conData,
'optsStr': optsStr,
'bltPath': bltPath, 'bltPath': bltPath,
'conPath': conPath, 'conPath': conPath,
// Options
'optsStr': optsStr,
'numbers': document.getElementById('selNumbers').value, 'numbers': document.getElementById('selNumbers').value,
'decimals': document.getElementById('txtDP').value, 'decimals': document.getElementById('txtDP').value,
'normaliseBallots': document.getElementById('chkNormaliseBallots').checked, 'normaliseBallots': document.getElementById('chkNormaliseBallots').checked,
'reportStyle': document.getElementById('selReport').value,
'reportTranspose': document.getElementById('chkReportTranspose').checked,
}); });
} }

View File

@ -54,6 +54,10 @@ li.highlight {
.menudiv .subheading { .menudiv .subheading {
font-size: 0.8em; font-size: 0.8em;
font-weight: 600; font-weight: 600;
margin-top: 0.5rem;
}
.menudiv > div > .subheading:first-child {
margin-top: 0;
} }
.pill-grey { .pill-grey {
@ -101,7 +105,7 @@ td.count sup {
font-size: 0.6rem; font-size: 0.6rem;
top: 0; top: 0;
} }
tr.stage-no td, tr.stage-kind td, tr.stage-comment td { tr.stage-no td, tr.stage-kind td, tr.stage-comment td, tr.hint-papers-votes td {
text-align: center; text-align: center;
} }
tr.stage-kind td { tr.stage-kind td {
@ -110,6 +114,10 @@ tr.stage-kind td {
color: #1b2839; color: #1b2839;
background-color: #f0f5fb; background-color: #f0f5fb;
} }
tr.hint-papers-votes td {
font-size: 0.75em;
font-style: italic;
}
td.excluded { td.excluded {
background-color: #fde2e2; background-color: #fde2e2;
} }
@ -119,7 +127,7 @@ td.elected {
tr.info td { tr.info td {
background-color: #f0f5fb; background-color: #f0f5fb;
} }
tr.stage-no td:not(:empty), tr.transfers td { tr.stage-no td:not(:empty), tr.hint-papers-votes td:not(:empty), tr.transfers td {
border-top: 1px solid #76858c; border-top: 1px solid #76858c;
} }
tr.info:last-child td, .bb { tr.info:last-child td, .bb {
@ -134,6 +142,7 @@ tr.info:last-child td, .bb {
tr.stage-no td:nth-child(even):not([rowspan]), tr.stage-no td:nth-child(even):not([rowspan]),
tr.stage-comment td:nth-child(odd), tr.stage-comment td:nth-child(odd),
tr.hint-papers-votes td:nth-child(even),
tr.candidate.transfers td:nth-child(even):not(.elected):not(.excluded), tr.candidate.transfers td:nth-child(even):not(.elected):not(.excluded),
tr.candidate.votes td:nth-child(odd):not(.elected):not(.excluded) { tr.candidate.votes td:nth-child(odd):not(.elected):not(.excluded) {
background-color: #f9f9f9; background-color: #f9f9f9;

View File

@ -17,6 +17,7 @@ async function initWasm() {
} }
initWasm(); initWasm();
var reportStyle;
var numbers, election, opts, state, stageNum; var numbers, election, opts, state, stageNum;
onmessage = function(evt) { onmessage = function(evt) {
@ -38,6 +39,8 @@ onmessage = function(evt) {
throw 'Unknown --numbers'; throw 'Unknown --numbers';
} }
reportStyle = evt.data.reportStyle;
// Init election // Init election
election = wasm['election_from_blt_' + numbers](evt.data.bltData); election = wasm['election_from_blt_' + numbers](evt.data.bltData);
@ -60,13 +63,13 @@ onmessage = function(evt) {
postMessage({'type': 'describeCount', 'content': wasm['describe_count_' + numbers](evt.data.bltPath, election, opts)}); postMessage({'type': 'describeCount', 'content': wasm['describe_count_' + numbers](evt.data.bltPath, election, opts)});
// Init results table // Init results table
postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election, opts)}); postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election, opts, reportStyle)});
// Step election // Step election
state = wasm['CountState' + numbers].new(election); state = wasm['CountState' + numbers].new(election);
wasm['count_init_' + numbers](state, opts); wasm['count_init_' + numbers](state, opts);
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](1, state, opts)}); postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](1, state, opts, reportStyle)});
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)}); postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
stageNum = 2; stageNum = 2;
@ -104,11 +107,11 @@ function resumeCount() {
break; break;
} }
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)}); postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts, reportStyle)});
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)}); postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
} }
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state)}); postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state, reportStyle)});
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state, opts)}); postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state, opts)});
} }

View File

@ -304,6 +304,9 @@ pub struct CountCard<'a, N> {
/// Votes of the candidate at the end of this stage /// Votes of the candidate at the end of this stage
pub votes: N, pub votes: N,
/// Net ballots transferred to this candidate in this stage
pub ballot_transfers: N,
/// Parcels of ballots assigned to this candidate /// Parcels of ballots assigned to this candidate
pub parcels: Vec<Parcel<'a, N>>, pub parcels: Vec<Parcel<'a, N>>,
@ -320,6 +323,7 @@ impl<'a, N: Number> CountCard<'a, N> {
finalised: false, finalised: false,
transfers: N::new(), transfers: N::new(),
votes: N::new(), votes: N::new(),
ballot_transfers: N::new(),
parcels: Vec::new(), parcels: Vec::new(),
keep_value: None, keep_value: None,
}; };
@ -333,8 +337,8 @@ impl<'a, N: Number> CountCard<'a, N> {
/// Set [transfers](CountCard::transfers) to 0 /// Set [transfers](CountCard::transfers) to 0
pub fn step(&mut self) { pub fn step(&mut self) {
//self.orig_votes = self.votes.clone();
self.transfers = N::new(); self.transfers = N::new();
self.ballot_transfers = N::new();
} }
/// Concatenate all parcels into a single parcel, leaving [parcels](CountCard::parcels) empty /// Concatenate all parcels into a single parcel, leaving [parcels](CountCard::parcels) empty
@ -345,6 +349,11 @@ impl<'a, N: Number> CountCard<'a, N> {
} }
return result; return result;
} }
/// Return the number of ballots across all parcels
pub fn num_ballots(&self) -> N {
return self.parcels.iter().fold(N::new(), |acc, p| acc + p.num_ballots());
}
} }
/// Parcel of [Vote]s during a count /// Parcel of [Vote]s during a count

View File

@ -46,6 +46,7 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
let count_card = state.candidates.get_mut(candidate).unwrap(); let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.parcels.push(parcel); count_card.parcels.push(parcel);
count_card.transfer(&entry.num_ballots); count_card.transfer(&entry.num_ballots);
count_card.ballot_transfers += entry.num_ballots;
} }
// Transfer exhausted votes // Transfer exhausted votes
@ -56,6 +57,7 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
}; };
state.exhausted.parcels.push(parcel); state.exhausted.parcels.push(parcel);
state.exhausted.transfer(&result.exhausted.num_ballots); state.exhausted.transfer(&result.exhausted.num_ballots);
state.exhausted.ballot_transfers += result.exhausted.num_ballots;
state.kind = None; state.kind = None;
state.title = "First preferences".to_string(); state.title = "First preferences".to_string();
@ -251,7 +253,9 @@ where
state.title = String::from(&elected_candidate.name); state.title = String::from(&elected_candidate.name);
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name)); state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
let count_card = &state.candidates[elected_candidate]; let count_card = state.candidates.get_mut(elected_candidate).unwrap();
count_card.ballot_transfers = -count_card.num_ballots();
let surplus = &count_card.votes - state.quota.as_ref().unwrap(); let surplus = &count_card.votes - state.quota.as_ref().unwrap();
// Determine which votes to examine // Determine which votes to examine
@ -361,13 +365,14 @@ where
candidate_transfers.insert(candidate, transfers_orig + transfers_add); candidate_transfers.insert(candidate, transfers_orig + transfers_add);
// Transfer candidate votes // Transfer candidate votes
let new_parcel = Parcel { let parcel = Parcel {
votes: entry.votes, votes: entry.votes,
value_fraction: reweight_value_fraction(&value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_surplus_fractions), value_fraction: reweight_value_fraction(&value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_surplus_fractions),
source_order: state.num_elected + state.num_excluded, source_order: state.num_elected + state.num_excluded,
}; };
let count_card = state.candidates.get_mut(candidate).unwrap(); let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.parcels.push(new_parcel); count_card.ballot_transfers += parcel.num_ballots();
count_card.parcels.push(parcel);
} }
// Record exhausted votes // Record exhausted votes
@ -387,12 +392,14 @@ where
value_fraction: value_fraction, // TODO: Reweight exhausted votes value_fraction: value_fraction, // TODO: Reweight exhausted votes
source_order: state.num_elected + state.num_excluded, source_order: state.num_elected + state.num_excluded,
}; };
state.exhausted.ballot_transfers += parcel.num_ballots();
state.exhausted.parcels.push(parcel); state.exhausted.parcels.push(parcel);
} }
let mut checksum = N::new(); let mut checksum = N::new();
// Credit transferred votes // Credit transferred votes
// ballot_transfers updated above
for (candidate, mut votes) in candidate_transfers { for (candidate, mut votes) in candidate_transfers {
if let Some(dps) = opts.round_votes { if let Some(dps) = opts.round_votes {
votes.floor_mut(dps); votes.floor_mut(dps);
@ -453,6 +460,7 @@ where
// Exclude in one round // Exclude in one round
for excluded_candidate in excluded_candidates.iter() { for excluded_candidate in excluded_candidates.iter() {
let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
count_card.ballot_transfers = -count_card.num_ballots();
count_card.finalised = true; count_card.finalised = true;
parcels.append(&mut count_card.parcels); parcels.append(&mut count_card.parcels);
@ -493,6 +501,8 @@ where
for mut parcel in cc_parcels { for mut parcel in cc_parcels {
if parcel.value_fraction == max_value { if parcel.value_fraction == max_value {
count_card.ballot_transfers -= parcel.num_ballots();
let votes_transferred = parcel.num_votes(); let votes_transferred = parcel.num_votes();
votes.append(&mut parcel.votes); votes.append(&mut parcel.votes);
@ -546,6 +556,8 @@ where
for parcel in cc_parcels { for parcel in cc_parcels {
if parcel.source_order == min_order { if parcel.source_order == min_order {
count_card.ballot_transfers -= parcel.num_ballots();
let votes_transferred = parcel.num_votes(); let votes_transferred = parcel.num_votes();
parcels.push(parcel); parcels.push(parcel);
@ -581,6 +593,8 @@ where
parcels.push(count_card.parcels.remove(0)); parcels.push(count_card.parcels.remove(0));
votes_remain = !count_card.parcels.is_empty(); votes_remain = !count_card.parcels.is_empty();
count_card.ballot_transfers -= parcels.first().unwrap().num_ballots();
// Update votes // Update votes
let votes_transferred = parcels.first().unwrap().num_votes(); let votes_transferred = parcels.first().unwrap().num_votes();
checksum -= &votes_transferred; checksum -= &votes_transferred;
@ -621,6 +635,7 @@ where
candidate_transfers.insert(candidate, transfers_orig + &entry.num_ballots * &parcel.value_fraction); candidate_transfers.insert(candidate, transfers_orig + &entry.num_ballots * &parcel.value_fraction);
let count_card = state.candidates.get_mut(candidate).unwrap(); let count_card = state.candidates.get_mut(candidate).unwrap();
count_card.ballot_transfers += parcel.num_ballots();
count_card.parcels.push(parcel); count_card.parcels.push(parcel);
} }
@ -632,8 +647,8 @@ where
}; };
// Record transfers // Record transfers
state.exhausted.ballot_transfers += parcel.num_ballots();
exhausted_transfers += &result.exhausted.num_ballots * &parcel.value_fraction; exhausted_transfers += &result.exhausted.num_ballots * &parcel.value_fraction;
state.exhausted.parcels.push(parcel); state.exhausted.parcels.push(parcel);
// TODO: Detailed transfers logs // TODO: Detailed transfers logs
@ -656,6 +671,7 @@ where
} }
// Credit transferred votes // Credit transferred votes
// ballot_transfers updated above
for (candidate, mut votes) in candidate_transfers { for (candidate, mut votes) in candidate_transfers {
if let Some(dps) = opts.round_votes { if let Some(dps) = opts.round_votes {
votes.floor_mut(dps); votes.floor_mut(dps);

View File

@ -118,8 +118,8 @@ macro_rules! impl_type {
/// Wrapper for [init_results_table] /// Wrapper for [init_results_table]
#[wasm_bindgen] #[wasm_bindgen]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions) -> String { pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions, report_style: &str) -> String {
return init_results_table(&election.0, &opts.0); return init_results_table(&election.0, &opts.0, report_style);
} }
/// Wrapper for [describe_count] /// Wrapper for [describe_count]
@ -132,8 +132,8 @@ macro_rules! impl_type {
/// Wrapper for [update_results_table] /// Wrapper for [update_results_table]
#[wasm_bindgen] #[wasm_bindgen]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions) -> Array { pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions, report_style: &str) -> Array {
return update_results_table(stage_num, &state.0, &opts.0); return update_results_table(stage_num, &state.0, &opts.0, report_style);
} }
/// Wrapper for [update_stage_comments] /// Wrapper for [update_stage_comments]
@ -146,8 +146,8 @@ macro_rules! impl_type {
/// Wrapper for [finalise_results_table] /// Wrapper for [finalise_results_table]
#[wasm_bindgen] #[wasm_bindgen]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn [<finalise_results_table_$type>](state: &[<CountState$type>]) -> Array { pub fn [<finalise_results_table_$type>](state: &[<CountState$type>], report_style: &str) -> Array {
return finalise_results_table(&state.0); return finalise_results_table(&state.0, report_style);
} }
/// Wrapper for [final_result_summary] /// Wrapper for [final_result_summary]
@ -321,8 +321,14 @@ fn should_show_vre(opts: &stv::STVOptions) -> bool {
} }
/// Generate the first column of the HTML results table /// Generate the first column of the HTML results table
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String { fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions, report_style: &str) -> 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>"#);
match report_style {
"ballots_votes" => {
result.push_str(&r#"<tr class="hint-papers-votes"><td></td></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));
} }
@ -334,7 +340,7 @@ fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions)
} }
/// Generate subsequent columns of the HTML results table /// Generate subsequent columns of the HTML results table
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions) -> Array { fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions, report_style: &str) -> Array {
let result = Array::new(); let result = Array::new();
// Insert borders to left of new exclusions in Wright STV // Insert borders to left of new exclusions in Wright STV
@ -345,55 +351,152 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
tdclasses2 = r#"blw "#; tdclasses2 = r#"blw "#;
} }
result.push(&format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, tdclasses1, stage_num).into()); // Header rows
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.kind.unwrap_or("")).into()); match report_style {
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.title).into()); "votes" => {
result.push(&format!(r##"<td{0}><a href="#stage{1}">{1}</a></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());
}
"ballots_votes" => {
result.push(&format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, tdclasses1, stage_num).into());
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, tdclasses1, state.kind.unwrap_or("")).into());
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, tdclasses1, state.title).into());
result.push(&format!(r#"<td{}>Ballots</td><td>Votes</td>"#, tdclasses1).into());
}
_ => unreachable!("Invalid report_style")
}
for candidate in state.election.candidates.iter() { for candidate in state.election.candidates.iter() {
let count_card = &state.candidates[candidate]; let count_card = &state.candidates[candidate];
match count_card.state {
CandidateState::Hopeful | CandidateState::Guarded => { match report_style {
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into()); "votes" => {
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into()); match count_card.state {
} CandidateState::Hopeful | CandidateState::Guarded => {
CandidateState::Elected => { result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.transfers, 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">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into()); }
} CandidateState::Elected => {
CandidateState::Doomed => { 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 excluded">{}</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());
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into()); }
} CandidateState::Doomed => {
CandidateState::Withdrawn => { result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count excluded"></td>"#, tdclasses2).into()); result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, tdclasses2).into()); }
} CandidateState::Withdrawn => {
CandidateState::Excluded => { result.push(&format!(r#"<td class="{}count excluded"></td>"#, tdclasses2).into());
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into()); result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, tdclasses2).into());
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) { }
result.push(&format!(r#"<td class="{}count excluded">Ex</td>"#, tdclasses2).into()); CandidateState::Excluded => {
} else { result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into()); if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
result.push(&format!(r#"<td class="{}count excluded">Ex</td>"#, tdclasses2).into());
} else {
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
}
}
} }
} }
"ballots_votes" => {
match count_card.state {
CandidateState::Hopeful | CandidateState::Guarded => {
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
}
CandidateState::Elected => {
result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
}
CandidateState::Doomed => {
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
}
CandidateState::Withdrawn => {
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded"></td>"#, tdclasses2).into());
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, tdclasses2).into());
}
CandidateState::Excluded => {
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded">Ex</td>"#, tdclasses2).into());
} else {
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
}
}
}
}
_ => unreachable!("Invalid report_style")
} }
} }
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()); match report_style {
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into()); "votes" => {
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, 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());
}
"ballots_votes" => {
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&state.exhausted.ballot_transfers, 0), pp(&state.exhausted.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&state.exhausted.num_ballots(), 0), pp(&state.exhausted.votes, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
}
_ => unreachable!("Invalid report_style")
}
// Calculate total votes // Calculate total votes
let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
total_vote += &state.exhausted.votes; total_vote += &state.exhausted.votes;
total_vote += &state.loss_fraction.votes; total_vote += &state.loss_fraction.votes;
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into()); match report_style {
"votes" => {
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into());
}
"ballots_votes" => {
// Calculate total ballots
let mut total_ballots = state.candidates.values().fold(N::zero(), |acc, cc| { acc + cc.num_ballots() });
total_ballots += state.exhausted.num_ballots();
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&total_ballots, 0), pp(&total_vote, opts.pp_decimals)).into());
}
_ => unreachable!("Invalid report_style")
}
match report_style {
"votes" => {
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
}
"ballots_votes" => {
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
}
_ => unreachable!("Invalid report_style")
}
if should_show_vre(opts) { if should_show_vre(opts) {
if let Some(vre) = &state.vote_required_election { if let Some(vre) = &state.vote_required_election {
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(vre, opts.pp_decimals)).into()); match report_style {
"votes" => {
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(vre, opts.pp_decimals)).into());
}
"ballots_votes" => {
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(vre, opts.pp_decimals)).into());
}
_ => unreachable!("Invalid report_style")
}
} else { } else {
result.push(&format!(r#"<td class="{}count"></td>"#, tdclasses2).into()); match report_style {
"votes" => {
result.push(&format!(r#"<td class="{}count"></td>"#, tdclasses2).into());
}
"ballots_votes" => {
result.push(&format!(r#"<td class="{}count"></td><td class="count"></td>"#, tdclasses2).into());
}
_ => unreachable!("Invalid report_style")
}
} }
} }
@ -406,13 +509,24 @@ fn update_stage_comments<N: Number>(state: &CountState<N>) -> String {
} }
/// Generate the final column of the HTML results table /// Generate the final column of the HTML results table
fn finalise_results_table<N: Number>(state: &CountState<N>) -> Array { fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Array {
let result = Array::new(); let result = Array::new();
// Header rows // Header rows
result.push(&r#"<td rowspan="3"></td>"#.into()); match report_style {
result.push(&"".into()); "votes" => {
result.push(&"".into()); result.push(&r#"<td rowspan="3"></td>"#.into());
result.push(&"".into());
result.push(&"".into());
}
"ballots_votes" => {
result.push(&r#"<td rowspan="4"></td>"#.into());
result.push(&"".into());
result.push(&"".into());
result.push(&"".into());
}
_ => unreachable!("Invalid report_style")
}
// Candidate states // Candidate states
for candidate in state.election.candidates.iter() { for candidate in state.election.candidates.iter() {