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 GreyFitzgerald method. Whenever a can
Multiple constraints are supported using the method described by Hill ([*Voting Matters* 1998;(9):24](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;(13):47](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
### 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.
* *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)
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>
<input type="file" id="conFile">
</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 class="col-6 cols-12" style="align-self: start;">
<div class="col-12 subheading">
@ -203,11 +224,6 @@
<input type="number" id="txtDP" value="5" min="0" style="width: 3em;">
</label>
</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">
<input type="checkbox" id="chkNormaliseBallots">
Normalise ballots

View File

@ -58,16 +58,19 @@ worker.onmessage = function(evt) {
document.getElementById('resultLogs1').innerHTML = evt.data.content;
} else if (evt.data.type === 'updateResultsTable') {
for (let i = 0; i < evt.data.result.length; i++) {
if (evt.data.result[i]) {
tblResult.rows[i].insertAdjacentHTML('beforeend', evt.data.result[i]);
for (let row = 0; row < evt.data.result.length; row++) {
if (evt.data.result[row]) {
tblResult.rows[row].insertAdjacentHTML('beforeend', evt.data.result[row]);
// Update candidate status
if (i >= 3 && i % 2 == 1) {
if (tblResult.rows[i].lastElementChild.classList.contains('elected')) {
tblResult.rows[i].cells[0].classList.add('elected');
if (
(document.getElementById('selReport').value == 'votes' && row >= 3 && row % 2 == 1) ||
(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 {
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
worker.postMessage({
'type': 'countElection',
// Data
'bltData': bltData,
'conData': conData,
'optsStr': optsStr,
'bltPath': bltPath,
'conPath': conPath,
// Options
'optsStr': optsStr,
'numbers': document.getElementById('selNumbers').value,
'decimals': document.getElementById('txtDP').value,
'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 {
font-size: 0.8em;
font-weight: 600;
margin-top: 0.5rem;
}
.menudiv > div > .subheading:first-child {
margin-top: 0;
}
.pill-grey {
@ -101,7 +105,7 @@ td.count sup {
font-size: 0.6rem;
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;
}
tr.stage-kind td {
@ -110,6 +114,10 @@ tr.stage-kind td {
color: #1b2839;
background-color: #f0f5fb;
}
tr.hint-papers-votes td {
font-size: 0.75em;
font-style: italic;
}
td.excluded {
background-color: #fde2e2;
}
@ -119,7 +127,7 @@ td.elected {
tr.info td {
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;
}
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-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.votes td:nth-child(odd):not(.elected):not(.excluded) {
background-color: #f9f9f9;

View File

@ -17,6 +17,7 @@ async function initWasm() {
}
initWasm();
var reportStyle;
var numbers, election, opts, state, stageNum;
onmessage = function(evt) {
@ -38,6 +39,8 @@ onmessage = function(evt) {
throw 'Unknown --numbers';
}
reportStyle = evt.data.reportStyle;
// Init election
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)});
// 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
state = wasm['CountState' + numbers].new(election);
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)});
stageNum = 2;
@ -104,11 +107,11 @@ function resumeCount() {
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': '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)});
}

View File

@ -304,6 +304,9 @@ pub struct CountCard<'a, N> {
/// Votes of the candidate at the end of this stage
pub votes: N,
/// Net ballots transferred to this candidate in this stage
pub ballot_transfers: N,
/// Parcels of ballots assigned to this candidate
pub parcels: Vec<Parcel<'a, N>>,
@ -320,6 +323,7 @@ impl<'a, N: Number> CountCard<'a, N> {
finalised: false,
transfers: N::new(),
votes: N::new(),
ballot_transfers: N::new(),
parcels: Vec::new(),
keep_value: None,
};
@ -333,8 +337,8 @@ impl<'a, N: Number> CountCard<'a, N> {
/// Set [transfers](CountCard::transfers) to 0
pub fn step(&mut self) {
//self.orig_votes = self.votes.clone();
self.transfers = N::new();
self.ballot_transfers = N::new();
}
/// 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 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

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

View File

@ -118,8 +118,8 @@ macro_rules! impl_type {
/// Wrapper for [init_results_table]
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions) -> String {
return init_results_table(&election.0, &opts.0);
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions, report_style: &str) -> String {
return init_results_table(&election.0, &opts.0, report_style);
}
/// Wrapper for [describe_count]
@ -132,8 +132,8 @@ macro_rules! impl_type {
/// Wrapper for [update_results_table]
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions) -> Array {
return update_results_table(stage_num, &state.0, &opts.0);
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, report_style);
}
/// Wrapper for [update_stage_comments]
@ -146,8 +146,8 @@ macro_rules! impl_type {
/// Wrapper for [finalise_results_table]
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn [<finalise_results_table_$type>](state: &[<CountState$type>]) -> Array {
return finalise_results_table(&state.0);
pub fn [<finalise_results_table_$type>](state: &[<CountState$type>], report_style: &str) -> Array {
return finalise_results_table(&state.0, report_style);
}
/// 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
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>"#);
match report_style {
"ballots_votes" => {
result.push_str(&r#"<tr class="hint-papers-votes"><td></td></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));
}
@ -334,7 +340,7 @@ 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 {
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions, report_style: &str) -> Array {
let result = Array::new();
// 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 "#;
}
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());
// Header rows
match report_style {
"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() {
let count_card = &state.candidates[candidate];
match count_card.state {
CandidateState::Hopeful | CandidateState::Guarded => {
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());
}
CandidateState::Elected => {
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());
}
CandidateState::Doomed => {
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());
}
CandidateState::Withdrawn => {
result.push(&format!(r#"<td class="{}count excluded"></td>"#, tdclasses2).into());
result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, tdclasses2).into());
}
CandidateState::Excluded => {
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, 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">Ex</td>"#, tdclasses2).into());
} else {
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
match report_style {
"votes" => {
match count_card.state {
CandidateState::Hopeful | CandidateState::Guarded => {
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());
}
CandidateState::Elected => {
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());
}
CandidateState::Doomed => {
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());
}
CandidateState::Withdrawn => {
result.push(&format!(r#"<td class="{}count excluded"></td>"#, tdclasses2).into());
result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, tdclasses2).into());
}
CandidateState::Excluded => {
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, 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">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());
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());
match report_style {
"votes" => {
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
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>"#, 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 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 {
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
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();
// Header rows
result.push(&r#"<td rowspan="3"></td>"#.into());
result.push(&"".into());
result.push(&"".into());
match report_style {
"votes" => {
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
for candidate in state.election.candidates.iter() {