2022-08-26 02:27:25 +10:00
/* OpenTally: Open-source election vote counting
2023-02-01 18:06:22 +11:00
* Copyright © 2021 – 2023 Lee Yingtong Li ( RunasSudo )
2022-08-26 02:27:25 +10:00
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https ://www.gnu.org/licenses/>.
* /
use crate ::election ::{ CandidateState , Election , StageKind } ;
use crate ::numbers ::Number ;
use crate ::stv ::{ self , CountState } ;
use itertools ::Itertools ;
/// Generate the lead-in description of the count in HTML
pub fn describe_count < N : Number > ( filename : & str , election : & Election < N > , opts : & stv ::STVOptions ) -> String {
let mut result = String ::from ( " <p>Count computed by OpenTally (revision " ) ;
result . push_str ( crate ::VERSION ) ;
let total_ballots = election . ballots . iter ( ) . fold ( N ::new ( ) , | mut acc , b | { acc + = & b . orig_value ; acc } ) ;
result . push_str ( & format! ( r # "). Read {:.0} ballots from ‘{}’ for election ‘{}’. There are {} candidates for {} vacancies. "# , total_ballots , filename , election . name , election . candidates . iter ( ) . filter ( | c | ! c . is_dummy ) . count ( ) , election . seats ) ) ;
let opts_str = opts . describe ::< N > ( ) ;
if ! opts_str . is_empty ( ) {
result . push_str ( & format! ( r # "Counting using options <span style="font-family: monospace;">{}</span>.</p>"# , opts_str ) )
} else {
result . push_str ( r # "Counting using default options.</p>"# ) ;
}
return result ;
}
/// Generate the first column of the HTML results table
pub fn init_results_table < N : Number > ( election : & Election < N > , opts : & stv ::STVOptions , report_style : & str ) -> Vec < String > {
let mut result = Vec ::new ( ) ;
result . push ( String ::from ( r # "<tr class="stage-no"><td rowspan="3"></td></tr>"# ) ) ;
result . push ( String ::from ( r # "<tr class="stage-kind"></tr>"# ) ) ;
result . push ( String ::from ( r # "<tr class="stage-comment"></tr>"# ) ) ;
if report_style = = " ballots_votes " {
result . push ( String ::from ( r # "<tr class="hint-papers-votes"><td></td></tr>"# ) ) ;
}
for candidate in election . candidates . iter ( ) {
if candidate . is_dummy {
continue ;
}
if report_style = = " votes_transposed " {
result . push ( format! ( r # "<tr class="candidate transfers"><td class="candidate-name">{}</td></tr>"# , candidate . name ) ) ;
} else {
result . push ( format! ( r # "<tr class="candidate transfers"><td rowspan="2" class="candidate-name">{}</td></tr>"# , candidate . name ) ) ;
result . push ( String ::from ( r # "<tr class="candidate votes"></tr>"# ) ) ;
}
}
if report_style = = " votes_transposed " {
result . push ( String ::from ( r # "<tr class="info transfers"><td>Exhausted</td></tr>"# ) ) ;
} else {
result . push ( String ::from ( r # "<tr class="info transfers"><td rowspan="2">Exhausted</td></tr>"# ) ) ;
result . push ( String ::from ( r # "<tr class="info votes"></tr>"# ) ) ;
}
if report_style = = " votes_transposed " {
result . push ( String ::from ( r # "<tr class="info transfers"><td>Loss by fraction</td></tr>"# ) ) ;
result . push ( String ::from ( r # "<tr class="info transfers"><td>Total</td></tr>"# ) ) ;
2023-02-01 18:06:22 +11:00
if election . seats = = 1 {
result . push ( String ::from ( r # "<tr class="info transfers"><td>Majority</td></tr>"# ) ) ;
} else {
result . push ( String ::from ( r # "<tr class="info transfers"><td>Quota</td></tr>"# ) ) ;
}
2022-08-26 02:27:25 +10:00
} else {
result . push ( String ::from ( r # "<tr class="info transfers"><td rowspan="2">Loss by fraction</td></tr>"# ) ) ;
result . push ( String ::from ( r # "<tr class="info votes"></tr>"# ) ) ;
result . push ( String ::from ( r # "<tr class="info transfers"><td>Total</td></tr>"# ) ) ;
2023-02-01 18:06:22 +11:00
if election . seats = = 1 {
result . push ( String ::from ( r # "<tr class="info transfers"><td>Majority</td></tr>"# ) ) ;
} else {
result . push ( String ::from ( r # "<tr class="info transfers"><td>Quota</td></tr>"# ) ) ;
}
2022-08-26 02:27:25 +10:00
}
if stv ::should_show_vre ( opts ) {
result . push ( String ::from ( r # "<tr class="info transfers"><td>Vote required for election</td></tr>"# ) ) ;
}
return result ;
}
/// Generate subsequent columns of the HTML results table
pub fn update_results_table < N : Number > ( stage_num : usize , state : & CountState < N > , opts : & stv ::STVOptions , report_style : & str ) -> Vec < String > {
let mut result = Vec ::new ( ) ;
// Insert borders to left of new exclusions if reset-and-reiterate method applied
let classes_o ; // Outer version
let classes_i ; // Inner version
if ( opts . exclusion = = stv ::ExclusionMethod ::ResetAndReiterate & & matches! ( state . title , StageKind ::ExclusionOf ( _ ) ) ) | | matches! ( state . title , StageKind ::Rollback ) {
classes_o = r # " class="blw""# ;
classes_i = r # "blw "# ;
} else {
classes_o = " " ;
classes_i = " " ;
}
// Hide transfers column for first preferences if transposed
let hide_xfers_trsp ;
if let StageKind ::FirstPreferences = state . title {
hide_xfers_trsp = true ;
} else if opts . exclusion = = stv ::ExclusionMethod ::ResetAndReiterate & & matches! ( state . title , StageKind ::ExclusionOf ( _ ) ) {
hide_xfers_trsp = true ;
} else if let StageKind ::Rollback = state . title {
hide_xfers_trsp = true ;
} else if let StageKind ::BulkElection = state . title {
hide_xfers_trsp = true ;
} else if state . candidates . values ( ) . all ( | cc | cc . transfers . is_zero ( ) ) & & state . exhausted . transfers . is_zero ( ) & & state . loss_fraction . transfers . is_zero ( ) {
hide_xfers_trsp = true ;
} else {
hide_xfers_trsp = false ;
}
// Header rows
let kind_str = state . title . kind_as_string ( ) ;
let title_str ;
match & state . title {
StageKind ::FirstPreferences | StageKind ::Rollback | StageKind ::RollbackExhausted | StageKind ::SurplusesDistributed | StageKind ::BulkElection = > {
title_str = format! ( " {} " , state . title ) ;
}
StageKind ::SurplusOf ( candidate ) = > {
title_str = candidate . name . clone ( ) ;
}
StageKind ::ExclusionOf ( candidates ) = > {
if candidates . len ( ) > 5 {
let first_4_cands = candidates . iter ( ) . map ( | c | & c . name ) . sorted ( ) . take ( 4 ) . join ( " ,<br> " ) ;
title_str = format! ( " {} ,<br>and {} others " , first_4_cands , candidates . len ( ) - 4 ) ;
} else {
title_str = candidates . iter ( ) . map ( | c | & c . name ) . join ( " ,<br> " ) ;
}
}
StageKind ::BallotsOf ( candidate ) = > {
title_str = candidate . name . clone ( ) ;
}
} ;
match report_style {
" votes " = > {
result . push ( format! ( r ## "<td{0}><a href="#stage{1}">{1}</a></td>"## , classes_o , stage_num ) ) ;
result . push ( format! ( r # "<td{}>{}</td>"# , classes_o , kind_str ) ) ;
result . push ( format! ( r # "<td{}>{}</td>"# , classes_o , title_str ) ) ;
}
" votes_transposed " = > {
if hide_xfers_trsp {
result . push ( format! ( r ## "<td{0}><a href="#stage{1}">{1}</a></td>"## , classes_o , stage_num ) ) ;
result . push ( format! ( r # "<td{}>{}</td>"# , classes_o , kind_str ) ) ;
result . push ( format! ( r # "<td{}>{}</td>"# , classes_o , title_str ) ) ;
} else {
result . push ( format! ( r ## "<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"## , classes_o , stage_num ) ) ;
result . push ( format! ( r # "<td{} colspan="2">{}</td>"# , classes_o , kind_str ) ) ;
result . push ( format! ( r # "<td{} colspan="2">{}</td>"# , classes_o , title_str ) ) ;
//result.push(format!(r#"<td{}>X'fers</td><td>Total</td>"#, tdclasses1));
}
}
" ballots_votes " = > {
result . push ( format! ( r ## "<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"## , classes_o , stage_num ) ) ;
result . push ( format! ( r # "<td{} colspan="2">{}</td>"# , classes_o , kind_str ) ) ;
result . push ( format! ( r # "<td{} colspan="2">{}</td>"# , classes_o , title_str ) ) ;
result . push ( format! ( r # "<td{}>Ballots</td><td>Votes</td>"# , classes_o ) ) ;
}
_ = > unreachable! ( " Invalid report_style " )
}
for candidate in state . election . candidates . iter ( ) {
if candidate . is_dummy {
continue ;
}
let count_card = & state . candidates [ candidate ] ;
// TODO: REFACTOR THIS!!
match report_style {
" votes " = > {
match count_card . state {
CandidateState ::Hopeful | CandidateState ::Guarded = > {
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
CandidateState ::Elected = > {
result . push ( format! ( r # "<td class="{}count elected">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count elected">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
CandidateState ::Doomed = > {
result . push ( format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
CandidateState ::Withdrawn = > {
result . push ( format! ( r # "<td class="{}count excluded"></td>"# , classes_i ) ) ;
result . push ( format! ( r # "<td class="{}count excluded">WD</td>"# , classes_i ) ) ;
}
CandidateState ::Excluded = > {
result . push ( format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) ) ) ;
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>"# , classes_i ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
}
}
}
" votes_transposed " = > {
match count_card . state {
CandidateState ::Hopeful | CandidateState ::Guarded = > {
if hide_xfers_trsp {
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
}
CandidateState ::Elected = > {
if hide_xfers_trsp {
result . push ( format! ( r # "<td class="{}count elected">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count elected">{}</td><td class="count elected">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
}
CandidateState ::Doomed = > {
if hide_xfers_trsp {
result . push ( format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
}
CandidateState ::Withdrawn = > {
if hide_xfers_trsp {
result . push ( format! ( r # "<td class="{}count excluded">WD</td>"# , classes_i ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count excluded"></td><td class="count excluded">WD</td>"# , classes_i ) ) ;
}
}
CandidateState ::Excluded = > {
if count_card . votes . is_zero ( ) & & count_card . parcels . iter ( ) . all ( | p | p . votes . is_empty ( ) ) {
if hide_xfers_trsp {
result . push ( format! ( r # "<td class="{}count excluded">Ex</td>"# , classes_i ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">Ex</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) ) ) ;
}
} else {
if hide_xfers_trsp {
result . push ( format! ( r # "<td class="{}count excluded">{}</td>"# , classes_i , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pps ( & count_card . transfers , opts . pp_decimals ) , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
}
}
}
}
" ballots_votes " = > {
match count_card . state {
CandidateState ::Hopeful | CandidateState ::Guarded = > {
result . push ( format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pps ( & count_card . ballot_transfers , 0 ) , pps ( & count_card . transfers , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pp ( & count_card . num_ballots ( ) , 0 ) , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
CandidateState ::Elected = > {
result . push ( format! ( r # "<td class="{}count elected">{}</td><td class="count elected">{}</td>"# , classes_i , pps ( & count_card . ballot_transfers , 0 ) , pps ( & count_card . transfers , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count elected">{}</td><td class="count elected">{}</td>"# , classes_i , pp ( & count_card . num_ballots ( ) , 0 ) , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
CandidateState ::Doomed = > {
result . push ( format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pps ( & count_card . ballot_transfers , 0 ) , pps ( & count_card . transfers , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pp ( & count_card . num_ballots ( ) , 0 ) , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
CandidateState ::Withdrawn = > {
result . push ( format! ( r # "<td class="{}count excluded"></td><td class="count excluded"></td>"# , classes_i ) ) ;
result . push ( format! ( r # "<td class="{}count excluded"></td><td class="count excluded">WD</td>"# , classes_i ) ) ;
}
CandidateState ::Excluded = > {
result . push ( format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pps ( & count_card . ballot_transfers , 0 ) , pps ( & count_card . transfers , opts . pp_decimals ) ) ) ;
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>"# , classes_i ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"# , classes_i , pp ( & count_card . num_ballots ( ) , 0 ) , pp ( & count_card . votes , opts . pp_decimals ) ) ) ;
}
}
}
}
_ = > unreachable! ( " Invalid report_style " )
}
}
match report_style {
" votes " = > {
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pps ( & state . exhausted . transfers , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & state . exhausted . votes , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pps ( & state . loss_fraction . transfers , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & state . loss_fraction . votes , opts . pp_decimals ) ) ) ;
}
" votes_transposed " = > {
if hide_xfers_trsp {
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & state . exhausted . votes , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & state . loss_fraction . votes , opts . pp_decimals ) ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pps ( & state . exhausted . transfers , opts . pp_decimals ) , pp ( & state . exhausted . votes , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pps ( & state . loss_fraction . transfers , opts . pp_decimals ) , pp ( & state . loss_fraction . votes , opts . pp_decimals ) ) ) ;
}
}
" ballots_votes " = > {
result . push ( format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pps ( & state . exhausted . ballot_transfers , 0 ) , pps ( & state . exhausted . transfers , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pp ( & state . exhausted . num_ballots ( ) , 0 ) , pp ( & state . exhausted . votes , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count"></td><td class="count">{}</td>"# , classes_i , pps ( & state . loss_fraction . transfers , opts . pp_decimals ) ) ) ;
result . push ( format! ( r # "<td class="{}count"></td><td class="count">{}</td>"# , classes_i , pp ( & state . loss_fraction . votes , opts . pp_decimals ) ) ) ;
}
_ = > unreachable! ( " Invalid report_style " )
}
// Calculate total votes
let mut total_vote = state . candidates . iter ( ) . filter_map ( | ( c , cc ) | if c . is_dummy { None } else { Some ( cc ) } ) . fold ( N ::new ( ) , | mut acc , cc | { acc + = & cc . votes ; acc } ) ;
total_vote + = & state . exhausted . votes ;
total_vote + = & state . loss_fraction . votes ;
match report_style {
" votes " = > {
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & total_vote , opts . pp_decimals ) ) ) ;
}
" votes_transposed " = > {
if hide_xfers_trsp {
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( & total_vote , opts . pp_decimals ) ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count"></td><td class="count">{}</td>"# , classes_i , pp ( & total_vote , opts . pp_decimals ) ) ) ;
}
}
" ballots_votes " = > {
// Calculate total ballots
let mut total_ballots = state . candidates . values ( ) . fold ( N ::new ( ) , | mut acc , cc | { acc + = cc . num_ballots ( ) ; acc } ) ;
total_ballots + = state . exhausted . num_ballots ( ) ;
result . push ( format! ( r # "<td class="{}count">{}</td><td class="count">{}</td>"# , classes_i , pp ( & total_ballots , 0 ) , pp ( & total_vote , opts . pp_decimals ) ) ) ;
}
_ = > unreachable! ( " Invalid report_style " )
}
if report_style = = " votes " | | ( report_style = = " votes_transposed " & & hide_xfers_trsp ) {
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( state . quota . as_ref ( ) . unwrap ( ) , opts . pp_decimals ) ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count"></td><td class="count">{}</td>"# , classes_i , pp ( state . quota . as_ref ( ) . unwrap ( ) , opts . pp_decimals ) ) ) ;
}
if stv ::should_show_vre ( opts ) {
if let Some ( vre ) = & state . vote_required_election {
if report_style = = " votes " | | ( report_style = = " votes_transposed " & & hide_xfers_trsp ) {
result . push ( format! ( r # "<td class="{}count">{}</td>"# , classes_i , pp ( vre , opts . pp_decimals ) ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count"></td><td class="count">{}</td>"# , classes_i , pp ( vre , opts . pp_decimals ) ) ) ;
}
} else {
if report_style = = " votes " | | ( report_style = = " votes_transposed " & & hide_xfers_trsp ) {
result . push ( format! ( r # "<td class="{}count"></td>"# , classes_i ) ) ;
} else {
result . push ( format! ( r # "<td class="{}count"></td><td class="count"></td>"# , classes_i ) ) ;
}
}
}
return result ;
}
/// Generate the final column of the HTML results table
pub fn finalise_results_table < N : Number > ( state : & CountState < N > , report_style : & str ) -> Vec < String > {
let mut result = Vec ::new ( ) ;
// Header rows
match report_style {
" votes " | " votes_transposed " = > {
result . push ( String ::from ( r # "<td rowspan="3"></td>"# ) ) ;
result . push ( String ::from ( " " ) ) ;
result . push ( String ::from ( " " ) ) ;
}
" ballots_votes " = > {
result . push ( String ::from ( r # "<td rowspan="4"></td>"# ) ) ;
result . push ( String ::from ( " " ) ) ;
result . push ( String ::from ( " " ) ) ;
result . push ( String ::from ( " " ) ) ;
}
_ = > unreachable! ( " Invalid report_style " )
}
let rowspan = if report_style = = " votes_transposed " { " " } else { r # " rowspan="2""# } ;
// Candidate states
for candidate in state . election . candidates . iter ( ) {
if candidate . is_dummy {
continue ;
}
let count_card = & state . candidates [ candidate ] ;
if count_card . state = = stv ::CandidateState ::Elected {
result . push ( format! ( r # "<td{} class="bb elected">ELECTED {}</td>"# , rowspan , count_card . order_elected ) ) ;
} else if count_card . state = = stv ::CandidateState ::Excluded {
result . push ( format! ( r # "<td{} class="bb excluded">Excluded {}</td>"# , rowspan , - count_card . order_elected ) ) ;
} else if count_card . state = = stv ::CandidateState ::Withdrawn {
result . push ( format! ( r # "<td{} class="bb excluded">Withdrawn</td>"# , rowspan ) ) ;
} else {
result . push ( format! ( r # "<td{} class="bb"></td>"# , rowspan ) ) ;
}
if report_style ! = " votes_transposed " {
result . push ( String ::from ( " " ) ) ;
}
}
return result ;
}
/// HTML pretty-print the number to the specified decimal places
fn pp < N : Number > ( n : & N , dps : usize ) -> String {
if n . is_zero ( ) {
return " " . to_string ( ) ;
}
let mut raw = format! ( " {:.dps$} " , n , dps = dps ) ;
if raw . contains ( '.' ) {
raw = raw . replacen ( " . " , " .<sup> " , 1 ) ;
raw . push_str ( " </sup> " ) ;
}
if raw . starts_with ( '-' ) {
raw = raw . replacen ( " - " , " − " , 1 ) ;
}
return raw ;
}
/// Signed version of [pp]
fn pps < N : Number > ( n : & N , dps : usize ) -> String {
if n . is_zero ( ) {
return " " . to_string ( ) ;
}
let mut raw = format! ( " {:.dps$} " , n , dps = dps ) ;
if raw . contains ( '.' ) {
raw = raw . replacen ( " . " , " .<sup> " , 1 ) ;
raw . push_str ( " </sup> " ) ;
}
if raw . starts_with ( '-' ) {
raw = raw . replacen ( " - " , " − " , 1 ) ;
} else {
raw . insert ( 0 , '+' ) ;
}
return raw ;
}