2021-06-01 13:20:38 +02:00
/* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li ( RunasSudo )
*
* 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 opentally ::election ::{ CandidateState , CountState , Election } ;
2021-06-16 12:28:03 +02:00
use opentally ::numbers ::Number ;
2021-08-19 18:16:54 +02:00
use opentally ::parser ::blt ;
2021-06-01 13:20:38 +02:00
use opentally ::stv ;
2021-08-19 18:16:54 +02:00
use csv ::{ ReaderBuilder , StringRecord } ;
2021-06-01 13:20:38 +02:00
use std ::ops ;
#[ allow(dead_code) ] // Suppress false positive
2021-06-16 12:28:03 +02:00
pub fn read_validate_election < N : Number > ( csv_file : & str , blt_file : & str , stv_opts : stv ::STVOptions , cmp_dps : Option < usize > , sum_rows : & [ & str ] )
where
for < ' r > & ' r N : ops ::Add < & ' r N , Output = N > ,
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 > ,
for < ' r > & ' r N : ops ::Neg < Output = N > ,
{
2021-06-01 13:20:38 +02:00
// Read CSV file
2021-08-19 18:16:54 +02:00
let reader = ReaderBuilder ::new ( )
2021-06-01 13:20:38 +02:00
. has_headers ( false )
. from_path ( csv_file )
. expect ( " IO Error " ) ;
let records : Vec < StringRecord > = reader . into_records ( ) . map ( | r | r . expect ( " Syntax Error " ) ) . collect ( ) ;
let mut candidates : Vec < & str > = records . iter ( ) . skip ( 2 ) . map ( | r | & r [ 0 ] ) . collect ( ) ;
// Remove exhausted/LBF rows
2021-06-27 14:46:37 +02:00
candidates . truncate ( candidates . len ( ) - sum_rows . len ( ) ) ;
2021-06-01 13:20:38 +02:00
let stages : Vec < usize > = records . first ( ) . unwrap ( ) . iter ( ) . skip ( 1 ) . step_by ( 2 ) . map ( | s | s . parse ( ) . unwrap ( ) ) . collect ( ) ;
2021-06-09 12:09:20 +02:00
// Read BLT
2021-08-19 18:16:54 +02:00
let election : Election < N > = blt ::parse_path ( blt_file ) . expect ( " Syntax Error " ) ;
2021-06-01 13:20:38 +02:00
2021-06-22 08:29:04 +02:00
// Validate candidate names
for ( i , candidate ) in candidates . iter ( ) . enumerate ( ) {
assert_eq! ( election . candidates [ i ] . name , * candidate ) ;
}
2021-06-16 12:28:03 +02:00
validate_election ( stages , records , election , stv_opts , cmp_dps , sum_rows ) ;
2021-06-01 13:20:38 +02:00
}
2021-06-16 12:28:03 +02:00
pub fn validate_election < N : Number > ( stages : Vec < usize > , records : Vec < StringRecord > , mut election : Election < N > , stv_opts : stv ::STVOptions , cmp_dps : Option < usize > , sum_rows : & [ & str ] )
2021-06-01 13:20:38 +02:00
where
2021-06-16 12:28:03 +02:00
for < ' r > & ' r N : ops ::Add < & ' r N , Output = N > ,
2021-06-01 13:20:38 +02:00
for < ' r > & ' r N : ops ::Sub < & ' r N , Output = N > ,
2021-06-16 05:00:54 +02:00
for < ' r > & ' r N : ops ::Mul < & ' r N , Output = N > ,
2021-06-01 13:20:38 +02:00
for < ' r > & ' r N : ops ::Div < & ' r N , Output = N > ,
for < ' r > & ' r N : ops ::Neg < Output = N > ,
{
2021-06-16 12:28:03 +02:00
if stv_opts . normalise_ballots {
election . normalise_ballots ( ) ;
}
2021-06-01 13:20:38 +02:00
// Initialise count state
let mut state = CountState ::new ( & election ) ;
// Distribute first preferences
2021-06-27 16:56:28 +02:00
stv ::count_init ( & mut state , & stv_opts ) . unwrap ( ) ;
2021-06-01 13:20:38 +02:00
let mut stage_num = 1 ;
for ( idx , stage ) in stages . into_iter ( ) . enumerate ( ) {
while stage_num < stage {
// Step through stages
// Assert count not yet done
2021-06-11 18:09:26 +02:00
assert_eq! ( stv ::count_one_stage ( & mut state , & stv_opts ) . unwrap ( ) , false ) ;
2021-06-01 13:20:38 +02:00
stage_num + = 1 ;
}
2021-07-22 12:31:06 +02:00
validate_stage ( idx , stage_num , & state , & records , cmp_dps , sum_rows ) ;
2021-06-01 13:20:38 +02:00
}
}
2021-07-22 12:31:06 +02:00
fn validate_stage < N : Number > ( idx : usize , stage_num : usize , state : & CountState < N > , records : & Vec < StringRecord > , cmp_dps : Option < usize > , sum_rows : & [ & str ] )
2021-06-16 12:28:03 +02:00
where
for < ' r > & ' r N : ops ::Add < & ' r N , Output = N > ,
{
2021-06-22 08:29:04 +02:00
// Validate stage name
let stage_name = & records . iter ( ) . nth ( 1 ) . unwrap ( ) [ idx * 2 + 1 ] ;
if stage_name . len ( ) > 0 {
match state . kind {
Some ( kind ) = > assert_eq! ( format! ( " {} {} " , kind , state . title ) , stage_name ) ,
None = > assert_eq! ( state . title , stage_name ) ,
}
}
2021-06-16 12:28:03 +02:00
let mut candidate_votes : Vec < Option < N > > = records . iter ( ) . skip ( 2 )
2021-06-29 07:31:38 +02:00
. map ( | r | if r [ idx * 2 + 1 ] . len ( ) > 0 { Some ( N ::parse ( & r [ idx * 2 + 1 ] ) ) } else { None } )
2021-06-16 12:28:03 +02:00
. collect ( ) ;
2021-06-04 14:54:26 +02:00
2021-07-22 12:31:06 +02:00
// Validate candidate votes
for ( candidate , votes ) in state . election . candidates . iter ( ) . zip ( candidate_votes . iter ( ) ) {
let count_card = state . candidates . get ( candidate ) . unwrap ( ) ;
if votes . is_some ( ) {
approx_eq ( & count_card . votes , votes . as_ref ( ) . unwrap ( ) , cmp_dps , stage_num , & format! ( " votes for candidate {} " , candidate . name ) ) ;
}
}
2021-06-04 14:54:26 +02:00
// Validate exhausted/LBF
2021-06-16 12:28:03 +02:00
for kind in sum_rows . iter ( ) . rev ( ) {
let votes = candidate_votes . pop ( ) . unwrap_or ( None ) ;
if let Some ( votes ) = votes {
match kind {
2021-07-22 12:31:06 +02:00
& " exhausted " = > approx_eq ( & state . exhausted . votes , & votes , cmp_dps , stage_num , " exhausted votes " ) ,
& " lbf " = > approx_eq ( & state . loss_fraction . votes , & votes , cmp_dps , stage_num , " LBF " ) ,
& " nt " = > approx_eq ( & ( & state . exhausted . votes + & state . loss_fraction . votes ) , & votes , cmp_dps , stage_num , " NTs " ) ,
& " quota " = > approx_eq ( state . quota . as_ref ( ) . unwrap ( ) , & votes , cmp_dps , stage_num , " quota " ) ,
//&"vre" => approx_eq(state.vote_required_election.as_ref().unwrap(), &votes, cmp_dps, stage_num, "VRE"),
& " vre " = > approx_eq ( state . vote_required_election . as_ref ( ) . unwrap ( ) , & votes , Some ( 2 ) , stage_num , " VRE " ) ,
2021-06-16 12:28:03 +02:00
_ = > panic! ( " Unknown sum_rows " ) ,
}
}
}
2021-06-01 13:20:38 +02:00
// Validate candidate states
let mut candidate_states : Vec < & str > = records . iter ( ) . skip ( 2 ) . map ( | r | & r [ idx * 2 + 2 ] ) . collect ( ) ;
candidate_states . truncate ( candidate_states . len ( ) - 2 ) ;
for ( candidate , candidate_state ) in state . election . candidates . iter ( ) . zip ( candidate_states ) {
let count_card = state . candidates . get ( candidate ) . unwrap ( ) ;
if candidate_state = = " " {
2021-08-01 15:50:15 +02:00
} else if candidate_state = = " H " {
assert! ( count_card . state = = CandidateState ::Hopeful , " Unexpected state for \" {} \" at stage {}, expected \" Hopeful \" , got \" {:?} \" " , candidate . name , stage_num , count_card . state ) ;
} else if candidate_state = = " G " {
assert! ( count_card . state = = CandidateState ::Guarded , " Unexpected state for \" {} \" at stage {}, expected \" Guarded \" , got \" {:?} \" " , candidate . name , stage_num , count_card . state ) ;
2021-06-01 13:20:38 +02:00
} else if candidate_state = = " EL " | | candidate_state = = " PEL " {
2021-08-01 15:50:15 +02:00
assert! ( count_card . state = = CandidateState ::Elected , " Unexpected state for \" {} \" at stage {}, expected \" Elected \" , got \" {:?} \" " , candidate . name , stage_num , count_card . state ) ;
} else if candidate_state = = " D " {
assert! ( count_card . state = = CandidateState ::Doomed , " Unexpected state for \" {} \" at stage {}, expected \" Doomed \" , got \" {:?} \" " , candidate . name , stage_num , count_card . state ) ;
2021-06-01 13:20:38 +02:00
} else if candidate_state = = " EX " | | candidate_state = = " EXCLUDING " {
2021-08-01 15:50:15 +02:00
assert! ( count_card . state = = CandidateState ::Excluded , " Unexpected state for \" {} \" at stage {}, expected \" Excluded \" , got \" {:?} \" " , candidate . name , stage_num , count_card . state ) ;
2021-06-01 13:20:38 +02:00
} else {
panic! ( " Unknown state descriptor {} " , candidate_state ) ;
}
}
}
2021-06-16 12:28:03 +02:00
2021-07-22 12:31:06 +02:00
fn approx_eq < N : Number > ( n1 : & N , n2 : & N , cmp_dps : Option < usize > , stage_num : usize , description : & str ) {
2021-06-16 12:28:03 +02:00
match cmp_dps {
Some ( dps ) = > {
let s1 = format! ( " {:.dps$} " , n1 , dps = dps ) ;
let s2 = format! ( " {:.dps$} " , n2 , dps = dps ) ;
2021-07-22 12:31:06 +02:00
assert! ( s1 = = s2 , " Failed to verify {} for stage {}. Expected {}, got {} " , description , stage_num , s2 , s1 ) ;
2021-06-16 12:28:03 +02:00
}
None = > {
2021-07-22 12:31:06 +02:00
assert! ( n1 = = n2 , " Failed to verify {} for stage {}. Expected {}, got {} " , description , stage_num , n2 , n1 ) ;
2021-06-16 12:28:03 +02:00
}
}
}