use std::{
collections::{BTreeMap, btree_map::Entry},
sync::LazyLock,
};
use regex::Regex;
use crate::{
EMLError, EMLErrorKind,
csv::NameResolver,
documents::election_count::{
CountType, ElectionCountContest, ElectionCountSelectionType, ReportingUnitVotes, TotalVotes,
},
utils::{AffiliationId, CandidateId, StringValueData as _},
};
static POSTCODE_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\(postcode: (\d{4}\s*\w{2})\)").expect("Failed to compile postcode regex")
});
static SB_ID_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\d+::SB").expect("Failed to compile stembureau id regex"));
pub struct CsvPollingStation<'a> {
pub cleaned_name: String,
pub plain_ps_id: String,
pub postal_code: String,
pub reporting_unit: &'a ReportingUnitVotes,
}
pub fn extract_polling_stations(
count_type: CountType,
count_contest: &ElectionCountContest,
) -> Result<Vec<CsvPollingStation<'_>>, EMLError> {
count_contest
.reporting_unit_votes
.iter()
.map(|ru| {
let raw_name = &ru.identifier.name;
let id = ru.identifier.id.cloned_value()?;
let raw_id = id.to_raw_value();
let (plain_ps_id, postal_code, cleaned_name) = if count_type == CountType::Municipal {
let plain_ps_id = SB_ID_RE.replace(&raw_id, "").to_string();
let postal_code = POSTCODE_RE
.captures(raw_name)
.map(|c| c[1].to_string())
.unwrap_or_default();
let name = POSTCODE_RE.replace(raw_name, "").to_string();
let cleaned_name = name
.strip_prefix("Stembureau ")
.or_else(|| name.strip_prefix("Briefstembureau "))
.unwrap_or(&name)
.trim()
.to_string();
(plain_ps_id, postal_code, cleaned_name)
} else {
(raw_id.as_ref().into(), "".into(), raw_name.as_ref().into())
};
Ok(CsvPollingStation {
cleaned_name,
plain_ps_id,
postal_code,
reporting_unit: ru,
})
})
.collect()
}
pub struct AffiliationWithVotes {
pub id: AffiliationId,
pub name: String,
pub total_votes: u64,
pub reporting_unit_votes: Vec<u64>,
pub candidates: BTreeMap<CandidateId, CandidateWithVotes>,
}
pub struct CandidateWithVotes {
pub id: CandidateId,
pub name: String,
pub total_votes: u64,
pub reporting_unit_votes: Vec<u64>,
}
pub fn extract_vote_counts(
name_resolver: &impl NameResolver,
total_votes: &TotalVotes,
stations: &[CsvPollingStation],
) -> Result<BTreeMap<AffiliationId, AffiliationWithVotes>, EMLError> {
let mut results = gather_total_votes(name_resolver, total_votes)?;
for station in stations {
let mut current_affiliation = None;
let mut current_aff_entry = None;
for selection in station.reporting_unit.selections.iter() {
match &selection.selection_type {
ElectionCountSelectionType::Affiliation(aff) => {
let aff_id = aff.id.copied_value()?;
current_affiliation = Some(aff_id);
let Entry::Occupied(mut entry) = results.entry(aff_id) else {
return Err(EMLErrorKind::UnknownAffiliation(aff_id).without_span());
};
entry
.get_mut()
.reporting_unit_votes
.push(selection.valid_votes.copied_value()?);
current_aff_entry = Some(entry);
}
ElectionCountSelectionType::Candidate(cand)
if let Some(aff_id) = current_affiliation
&& let Some(aff_entry) = &mut current_aff_entry =>
{
let cand_id = cand.identifier.id.copied_value()?;
let Entry::Occupied(mut entry) = aff_entry.get_mut().candidates.entry(cand_id)
else {
return Err(EMLErrorKind::UnknownCandidate(aff_id, cand_id).without_span());
};
entry
.get_mut()
.reporting_unit_votes
.push(selection.valid_votes.copied_value()?);
}
ElectionCountSelectionType::Candidate(_) => {
return Err(EMLErrorKind::CandidateWithoutAffiliationFound.without_span());
}
ElectionCountSelectionType::ReferendumOption(_) => {
return Err(EMLErrorKind::UnexpectedReferendumOptionSelection.without_span());
}
}
}
}
Ok(results)
}
fn gather_total_votes(
name_resolver: &impl NameResolver,
total_votes: &TotalVotes,
) -> Result<BTreeMap<AffiliationId, AffiliationWithVotes>, EMLError> {
let mut results = BTreeMap::new();
for aff in total_votes.selections_per_affiliation()? {
let aff_id = aff.affiliation.id.copied_value()?;
let aff_name = name_resolver
.resolve_affiliation_name(aff_id)
.ok_or_else(|| EMLErrorKind::UnknownAffiliation(aff_id).without_span())?;
let candidates = aff
.candidates
.iter()
.map(|cand| {
let cand_id = cand.candidate.identifier.id.copied_value()?;
let cand_name = name_resolver
.resolve_candidate_name(aff_id, cand_id)
.ok_or_else(|| {
EMLErrorKind::UnknownCandidate(aff_id, cand_id).without_span()
})?;
Ok((
cand_id,
CandidateWithVotes {
id: cand_id,
name: cand_name,
total_votes: cand.valid_votes,
reporting_unit_votes: vec![],
},
))
})
.collect::<Result<BTreeMap<_, _>, EMLError>>()?;
results.insert(
aff_id,
AffiliationWithVotes {
id: aff_id,
name: aff_name,
total_votes: aff.valid_votes,
reporting_unit_votes: vec![],
candidates,
},
);
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_postcode_re_compiles() {
LazyLock::force(&POSTCODE_RE);
}
#[test]
fn test_sb_id_re_compiles() {
LazyLock::force(&SB_ID_RE);
}
}