1use std::{collections::BTreeMap, num::NonZeroU64, str::FromStr};
4
5use crate::{
6 EML_SCHEMA_VERSION, EMLError, EMLErrorKind, EMLResultExt as _, EMLValueResultExt, NS_EML,
7 NS_KR,
8 common::{
9 CandidateIdentifier, CanonicalizationMethod, ContestIdentifier, CreationDateTime,
10 ElectionDomain, ManagingAuthority, MinimalQualifyingAddress, PersonNameStructure,
11 ReportingUnitIdentifier, TransactionId,
12 },
13 documents::{ElectionIdentifierBuilder, accepted_root},
14 io::{
15 EMLElement, EMLElementReader, EMLElementWriter, EMLReadElement as _, EMLWriteElement,
16 QualifiedName, collect_struct,
17 },
18 utils::{
19 AffiliationId, CandidateId, ElectionCategory, ElectionId, ElectionSubcategory, Gender,
20 StringValue, XsDate, XsDateTime,
21 },
22};
23
24#[derive(Debug, Clone)]
26pub struct ElectionCount {
27 pub count_type: CountType,
29
30 pub transaction_id: TransactionId,
32
33 pub managing_authority: ManagingAuthority,
35
36 pub creation_date_time: CreationDateTime,
38
39 pub canonicalization_method: Option<CanonicalizationMethod>,
41
42 pub count: ElectionCountCount,
44}
45
46impl ElectionCount {
47 pub fn builder() -> ElectionCountBuilder {
49 ElectionCountBuilder::new()
50 }
51}
52
53impl FromStr for ElectionCount {
54 type Err = EMLError;
55
56 fn from_str(s: &str) -> Result<Self, Self::Err> {
57 use crate::io::EMLRead as _;
58 Self::parse_eml(s, crate::io::EMLParsingMode::Strict).ok()
59 }
60}
61
62impl TryFrom<&str> for ElectionCount {
63 type Error = EMLError;
64
65 fn try_from(value: &str) -> Result<Self, Self::Error> {
66 use crate::io::EMLRead as _;
67 Self::parse_eml(value, crate::io::EMLParsingMode::Strict).ok()
68 }
69}
70
71impl TryFrom<ElectionCount> for String {
72 type Error = EMLError;
73
74 fn try_from(value: ElectionCount) -> Result<Self, Self::Error> {
75 use crate::io::EMLWrite as _;
76 value.write_eml_root_str(true, true)
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct ElectionCountBuilder {
83 count_type: Option<CountType>,
84 transaction_id: Option<TransactionId>,
85 managing_authority: Option<ManagingAuthority>,
86 creation_date_time: Option<CreationDateTime>,
87 canonicalization_method: Option<CanonicalizationMethod>,
88 count: Option<ElectionCountCount>,
89 election_identifier: Option<ElectionCountElectionIdentifier>,
90 contests: Vec<ElectionCountContest>,
91}
92
93impl ElectionCountBuilder {
94 pub fn new() -> Self {
96 Self {
97 count_type: None,
98 transaction_id: None,
99 managing_authority: None,
100 creation_date_time: None,
101 canonicalization_method: None,
102 count: None,
103 election_identifier: None,
104 contests: vec![],
105 }
106 }
107
108 pub fn count_type(mut self, count_type: impl Into<CountType>) -> Self {
110 self.count_type = Some(count_type.into());
111 self
112 }
113
114 pub fn transaction_id(mut self, transaction_id: impl Into<TransactionId>) -> Self {
116 self.transaction_id = Some(transaction_id.into());
117 self
118 }
119
120 pub fn managing_authority(mut self, managing_authority: impl Into<ManagingAuthority>) -> Self {
122 self.managing_authority = Some(managing_authority.into());
123 self
124 }
125
126 pub fn creation_date_time(mut self, creation_date_time: impl Into<XsDateTime>) -> Self {
128 self.creation_date_time = Some(CreationDateTime::new(creation_date_time.into()));
129 self
130 }
131
132 pub fn canonicalization_method(
134 mut self,
135 canonicalization_method: impl Into<CanonicalizationMethod>,
136 ) -> Self {
137 self.canonicalization_method = Some(canonicalization_method.into());
138 self
139 }
140
141 pub fn count(mut self, count: impl Into<ElectionCountCount>) -> Self {
148 self.count = Some(count.into());
149 self
150 }
151
152 pub fn election_identifier(
157 mut self,
158 election_identifier: impl Into<ElectionCountElectionIdentifier>,
159 ) -> Self {
160 self.election_identifier = Some(election_identifier.into());
161 self
162 }
163
164 pub fn contests(mut self, contests: impl Into<Vec<ElectionCountContest>>) -> Self {
169 self.contests = contests.into();
170 self
171 }
172
173 pub fn push_contest(mut self, contest: impl Into<ElectionCountContest>) -> Self {
178 self.contests.push(contest.into());
179 self
180 }
181
182 pub fn build(self) -> Result<ElectionCount, EMLError> {
184 Ok(ElectionCount {
185 count_type: self
186 .count_type
187 .ok_or_else(|| EMLErrorKind::MissingBuildProperty("count_type").without_span())?,
188 transaction_id: self.transaction_id.ok_or_else(|| {
189 EMLErrorKind::MissingBuildProperty("transaction_id").without_span()
190 })?,
191 managing_authority: self.managing_authority.ok_or_else(|| {
192 EMLErrorKind::MissingBuildProperty("managing_authority").without_span()
193 })?,
194 creation_date_time: self.creation_date_time.ok_or_else(|| {
195 EMLErrorKind::MissingBuildProperty("creation_date_time").without_span()
196 })?,
197 canonicalization_method: self.canonicalization_method,
198 count: self.count.map_or_else(
199 || {
200 if self.contests.is_empty() {
201 return Err(EMLErrorKind::MissingBuildProperty("contests").without_span());
202 }
203
204 Ok(ElectionCountCount::new(ElectionCountElection::new(
205 self.election_identifier.ok_or_else(|| {
206 EMLErrorKind::MissingBuildProperty("election_identifier").without_span()
207 })?,
208 self.contests,
209 )))
210 },
211 Ok,
212 )?,
213 })
214 }
215}
216
217impl Default for ElectionCountBuilder {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223impl EMLElement for ElectionCount {
224 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("EML", Some(NS_EML));
225
226 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
227 accepted_root(elem)?;
228
229 let document_id = elem.attribute_value_req(("Id", None))?;
230 let count_type = CountType::from_eml_id(document_id.as_ref())
231 .map_err(|e| e.into_kind().with_span(elem.span()))?;
232
233 Ok(collect_struct!(elem, ElectionCount {
234 count_type: count_type,
235 transaction_id: TransactionId::EML_NAME => |elem| TransactionId::read_eml(elem)?,
236 managing_authority: ManagingAuthority::EML_NAME => |elem| ManagingAuthority::read_eml(elem)?,
237 creation_date_time: CreationDateTime::EML_NAME => |elem| CreationDateTime::read_eml(elem)?,
238 canonicalization_method as Option: CanonicalizationMethod::EML_NAME => |elem| CanonicalizationMethod::read_eml(elem)?,
239 count: ElectionCountCount::EML_NAME => |elem| ElectionCountCount::read_eml(elem)?,
240 }))
241 }
242
243 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
244 writer
245 .attr(("Id", None), self.count_type.to_eml_id())?
246 .attr(("SchemaVersion", None), EML_SCHEMA_VERSION)?
247 .child_elem(TransactionId::EML_NAME, &self.transaction_id)?
248 .child_elem(ManagingAuthority::EML_NAME, &self.managing_authority)?
249 .child_elem(CreationDateTime::EML_NAME, &self.creation_date_time)?
250 .child_elem(ElectionCountCount::EML_NAME, &self.count)?
256 .finish()
257 }
258}
259
260pub(crate) const EML_COUNT_POLLING_STATION_ID: &str = "510a";
262
263pub(crate) const EML_COUNT_MUNICIPAL_ID: &str = "510b";
265
266pub(crate) const EML_COUNT_DISTRICT_ID: &str = "510c";
268
269pub(crate) const EML_COUNT_CENTRAL_ID: &str = "510d";
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum CountType {
275 PollingStation,
277 Municipal,
279 District,
281 Central,
283}
284
285impl CountType {
286 pub fn from_eml_id(s: impl AsRef<str>) -> Result<Self, EMLError> {
288 let data = s.as_ref();
289 match data {
290 EML_COUNT_POLLING_STATION_ID => Ok(CountType::PollingStation),
291 EML_COUNT_MUNICIPAL_ID => Ok(CountType::Municipal),
292 EML_COUNT_DISTRICT_ID => Ok(CountType::District),
293 EML_COUNT_CENTRAL_ID => Ok(CountType::Central),
294 _ => Err(
295 EMLErrorKind::InvalidDocumentType("510a/510b/510c/510d", data.to_string())
296 .without_span(),
297 ),
298 }
299 }
300
301 pub fn to_eml_id(&self) -> &'static str {
303 match self {
304 CountType::PollingStation => EML_COUNT_POLLING_STATION_ID,
305 CountType::Municipal => EML_COUNT_MUNICIPAL_ID,
306 CountType::District => EML_COUNT_DISTRICT_ID,
307 CountType::Central => EML_COUNT_CENTRAL_ID,
308 }
309 }
310
311 pub fn to_friendly_name(&self) -> &'static str {
313 match self {
314 CountType::PollingStation => "Polling Station Count",
315 CountType::Municipal => "Municipal Count",
316 CountType::District => "District Count",
317 CountType::Central => "Central Count",
318 }
319 }
320
321 pub fn is_valid_eml_id(s: &str) -> bool {
323 matches!(
324 s,
325 EML_COUNT_POLLING_STATION_ID
326 | EML_COUNT_MUNICIPAL_ID
327 | EML_COUNT_DISTRICT_ID
328 | EML_COUNT_CENTRAL_ID
329 )
330 }
331}
332
333#[derive(Debug, Clone)]
335pub struct ElectionCountCount {
336 pub election: ElectionCountElection,
338}
339
340impl ElectionCountCount {
341 pub fn new(election: impl Into<ElectionCountElection>) -> Self {
343 ElectionCountCount {
344 election: election.into(),
345 }
346 }
347}
348
349impl From<ElectionCountElection> for ElectionCountCount {
350 fn from(value: ElectionCountElection) -> Self {
351 ElectionCountCount::new(value)
352 }
353}
354
355impl EMLElement for ElectionCountCount {
356 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Count", Some(NS_EML));
357
358 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
359 Ok(collect_struct!(elem, ElectionCountCount {
360 id as None: ("EventIdentifier", NS_EML) => |elem| elem.skip().map(|_| ())?,
361 election: ElectionCountElection::EML_NAME => |elem| ElectionCountElection::read_eml(elem)?,
362 }))
363 }
364
365 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
366 writer
367 .child(("EventIdentifier", NS_EML), |w| w.empty())?
368 .child_elem(ElectionCountElection::EML_NAME, &self.election)?
369 .finish()
370 }
371}
372
373#[derive(Debug, Clone)]
375pub struct ElectionCountElection {
376 pub identifier: ElectionCountElectionIdentifier,
378
379 pub contests: Vec<ElectionCountContest>,
381}
382
383impl ElectionCountElection {
384 pub fn new(
386 identifier: impl Into<ElectionCountElectionIdentifier>,
387 contests: impl Into<Vec<ElectionCountContest>>,
388 ) -> Self {
389 ElectionCountElection {
390 identifier: identifier.into(),
391 contests: contests.into(),
392 }
393 }
394}
395
396impl EMLElement for ElectionCountElection {
397 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Election", Some(NS_EML));
398
399 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
400 let data = collect_struct!(elem, ElectionCountElection {
401 identifier: ElectionCountElectionIdentifier::EML_NAME => |elem| ElectionCountElectionIdentifier::read_eml(elem)?,
402 contests: ("Contests", NS_EML) => |elem| {
403 struct VecCollector {
404 contests: Vec<ElectionCountContest>,
405 }
406
407 let data = collect_struct!(elem, VecCollector {
408 contests as Vec: ElectionCountContest::EML_NAME => |elem| ElectionCountContest::read_eml(elem)?,
409 });
410
411 data.contests
412 },
413 });
414
415 if data.contests.is_empty() {
416 let err = EMLErrorKind::MissingElement(ElectionCountContest::EML_NAME.as_owned())
417 .with_span(elem.full_span());
418 if elem.parsing_mode().is_strict() {
419 return Err(err);
420 } else {
421 elem.push_err(err);
422 }
423 }
424
425 Ok(data)
426 }
427
428 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
429 writer
430 .child_elem(ElectionCountElectionIdentifier::EML_NAME, &self.identifier)?
431 .child(("Contests", NS_EML), |writer| {
432 writer
433 .child_elems(ElectionCountContest::EML_NAME, &self.contests)?
434 .finish()
435 })?
436 .finish()
437 }
438}
439
440#[derive(Debug, Clone)]
442pub struct ElectionCountElectionIdentifier {
443 pub id: StringValue<ElectionId>,
445
446 pub name: Option<Box<str>>,
448
449 pub category: StringValue<ElectionCategory>,
451
452 pub subcategory: Option<StringValue<ElectionSubcategory>>,
454
455 pub domain: Option<ElectionDomain>,
457
458 pub election_date: StringValue<XsDate>,
460}
461
462impl ElectionCountElectionIdentifier {
463 pub fn builder() -> ElectionIdentifierBuilder {
465 ElectionIdentifierBuilder::new()
466 }
467}
468
469impl EMLElement for ElectionCountElectionIdentifier {
470 const EML_NAME: QualifiedName<'_, '_> =
471 QualifiedName::from_static("ElectionIdentifier", Some(NS_EML));
472
473 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
474 Ok(collect_struct!(elem, ElectionCountElectionIdentifier {
475 id: elem.string_value_attr("Id", None)?,
476 name as Option: ("ElectionName", NS_EML) => |elem| elem.text_without_children()?,
477 category: ("ElectionCategory", NS_EML) => |elem| elem.string_value()?,
478 subcategory as Option: ("ElectionSubcategory", NS_KR) => |elem| elem.string_value()?,
479 domain as Option: ElectionDomain::EML_NAME => |elem| ElectionDomain::read_eml(elem)?,
480 election_date: ("ElectionDate", NS_KR) => |elem| elem.string_value()?,
481 }))
482 }
483
484 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
485 writer
486 .attr("Id", self.id.raw().as_ref())?
487 .child_option(
488 ("ElectionName", NS_EML),
489 self.name.as_ref(),
490 |elem, value| elem.text(value.as_ref())?.finish(),
491 )?
492 .child(("ElectionCategory", NS_EML), |elem| {
493 elem.text(self.category.raw().as_ref())?.finish()
494 })?
495 .child_option(
496 ("ElectionSubcategory", NS_KR),
497 self.subcategory.as_ref(),
498 |elem, value| elem.text(value.raw().as_ref())?.finish(),
499 )?
500 .child_elem_option(ElectionDomain::EML_NAME, self.domain.as_ref())?
501 .child(("ElectionDate", NS_KR), |elem| {
502 elem.text(self.election_date.raw().as_ref())?.finish()
503 })?
504 .finish()
505 }
506}
507
508#[derive(Debug, Clone)]
510pub struct ElectionCountContest {
511 pub identifier: ContestIdentifier,
513
514 pub total_votes: Option<TotalVotes>,
516
517 pub reporting_unit_votes: Vec<ReportingUnitVotes>,
519}
520
521impl ElectionCountContest {
522 pub fn builder() -> ElectionCountContestBuilder {
524 ElectionCountContestBuilder::new()
525 }
526}
527
528#[derive(Debug, Clone)]
530pub struct ElectionCountContestBuilder {
531 identifier: Option<ContestIdentifier>,
532 total_votes: Option<TotalVotes>,
533 total_votes_selections: Vec<ElectionCountSelection>,
534 total_eligible_voter_count: Option<StringValue<u64>>,
535 total_candidate_votes_count: Option<StringValue<u64>>,
536 total_rejected_votes: BTreeMap<RejectedVotesReason, StringValue<u64>>,
537 total_uncounted_votes: BTreeMap<UncountedVotesReason, StringValue<u64>>,
538 reporting_unit_votes: Vec<ReportingUnitVotes>,
539}
540
541impl ElectionCountContestBuilder {
542 pub fn new() -> Self {
544 Self {
545 identifier: None,
546 total_votes: None,
547 total_votes_selections: vec![],
548 total_eligible_voter_count: None,
549 total_candidate_votes_count: None,
550 total_rejected_votes: BTreeMap::new(),
551 total_uncounted_votes: BTreeMap::new(),
552 reporting_unit_votes: vec![],
553 }
554 }
555
556 pub fn identifier(mut self, identifier: impl Into<ContestIdentifier>) -> Self {
558 self.identifier = Some(identifier.into());
559 self
560 }
561
562 pub fn total_votes(mut self, total_votes: impl Into<TotalVotes>) -> Self {
564 self.total_votes = Some(total_votes.into());
565 self
566 }
567
568 pub fn total_votes_selections(
570 mut self,
571 selections: impl Into<Vec<ElectionCountSelection>>,
572 ) -> Self {
573 self.total_votes_selections = selections.into();
574 self
575 }
576
577 pub fn push_total_votes_selection(
579 mut self,
580 selection: impl Into<ElectionCountSelection>,
581 ) -> Self {
582 self.total_votes_selections.push(selection.into());
583 self
584 }
585
586 pub fn total_eligible_voter_count(mut self, count: impl Into<u64>) -> Self {
588 self.total_eligible_voter_count = Some(StringValue::from_value(count.into()));
589 self
590 }
591
592 pub fn total_candidate_votes_count(mut self, count: impl Into<u64>) -> Self {
594 self.total_candidate_votes_count = Some(StringValue::from_value(count.into()));
595 self
596 }
597
598 pub fn total_rejected_votes(
600 mut self,
601 reason: RejectedVotesReason,
602 count: impl Into<u64>,
603 ) -> Self {
604 self.total_rejected_votes
605 .insert(reason, StringValue::from_value(count.into()));
606 self
607 }
608
609 pub fn total_uncounted_votes(
611 mut self,
612 reason: UncountedVotesReason,
613 count: impl Into<u64>,
614 ) -> Self {
615 self.total_uncounted_votes
616 .insert(reason, StringValue::from_value(count.into()));
617 self
618 }
619
620 pub fn reporting_unit_votes(
623 mut self,
624 reporting_unit_votes: impl Into<Vec<ReportingUnitVotes>>,
625 ) -> Self {
626 self.reporting_unit_votes = reporting_unit_votes.into();
627 self
628 }
629
630 pub fn push_reporting_unit_votes(
632 mut self,
633 reporting_unit_votes: impl Into<ReportingUnitVotes>,
634 ) -> Self {
635 self.reporting_unit_votes.push(reporting_unit_votes.into());
636 self
637 }
638
639 pub fn build(self) -> Result<ElectionCountContest, EMLError> {
641 Ok(ElectionCountContest {
642 identifier: self
643 .identifier
644 .ok_or_else(|| EMLErrorKind::MissingBuildProperty("identifier").without_span())?,
645 total_votes: self.total_votes.map_or_else(
646 || {
647 if self.total_votes_selections.is_empty()
648 && self.total_eligible_voter_count.is_none()
649 && self.total_candidate_votes_count.is_none()
650 && self.total_rejected_votes.is_empty()
651 && self.total_uncounted_votes.is_empty()
652 {
653 Ok(None)
654 } else {
655 if self.total_votes_selections.is_empty() {
656 return Err(EMLErrorKind::MissingBuildProperty(
657 "total_votes_selections",
658 )
659 .without_span());
660 }
661
662 if !self
663 .total_rejected_votes
664 .contains_key(&RejectedVotesReason::Blank)
665 {
666 return Err(EMLErrorKind::MissingRejectedVotesBlank).without_span();
667 }
668
669 if !self
670 .total_rejected_votes
671 .contains_key(&RejectedVotesReason::Invalid)
672 {
673 return Err(EMLErrorKind::MissingRejectedVotesInvalid).without_span();
674 }
675
676 Ok(Some(TotalVotes {
677 selections: self.total_votes_selections,
678 eligible_voter_count: self.total_eligible_voter_count.ok_or_else(
679 || {
680 EMLErrorKind::MissingBuildProperty("total_eligible_voter_count")
681 .without_span()
682 },
683 )?,
684 candidate_votes_count: self.total_candidate_votes_count.ok_or_else(
685 || {
686 EMLErrorKind::MissingBuildProperty(
687 "total_candidate_votes_count",
688 )
689 .without_span()
690 },
691 )?,
692 rejected_votes: self.total_rejected_votes,
693 uncounted_votes: self.total_uncounted_votes,
694 }))
695 }
696 },
697 |v| Ok(Some(v)),
698 )?,
699 reporting_unit_votes: self.reporting_unit_votes,
700 })
701 }
702}
703
704impl Default for ElectionCountContestBuilder {
705 fn default() -> Self {
706 Self::new()
707 }
708}
709
710impl EMLElement for ElectionCountContest {
711 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Contest", Some(NS_EML));
712
713 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
714 Ok(collect_struct!(elem, ElectionCountContest {
715 identifier: ContestIdentifier::EML_NAME => |elem| ContestIdentifier::read_eml(elem)?,
716 total_votes as Option: TotalVotes::EML_NAME => |elem| TotalVotes::read_eml(elem)?,
717 reporting_unit_votes as Vec: ReportingUnitVotes::EML_NAME => |elem| ReportingUnitVotes::read_eml(elem)?,
718 }))
719 }
720
721 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
722 writer
723 .child_elem(ContestIdentifier::EML_NAME, &self.identifier)?
724 .child_elem_option(TotalVotes::EML_NAME, self.total_votes.as_ref())?
725 .child_elems(ReportingUnitVotes::EML_NAME, &self.reporting_unit_votes)?
726 .finish()
727 }
728}
729
730const REJECTED_VOTES_EML_NAME: QualifiedName<'_, '_> =
731 QualifiedName::from_static("RejectedVotes", Some(NS_EML));
732
733const UNCOUNTED_VOTES_EML_NAME: QualifiedName<'_, '_> =
734 QualifiedName::from_static("UncountedVotes", Some(NS_EML));
735
736#[derive(Debug, Clone)]
738pub struct TotalVotes {
739 pub selections: Vec<ElectionCountSelection>,
741
742 pub eligible_voter_count: StringValue<u64>,
749
750 pub candidate_votes_count: StringValue<u64>,
757
758 pub rejected_votes: BTreeMap<RejectedVotesReason, StringValue<u64>>,
762
763 pub uncounted_votes: BTreeMap<UncountedVotesReason, StringValue<u64>>,
765}
766
767impl TotalVotes {
768 pub fn selections_per_affiliation(
773 &self,
774 ) -> Result<Vec<SelectionAffiliationVotes<'_>>, EMLError> {
775 selections_per_affiliation(&self.selections)
776 }
777
778 pub fn find_candidate_valid_votes(
780 &self,
781 affiliation_id: AffiliationId,
782 candidate_id: CandidateId,
783 ) -> Result<u64, EMLError> {
784 find_candidate_valid_votes(&self.selections, affiliation_id, candidate_id)
785 }
786
787 pub fn find_affiliation_valid_votes(
789 &self,
790 affiliation_id: AffiliationId,
791 ) -> Result<u64, EMLError> {
792 find_affiliation_valid_votes(&self.selections, affiliation_id)
793 }
794
795 pub fn blank_votes(&self) -> Result<&StringValue<u64>, EMLError> {
797 self.rejected_votes
798 .get(&RejectedVotesReason::Blank)
799 .ok_or_else(|| EMLErrorKind::MissingRejectedVotesBlank.without_span())
800 }
801
802 pub fn invalid_votes(&self) -> Result<&StringValue<u64>, EMLError> {
804 self.rejected_votes
805 .get(&RejectedVotesReason::Invalid)
806 .ok_or_else(|| EMLErrorKind::MissingRejectedVotesInvalid.without_span())
807 }
808}
809
810impl EMLElement for TotalVotes {
811 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("TotalVotes", Some(NS_EML));
812
813 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
814 let data = collect_struct!(elem, TotalVotes {
815 selections as Vec: ElectionCountSelection::EML_NAME => |elem| ElectionCountSelection::read_eml(elem)?,
816 eligible_voter_count: ("Cast", NS_EML) => |elem| elem.string_value()?,
817 candidate_votes_count: ("TotalCounted", NS_EML) => |elem| elem.string_value()?,
818 rejected_votes as BTreeMap: REJECTED_VOTES_EML_NAME => |elem| {
819 let reason_code = elem.attribute_value_req("ReasonCode")?;
820 let reason = RejectedVotesReason::from_eml_value(&reason_code)
821 .map_err(|e| EMLError::invalid_value(REJECTED_VOTES_EML_NAME.as_owned(), e, Some(elem.full_span())))?;
822
823 (reason, elem.string_value()?)
824 },
825 uncounted_votes as BTreeMap: UNCOUNTED_VOTES_EML_NAME => |elem| {
826 let reason_code = elem.attribute_value_req("ReasonCode")?;
827 let reason = UncountedVotesReason::from_eml_value(&reason_code)
828 .map_err(|e| EMLError::invalid_value(UNCOUNTED_VOTES_EML_NAME.as_owned(), e, Some(elem.full_span())))?;
829
830 (reason, elem.string_value()?)
831 },
832 });
833
834 if !data
835 .rejected_votes
836 .contains_key(&RejectedVotesReason::Blank)
837 {
838 let err = EMLErrorKind::MissingRejectedVotesBlank.with_span(elem.full_span());
839 if elem.parsing_mode().is_strict() {
840 return Err(err);
841 } else {
842 elem.push_err(err);
843 }
844 }
845
846 if !data
847 .rejected_votes
848 .contains_key(&RejectedVotesReason::Invalid)
849 {
850 let err = EMLErrorKind::MissingRejectedVotesInvalid.with_span(elem.full_span());
851 if elem.parsing_mode().is_strict() {
852 return Err(err);
853 } else {
854 elem.push_err(err);
855 }
856 }
857
858 Ok(data)
859 }
860
861 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
862 writer
863 .child_elems(ElectionCountSelection::EML_NAME, &self.selections)?
864 .child(("Cast", NS_EML), |elem| {
865 elem.text(self.eligible_voter_count.raw().as_ref())?
866 .finish()
867 })?
868 .child(("TotalCounted", NS_EML), |elem| {
869 elem.text(self.candidate_votes_count.raw().as_ref())?
870 .finish()
871 })?
872 .child_elems_map(
873 REJECTED_VOTES_EML_NAME,
874 &self.rejected_votes,
875 |elem, (reason, count)| {
876 let reason_code = reason.to_eml_value();
877 elem.attr("ReasonCode", reason_code.as_ref())?
878 .text(count.raw().as_ref())?
879 .finish()
880 },
881 )?
882 .child_elems_map(
883 UNCOUNTED_VOTES_EML_NAME,
884 &self.uncounted_votes,
885 |elem, (reason, count)| {
886 let reason_code = reason.to_eml_value();
887 elem.attr("ReasonCode", reason_code.as_ref())?
888 .text(count.raw().as_ref())?
889 .finish()
890 },
891 )?
892 .finish()
893 }
894}
895
896#[derive(Debug, Clone)]
898pub struct ReportingUnitVotes {
899 pub identifier: ReportingUnitIdentifier,
901
902 pub selections: Vec<ElectionCountSelection>,
904
905 pub eligible_voter_count: StringValue<u64>,
912
913 pub candidate_votes_count: StringValue<u64>,
920
921 pub rejected_votes: BTreeMap<RejectedVotesReason, StringValue<u64>>,
925
926 pub uncounted_votes: BTreeMap<UncountedVotesReason, StringValue<u64>>,
928
929 pub investigations: BTreeMap<InvestigationReason, StringValue<bool>>,
931}
932
933pub struct SelectionAffiliationVotes<'a> {
935 pub affiliation: &'a AffiliationSelection,
937
938 pub valid_votes: u64,
940
941 pub candidates: Vec<SelectionCandidateVotes<'a>>,
943}
944
945pub struct SelectionCandidateVotes<'a> {
947 pub affiliation: &'a AffiliationSelection,
949
950 pub candidate: &'a CandidateSelection,
952
953 pub valid_votes: u64,
955}
956
957impl ReportingUnitVotes {
958 pub fn builder() -> ReportingUnitVotesBuilder {
960 ReportingUnitVotesBuilder::new()
961 }
962
963 pub fn selections_per_affiliation(
968 &self,
969 ) -> Result<Vec<SelectionAffiliationVotes<'_>>, EMLError> {
970 selections_per_affiliation(&self.selections)
971 }
972
973 pub fn find_candidate_valid_votes(
975 &self,
976 affiliation_id: AffiliationId,
977 candidate_id: CandidateId,
978 ) -> Result<u64, EMLError> {
979 find_candidate_valid_votes(&self.selections, affiliation_id, candidate_id)
980 }
981
982 pub fn find_affiliation_valid_votes(
984 &self,
985 affiliation_id: AffiliationId,
986 ) -> Result<u64, EMLError> {
987 find_affiliation_valid_votes(&self.selections, affiliation_id)
988 }
989
990 pub fn blank_votes(&self) -> Result<&StringValue<u64>, EMLError> {
992 self.rejected_votes
993 .get(&RejectedVotesReason::Blank)
994 .ok_or_else(|| EMLErrorKind::MissingRejectedVotesBlank.without_span())
995 }
996
997 pub fn invalid_votes(&self) -> Result<&StringValue<u64>, EMLError> {
999 self.rejected_votes
1000 .get(&RejectedVotesReason::Invalid)
1001 .ok_or_else(|| EMLErrorKind::MissingRejectedVotesInvalid.without_span())
1002 }
1003}
1004
1005fn selections_per_affiliation(
1006 selections: &[ElectionCountSelection],
1007) -> Result<Vec<SelectionAffiliationVotes<'_>>, EMLError> {
1008 let mut result = Vec::new();
1009
1010 let mut current_affiliation = None;
1013 let mut current_affiliation_selection: Option<&ElectionCountSelection> = None;
1014 let mut current_candidates = vec![];
1015
1016 for selection in selections {
1017 match &selection.selection_type {
1018 ElectionCountSelectionType::Affiliation(affiliation) => {
1019 if let (Some(ca), Some(cas)) = (current_affiliation, current_affiliation_selection)
1020 {
1021 result.push(SelectionAffiliationVotes {
1022 affiliation: ca,
1023 valid_votes: cas
1024 .valid_votes
1025 .copied_value()
1026 .wrap_field_value_error(VALID_VOTES_EML_NAME)?,
1027 candidates: current_candidates,
1028 });
1029 }
1030
1031 current_affiliation = Some(affiliation);
1032 current_affiliation_selection = Some(selection);
1033 current_candidates = vec![];
1034 }
1035 ElectionCountSelectionType::Candidate(candidate) => {
1036 let curr_aff = current_affiliation
1038 .ok_or_else(|| EMLErrorKind::CandidateWithoutAffiliationFound.without_span())?;
1039
1040 current_candidates.push(SelectionCandidateVotes {
1041 affiliation: curr_aff,
1042 candidate,
1043 valid_votes: selection
1044 .valid_votes
1045 .copied_value()
1046 .wrap_field_value_error(VALID_VOTES_EML_NAME)?,
1047 });
1048 }
1049 ElectionCountSelectionType::ReferendumOption(_) => {
1051 return Err(EMLErrorKind::UnexpectedReferendumOptionSelection.without_span());
1052 }
1053 }
1054 }
1055
1056 if let (Some(ca), Some(cas)) = (current_affiliation, current_affiliation_selection) {
1058 result.push(SelectionAffiliationVotes {
1059 affiliation: ca,
1060 valid_votes: cas
1061 .valid_votes
1062 .copied_value()
1063 .wrap_field_value_error(VALID_VOTES_EML_NAME)?,
1064 candidates: current_candidates,
1065 });
1066 }
1067
1068 Ok(result)
1069}
1070
1071fn find_candidate_valid_votes(
1072 selections: &[ElectionCountSelection],
1073 affiliation_id: AffiliationId,
1074 candidate_id: CandidateId,
1075) -> Result<u64, EMLError> {
1076 let mut last_affiliation_id = None;
1077 for selection in selections {
1078 if let ElectionCountSelectionType::Affiliation(affiliation) = &selection.selection_type {
1079 last_affiliation_id = Some(affiliation.id.copied_value()?);
1080 }
1081
1082 if let ElectionCountSelectionType::Candidate(candidate) = &selection.selection_type
1083 && last_affiliation_id == Some(affiliation_id)
1084 && candidate.identifier.id.copied_value()? == candidate_id
1085 {
1086 return selection.valid_votes.copied_value();
1087 }
1088 }
1089
1090 Err(EMLErrorKind::UnknownCandidate(affiliation_id, candidate_id).without_span())
1091}
1092
1093fn find_affiliation_valid_votes(
1094 selections: &[ElectionCountSelection],
1095 affiliation_id: AffiliationId,
1096) -> Result<u64, EMLError> {
1097 for selection in selections {
1098 if let ElectionCountSelectionType::Affiliation(affiliation) = &selection.selection_type
1099 && affiliation.id.copied_value()? == affiliation_id
1100 {
1101 return selection.valid_votes.copied_value();
1102 }
1103 }
1104
1105 Err(EMLErrorKind::UnknownAffiliation(affiliation_id).without_span())
1106}
1107
1108#[derive(Debug, Clone)]
1110pub struct ReportingUnitVotesBuilder {
1111 identifier: Option<ReportingUnitIdentifier>,
1112 selections: Vec<ElectionCountSelection>,
1113 eligible_voter_count: Option<StringValue<u64>>,
1114 candidate_votes_count: Option<StringValue<u64>>,
1115 rejected_votes: BTreeMap<RejectedVotesReason, StringValue<u64>>,
1116 uncounted_votes: BTreeMap<UncountedVotesReason, StringValue<u64>>,
1117 investigations: BTreeMap<InvestigationReason, StringValue<bool>>,
1118}
1119
1120impl ReportingUnitVotesBuilder {
1121 pub fn new() -> Self {
1123 Self {
1124 identifier: None,
1125 selections: vec![],
1126 eligible_voter_count: None,
1127 candidate_votes_count: None,
1128 rejected_votes: BTreeMap::new(),
1129 uncounted_votes: BTreeMap::new(),
1130 investigations: BTreeMap::new(),
1131 }
1132 }
1133
1134 pub fn identifier(mut self, identifier: impl Into<ReportingUnitIdentifier>) -> Self {
1136 self.identifier = Some(identifier.into());
1137 self
1138 }
1139
1140 pub fn selections(mut self, selections: impl Into<Vec<ElectionCountSelection>>) -> Self {
1142 self.selections = selections.into();
1143 self
1144 }
1145
1146 pub fn push_selection(mut self, selection: impl Into<ElectionCountSelection>) -> Self {
1148 self.selections.push(selection.into());
1149 self
1150 }
1151
1152 pub fn eligible_voter_count(mut self, count: impl Into<u64>) -> Self {
1154 self.eligible_voter_count = Some(StringValue::from_value(count.into()));
1155 self
1156 }
1157
1158 pub fn candidate_votes_count(mut self, count: impl Into<u64>) -> Self {
1160 self.candidate_votes_count = Some(StringValue::from_value(count.into()));
1161 self
1162 }
1163
1164 pub fn rejected_votes(mut self, reason: RejectedVotesReason, count: impl Into<u64>) -> Self {
1166 self.rejected_votes
1167 .insert(reason, StringValue::from_value(count.into()));
1168 self
1169 }
1170
1171 pub fn uncounted_votes(mut self, reason: UncountedVotesReason, count: impl Into<u64>) -> Self {
1173 self.uncounted_votes
1174 .insert(reason, StringValue::from_value(count.into()));
1175 self
1176 }
1177
1178 pub fn investigation(mut self, reason: InvestigationReason, value: bool) -> Self {
1180 self.investigations
1181 .insert(reason, StringValue::from_value(value));
1182 self
1183 }
1184
1185 pub fn build(self) -> Result<ReportingUnitVotes, EMLError> {
1187 if self.selections.is_empty() {
1188 return Err(EMLErrorKind::MissingBuildProperty("selections").without_span());
1189 }
1190
1191 if !self
1192 .rejected_votes
1193 .contains_key(&RejectedVotesReason::Blank)
1194 {
1195 return Err(EMLErrorKind::MissingRejectedVotesBlank).without_span();
1196 }
1197
1198 if !self
1199 .rejected_votes
1200 .contains_key(&RejectedVotesReason::Invalid)
1201 {
1202 return Err(EMLErrorKind::MissingRejectedVotesInvalid).without_span();
1203 }
1204
1205 Ok(ReportingUnitVotes {
1206 identifier: self
1207 .identifier
1208 .ok_or_else(|| EMLErrorKind::MissingBuildProperty("identifier").without_span())?,
1209 selections: self.selections,
1210 eligible_voter_count: self.eligible_voter_count.ok_or_else(|| {
1211 EMLErrorKind::MissingBuildProperty("eligible_voter_count").without_span()
1212 })?,
1213 candidate_votes_count: self.candidate_votes_count.ok_or_else(|| {
1214 EMLErrorKind::MissingBuildProperty("candidate_votes_count").without_span()
1215 })?,
1216 rejected_votes: self.rejected_votes,
1217 uncounted_votes: self.uncounted_votes,
1218 investigations: self.investigations,
1219 })
1220 }
1221}
1222
1223impl Default for ReportingUnitVotesBuilder {
1224 fn default() -> Self {
1225 Self::new()
1226 }
1227}
1228
1229const REPORTING_UNIT_INVESTIGATIONS_EML_NAME: QualifiedName<'_, '_> =
1230 QualifiedName::from_static("ReportingUnitInvestigations", Some(NS_KR));
1231
1232const INVESTIGATION_EML_NAME: QualifiedName<'_, '_> =
1233 QualifiedName::from_static("Investigation", Some(NS_KR));
1234
1235impl EMLElement for ReportingUnitVotes {
1236 const EML_NAME: QualifiedName<'_, '_> =
1237 QualifiedName::from_static("ReportingUnitVotes", Some(NS_EML));
1238
1239 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
1240 struct ReportingUnitVotesInternal {
1241 identifier: ReportingUnitIdentifier,
1242 selections: Vec<ElectionCountSelection>,
1243 eligible_voter_count: StringValue<u64>,
1244 candidate_votes_count: StringValue<u64>,
1245 rejected_votes: BTreeMap<RejectedVotesReason, StringValue<u64>>,
1246 uncounted_votes: BTreeMap<UncountedVotesReason, StringValue<u64>>,
1247 investigations: Option<BTreeMap<InvestigationReason, StringValue<bool>>>,
1248 }
1249
1250 let data = collect_struct!(elem, ReportingUnitVotesInternal {
1251 identifier: ReportingUnitIdentifier::EML_NAME => |elem| ReportingUnitIdentifier::read_eml(elem)?,
1252 selections as Vec: ElectionCountSelection::EML_NAME => |elem| ElectionCountSelection::read_eml(elem)?,
1253 eligible_voter_count: ("Cast", NS_EML) => |elem| elem.string_value()?,
1254 candidate_votes_count: ("TotalCounted", NS_EML) => |elem| elem.string_value()?,
1255 rejected_votes as BTreeMap: REJECTED_VOTES_EML_NAME => |elem| {
1256 let reason_code = elem.attribute_value_req("ReasonCode")?;
1257 let reason = RejectedVotesReason::from_eml_value(&reason_code)
1258 .map_err(|e| EMLError::invalid_value(REJECTED_VOTES_EML_NAME.as_owned(), e, Some(elem.full_span())))?;
1259
1260 (reason, elem.string_value()?)
1261 },
1262 uncounted_votes as BTreeMap: UNCOUNTED_VOTES_EML_NAME => |elem| {
1263 let reason_code = elem.attribute_value_req("ReasonCode")?;
1264 let reason = UncountedVotesReason::from_eml_value(&reason_code)
1265 .map_err(|e| EMLError::invalid_value(UNCOUNTED_VOTES_EML_NAME.as_owned(), e, Some(elem.full_span())))?;
1266
1267 (reason, elem.string_value()?)
1268 },
1269 investigations as Option: REPORTING_UNIT_INVESTIGATIONS_EML_NAME => |elem| {
1270 struct Collector {
1271 investigations: BTreeMap<InvestigationReason, StringValue<bool>>,
1272 }
1273
1274 let data = collect_struct!(elem, Collector {
1275 investigations as BTreeMap: INVESTIGATION_EML_NAME => |elem| {
1276 let reason_code = elem.attribute_value_req("ReasonCode")?;
1277 let reason = InvestigationReason::from_eml_value(&reason_code)
1278 .map_err(|e| EMLError::invalid_value(INVESTIGATION_EML_NAME.as_owned(), e, Some(elem.full_span())))?;
1279
1280 (reason, elem.string_value()?)
1281 },
1282 });
1283
1284 data.investigations
1285 },
1286 });
1287
1288 if !data
1289 .rejected_votes
1290 .contains_key(&RejectedVotesReason::Blank)
1291 {
1292 let err = EMLErrorKind::MissingRejectedVotesBlank.with_span(elem.full_span());
1293 if elem.parsing_mode().is_strict() {
1294 return Err(err);
1295 } else {
1296 elem.push_err(err);
1297 }
1298 }
1299
1300 if !data
1301 .rejected_votes
1302 .contains_key(&RejectedVotesReason::Invalid)
1303 {
1304 let err = EMLErrorKind::MissingRejectedVotesInvalid.with_span(elem.full_span());
1305 if elem.parsing_mode().is_strict() {
1306 return Err(err);
1307 } else {
1308 elem.push_err(err);
1309 }
1310 }
1311
1312 Ok(ReportingUnitVotes {
1313 identifier: data.identifier,
1314 selections: data.selections,
1315 eligible_voter_count: data.eligible_voter_count,
1316 candidate_votes_count: data.candidate_votes_count,
1317 rejected_votes: data.rejected_votes,
1318 uncounted_votes: data.uncounted_votes,
1319 investigations: data.investigations.unwrap_or_default(),
1320 })
1321 }
1322
1323 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
1324 writer
1325 .child_elem(ReportingUnitIdentifier::EML_NAME, &self.identifier)?
1326 .child_elems(ElectionCountSelection::EML_NAME, &self.selections)?
1327 .child(("Cast", NS_EML), |elem| {
1328 elem.text(self.eligible_voter_count.raw().as_ref())?
1329 .finish()
1330 })?
1331 .child(("TotalCounted", NS_EML), |elem| {
1332 elem.text(self.candidate_votes_count.raw().as_ref())?
1333 .finish()
1334 })?
1335 .child_elems_map(
1336 REJECTED_VOTES_EML_NAME,
1337 &self.rejected_votes,
1338 |elem, (reason, count)| {
1339 let reason_code = reason.to_eml_value();
1340 elem.attr("ReasonCode", reason_code.as_ref())?
1341 .text(count.raw().as_ref())?
1342 .finish()
1343 },
1344 )?
1345 .child_elems_map(
1346 UNCOUNTED_VOTES_EML_NAME,
1347 &self.uncounted_votes,
1348 |elem, (reason, count)| {
1349 let reason_code = reason.to_eml_value();
1350 elem.attr("ReasonCode", reason_code.as_ref())?
1351 .text(count.raw().as_ref())?
1352 .finish()
1353 },
1354 )?
1355 .child_option(
1356 REPORTING_UNIT_INVESTIGATIONS_EML_NAME,
1357 if self.investigations.is_empty() {
1358 None
1359 } else {
1360 Some(&self.investigations)
1361 },
1362 |elem, value| {
1363 let mut elem = elem.content()?;
1364
1365 for (reason, investigated) in value {
1366 elem = elem.child(INVESTIGATION_EML_NAME, |elem| {
1367 let reason_code = reason.to_eml_value();
1368 elem.attr("ReasonCode", reason_code.as_ref())?
1369 .text(investigated.raw().as_ref())?
1370 .finish()
1371 })?;
1372 }
1373
1374 elem.finish()
1375 },
1376 )?
1377 .finish()
1378 }
1379}
1380
1381#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
1383pub enum InvestigationReason {
1384 UnexplainedDifference,
1386 OtherError,
1388 ResultCorrected,
1390 AdmittedVotersReestablished,
1392 OtherReason,
1394 PartiallyRecountedBallots,
1396}
1397
1398impl InvestigationReason {
1399 pub fn from_eml_value(s: impl AsRef<str>) -> Result<Self, InvalidInvestigationReasonError> {
1401 let data = s.as_ref();
1402 match data {
1403 "onderzocht vanwege onverklaard verschil" => {
1404 Ok(InvestigationReason::UnexplainedDifference)
1405 }
1406 "onderzocht vanwege andere fout" => Ok(InvestigationReason::OtherError),
1407 "uitslag gecorrigeerd" => Ok(InvestigationReason::ResultCorrected),
1408 "toegelaten kiezers opnieuw vastgesteld" => {
1409 Ok(InvestigationReason::AdmittedVotersReestablished)
1410 }
1411 "onderzocht vanwege andere reden" => Ok(InvestigationReason::OtherReason),
1412 "stembiljetten deels herteld" => Ok(InvestigationReason::PartiallyRecountedBallots),
1413 _ => Err(InvalidInvestigationReasonError(data.to_string())),
1414 }
1415 }
1416
1417 pub fn to_eml_value(&self) -> &'static str {
1419 match self {
1420 InvestigationReason::UnexplainedDifference => "onderzocht vanwege onverklaard verschil",
1421 InvestigationReason::OtherError => "onderzocht vanwege andere fout",
1422 InvestigationReason::ResultCorrected => "uitslag gecorrigeerd",
1423 InvestigationReason::AdmittedVotersReestablished => {
1424 "toegelaten kiezers opnieuw vastgesteld"
1425 }
1426 InvestigationReason::OtherReason => "onderzocht vanwege andere reden",
1427 InvestigationReason::PartiallyRecountedBallots => "stembiljetten deels herteld",
1428 }
1429 }
1430}
1431
1432#[derive(Debug, Clone, thiserror::Error)]
1434#[error("Invalid investigation reason: {0}")]
1435pub struct InvalidInvestigationReasonError(String);
1436
1437#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
1439pub enum UncountedVotesReason {
1440 ValidPollCards,
1442 ValidProxyCertificates,
1444 ValidVoterCards,
1446 AdmittedVoters,
1448 MoreBallotsCounted,
1450 FewerBallotsCounted,
1452 BallotsTaken,
1454 TooFewBallotsIssued,
1456 TooManyBallotsIssued,
1458 NoPostalBallots,
1460 TooManyPostalBallots,
1462 LostBallots,
1464 NoExplanation,
1466 OtherExplanation,
1468}
1469
1470impl UncountedVotesReason {
1471 pub fn from_eml_value(s: impl AsRef<str>) -> Result<Self, InvalidUncountedVotesReasonError> {
1473 match s.as_ref() {
1474 "geldige stempassen" => Ok(UncountedVotesReason::ValidPollCards),
1475 "geldige volmachtbewijzen" => Ok(UncountedVotesReason::ValidProxyCertificates),
1476 "geldige kiezerspassen" => Ok(UncountedVotesReason::ValidVoterCards),
1477 "toegelaten kiezers" => Ok(UncountedVotesReason::AdmittedVoters),
1478 "meer getelde stembiljetten" => Ok(UncountedVotesReason::MoreBallotsCounted),
1479 "minder getelde stembiljetten" => Ok(UncountedVotesReason::FewerBallotsCounted),
1480 "meegenomen stembiljetten" => Ok(UncountedVotesReason::BallotsTaken),
1481 "te weinig uitgereikte stembiljetten" => Ok(UncountedVotesReason::TooFewBallotsIssued),
1482 "te veel uitgereikte stembiljetten" => Ok(UncountedVotesReason::TooManyBallotsIssued),
1483 "geen briefstembiljetten" => Ok(UncountedVotesReason::NoPostalBallots),
1484 "te veel briefstembiljetten" => Ok(UncountedVotesReason::TooManyPostalBallots),
1485 "kwijtgeraakte stembiljetten" => Ok(UncountedVotesReason::LostBallots),
1486 "geen verklaring" => Ok(UncountedVotesReason::NoExplanation),
1487 "andere verklaring" => Ok(UncountedVotesReason::OtherExplanation),
1488 _ => Err(InvalidUncountedVotesReasonError(s.as_ref().to_string())),
1489 }
1490 }
1491
1492 pub fn to_eml_value(&self) -> &'static str {
1494 match self {
1495 UncountedVotesReason::ValidPollCards => "geldige stempassen",
1496 UncountedVotesReason::ValidProxyCertificates => "geldige volmachtbewijzen",
1497 UncountedVotesReason::ValidVoterCards => "geldige kiezerspassen",
1498 UncountedVotesReason::AdmittedVoters => "toegelaten kiezers",
1499 UncountedVotesReason::MoreBallotsCounted => "meer getelde stembiljetten",
1500 UncountedVotesReason::FewerBallotsCounted => "minder getelde stembiljetten",
1501 UncountedVotesReason::BallotsTaken => "meegenomen stembiljetten",
1502 UncountedVotesReason::TooFewBallotsIssued => "te weinig uitgereikte stembiljetten",
1503 UncountedVotesReason::TooManyBallotsIssued => "te veel uitgereikte stembiljetten",
1504 UncountedVotesReason::NoPostalBallots => "geen briefstembiljetten",
1505 UncountedVotesReason::TooManyPostalBallots => "te veel briefstembiljetten",
1506 UncountedVotesReason::LostBallots => "kwijtgeraakte stembiljetten",
1507 UncountedVotesReason::NoExplanation => "geen verklaring",
1508 UncountedVotesReason::OtherExplanation => "andere verklaring",
1509 }
1510 }
1511}
1512
1513#[derive(Debug, Clone, thiserror::Error)]
1515#[error("Invalid uncounted votes reason: {0}")]
1516pub struct InvalidUncountedVotesReasonError(String);
1517
1518#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
1520pub enum RejectedVotesReason {
1521 Blank,
1523 Invalid,
1525}
1526
1527impl RejectedVotesReason {
1528 pub fn from_eml_value(s: impl AsRef<str>) -> Result<Self, InvalidRejectedVotesReasonError> {
1530 let data = s.as_ref();
1531 match data {
1532 "blanco" => Ok(RejectedVotesReason::Blank),
1533 "ongeldig" => Ok(RejectedVotesReason::Invalid),
1534 _ => Err(InvalidRejectedVotesReasonError(data.to_string())),
1535 }
1536 }
1537
1538 pub fn to_eml_value(&self) -> &'static str {
1540 match self {
1541 RejectedVotesReason::Blank => "blanco",
1542 RejectedVotesReason::Invalid => "ongeldig",
1543 }
1544 }
1545}
1546
1547#[derive(Debug, Clone, thiserror::Error)]
1549#[error("Invalid rejected votes reason: {0}")]
1550pub struct InvalidRejectedVotesReasonError(String);
1551
1552#[derive(Debug, Clone)]
1554pub struct ElectionCountSelection {
1555 pub selection_type: ElectionCountSelectionType,
1557
1558 pub valid_votes: StringValue<u64>,
1560
1561 pub value: Option<Box<str>>,
1563
1564 pub category: Option<Box<str>>,
1566}
1567
1568impl ElectionCountSelection {
1569 pub fn builder() -> ElectionCountSelectionBuilder {
1571 ElectionCountSelectionBuilder::new()
1572 }
1573}
1574
1575#[derive(Debug, Clone)]
1577pub struct ElectionCountSelectionBuilder {
1578 selection_type: Option<ElectionCountSelectionType>,
1579 valid_votes: Option<StringValue<u64>>,
1580 value: Option<Box<str>>,
1581 category: Option<Box<str>>,
1582}
1583
1584impl ElectionCountSelectionBuilder {
1585 pub fn new() -> Self {
1587 Self {
1588 selection_type: None,
1589 valid_votes: None,
1590 value: None,
1591 category: None,
1592 }
1593 }
1594
1595 pub fn candidate(mut self, candidate: impl Into<CandidateSelection>) -> Self {
1597 self.selection_type = Some(ElectionCountSelectionType::Candidate(Box::new(
1598 candidate.into(),
1599 )));
1600 self
1601 }
1602
1603 pub fn affiliation(mut self, affiliation: impl Into<AffiliationSelection>) -> Self {
1605 self.selection_type = Some(ElectionCountSelectionType::Affiliation(Box::new(
1606 affiliation.into(),
1607 )));
1608 self
1609 }
1610
1611 pub fn referendum_option(
1613 mut self,
1614 referendum_option: impl Into<ReferendumOptionSelection>,
1615 ) -> Self {
1616 self.selection_type = Some(ElectionCountSelectionType::ReferendumOption(Box::new(
1617 referendum_option.into(),
1618 )));
1619 self
1620 }
1621
1622 pub fn valid_votes(mut self, valid_votes: impl Into<u64>) -> Self {
1624 self.valid_votes = Some(StringValue::from_value(valid_votes.into()));
1625 self
1626 }
1627
1628 pub fn value(mut self, value: impl Into<Box<str>>) -> Self {
1630 self.value = Some(value.into());
1631 self
1632 }
1633
1634 pub fn category(mut self, category: impl Into<Box<str>>) -> Self {
1636 self.category = Some(category.into());
1637 self
1638 }
1639
1640 pub fn build(self) -> Result<ElectionCountSelection, EMLError> {
1642 Ok(ElectionCountSelection {
1643 selection_type: self.selection_type.ok_or_else(|| {
1644 EMLErrorKind::MissingBuildProperty("selection_type").without_span()
1645 })?,
1646 valid_votes: self
1647 .valid_votes
1648 .ok_or_else(|| EMLErrorKind::MissingBuildProperty("valid_votes").without_span())?,
1649 value: self.value,
1650 category: self.category,
1651 })
1652 }
1653}
1654
1655impl Default for ElectionCountSelectionBuilder {
1656 fn default() -> Self {
1657 Self::new()
1658 }
1659}
1660
1661const VALID_VOTES_EML_NAME: QualifiedName<'_, '_> =
1662 QualifiedName::from_static("ValidVotes", Some(NS_EML));
1663
1664impl EMLElement for ElectionCountSelection {
1665 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Selection", Some(NS_EML));
1666
1667 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
1668 let value = elem.attribute_value("Value")?.map(|s| s.into());
1669 let category = elem.attribute_value("Category")?.map(|s| s.into());
1670 let mut selection_type = None;
1671 let mut valid_votes = None;
1672
1673 while let Some(mut child) = elem.next_child()? {
1674 let name = child.name()?;
1675
1676 match name {
1677 n if n == CandidateSelection::EML_NAME => {
1678 selection_type = Some(ElectionCountSelectionType::Candidate(Box::new(
1679 CandidateSelection::read_eml(&mut child)?,
1680 )));
1681 }
1682 n if n == AffiliationSelection::EML_NAME => {
1683 selection_type = Some(ElectionCountSelectionType::Affiliation(Box::new(
1684 AffiliationSelection::read_eml(&mut child)?,
1685 )));
1686 }
1687 n if n == ReferendumOptionSelection::EML_NAME => {
1688 selection_type = Some(ElectionCountSelectionType::ReferendumOption(Box::new(
1689 ReferendumOptionSelection::read_eml(&mut child)?,
1690 )));
1691 }
1692 n if n == VALID_VOTES_EML_NAME => {
1693 valid_votes = Some(child.string_value()?);
1694 }
1695 n => {
1696 let err =
1697 EMLErrorKind::UnexpectedElement(n.as_owned(), Self::EML_NAME.as_owned())
1698 .with_span(child.inner_span());
1699 if child.parsing_mode().is_strict() {
1700 return Err(err);
1701 } else {
1702 child.push_err(err);
1703 child.skip()?;
1704 }
1705 }
1706 }
1707 }
1708 Ok(ElectionCountSelection {
1709 selection_type: selection_type
1710 .ok_or_else(|| EMLErrorKind::MissingSelectionType.with_span(elem.inner_span()))?,
1711 valid_votes: valid_votes.ok_or_else(|| {
1712 EMLErrorKind::MissingElement(VALID_VOTES_EML_NAME.as_owned())
1713 .with_span(elem.inner_span())
1714 })?,
1715 value,
1716 category,
1717 })
1718 }
1719
1720 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
1721 let writer = writer
1722 .attr_opt("Value", self.value.as_deref())?
1723 .attr_opt("Category", self.category.as_deref())?;
1724 let writer = match &self.selection_type {
1725 ElectionCountSelectionType::Candidate(candidate_selection) => {
1726 writer.child_elem(CandidateSelection::EML_NAME, candidate_selection.as_ref())?
1727 }
1728 ElectionCountSelectionType::Affiliation(affiliation_selection) => writer.child_elem(
1729 AffiliationSelection::EML_NAME,
1730 affiliation_selection.as_ref(),
1731 )?,
1732 ElectionCountSelectionType::ReferendumOption(referendum_option_selection) => writer
1733 .child_elem(
1734 ReferendumOptionSelection::EML_NAME,
1735 referendum_option_selection.as_ref(),
1736 )?,
1737 };
1738 writer
1739 .child(("ValidVotes", NS_EML), |elem| {
1740 elem.text(self.valid_votes.raw().as_ref())?.finish()
1741 })?
1742 .finish()
1743 }
1744}
1745
1746#[derive(Debug, Clone)]
1748pub enum ElectionCountSelectionType {
1749 Candidate(Box<CandidateSelection>),
1751
1752 Affiliation(Box<AffiliationSelection>),
1754
1755 ReferendumOption(Box<ReferendumOptionSelection>),
1757}
1758
1759impl ElectionCountSelectionType {
1760 pub fn is_candidate(&self) -> bool {
1762 matches!(self, ElectionCountSelectionType::Candidate(_))
1763 }
1764
1765 pub fn is_affiliation(&self) -> bool {
1767 matches!(self, ElectionCountSelectionType::Affiliation(_))
1768 }
1769
1770 pub fn is_referendum_option(&self) -> bool {
1772 matches!(self, ElectionCountSelectionType::ReferendumOption(_))
1773 }
1774
1775 pub fn as_candidate(&self) -> Option<&CandidateSelection> {
1777 if let ElectionCountSelectionType::Candidate(candidate_selection) = self {
1778 Some(candidate_selection)
1779 } else {
1780 None
1781 }
1782 }
1783
1784 pub fn as_affiliation(&self) -> Option<&AffiliationSelection> {
1786 if let ElectionCountSelectionType::Affiliation(affiliation_selection) = self {
1787 Some(affiliation_selection)
1788 } else {
1789 None
1790 }
1791 }
1792
1793 pub fn as_referendum_option(&self) -> Option<&ReferendumOptionSelection> {
1795 if let ElectionCountSelectionType::ReferendumOption(referendum_option_selection) = self {
1796 Some(referendum_option_selection)
1797 } else {
1798 None
1799 }
1800 }
1801}
1802
1803#[derive(Debug, Clone)]
1805pub struct CandidateSelection {
1806 pub identifier: CandidateIdentifier,
1808
1809 pub name: Option<PersonNameStructure>,
1811
1812 pub gender: Option<StringValue<Gender>>,
1814
1815 pub qualifying_address: Option<MinimalQualifyingAddress>,
1817}
1818
1819impl CandidateSelection {
1820 pub fn builder() -> CandidateSelectionBuilder {
1822 CandidateSelectionBuilder::new()
1823 }
1824}
1825
1826impl From<CandidateIdentifier> for CandidateSelection {
1827 fn from(identifier: CandidateIdentifier) -> Self {
1828 CandidateSelection {
1829 identifier,
1830 name: None,
1831 gender: None,
1832 qualifying_address: None,
1833 }
1834 }
1835}
1836
1837impl From<CandidateId> for CandidateSelection {
1838 fn from(id: CandidateId) -> Self {
1839 CandidateSelection::from(CandidateIdentifier::from(id))
1840 }
1841}
1842
1843#[derive(Debug, Clone)]
1845pub struct CandidateSelectionBuilder {
1846 identifier: Option<CandidateIdentifier>,
1847 name: Option<PersonNameStructure>,
1848 gender: Option<StringValue<Gender>>,
1849 qualifying_address: Option<MinimalQualifyingAddress>,
1850 locality_name: Option<Box<str>>,
1851 country_name_code: Option<Box<str>>,
1852}
1853
1854impl CandidateSelectionBuilder {
1855 pub fn new() -> Self {
1857 CandidateSelectionBuilder {
1858 identifier: None,
1859 name: None,
1860 gender: None,
1861 qualifying_address: None,
1862 locality_name: None,
1863 country_name_code: None,
1864 }
1865 }
1866
1867 pub fn identifier(mut self, identifier: impl Into<CandidateIdentifier>) -> Self {
1869 self.identifier = Some(identifier.into());
1870 self
1871 }
1872
1873 pub fn name(mut self, name: impl Into<PersonNameStructure>) -> Self {
1875 self.name = Some(name.into());
1876 self
1877 }
1878
1879 pub fn gender(mut self, gender: impl Into<Gender>) -> Self {
1881 self.gender = Some(StringValue::from_value(gender.into()));
1882 self
1883 }
1884
1885 pub fn qualifying_address(
1890 mut self,
1891 qualifying_address: impl Into<MinimalQualifyingAddress>,
1892 ) -> Self {
1893 self.qualifying_address = Some(qualifying_address.into());
1894 self
1895 }
1896
1897 pub fn locality_name(mut self, locality_name: impl Into<Box<str>>) -> Self {
1902 self.locality_name = Some(locality_name.into());
1903 self
1904 }
1905
1906 pub fn country_name_code(mut self, country_name_code: impl Into<Box<str>>) -> Self {
1911 self.country_name_code = Some(country_name_code.into());
1912 self
1913 }
1914
1915 pub fn build(self) -> Result<CandidateSelection, EMLError> {
1917 Ok(CandidateSelection {
1918 identifier: self
1919 .identifier
1920 .ok_or_else(|| EMLErrorKind::MissingBuildProperty("identifier").without_span())?,
1921 name: self.name,
1922 gender: self.gender,
1923 qualifying_address: self.qualifying_address.map_or_else(
1924 || {
1925 if let Some(locality_name) = self.locality_name {
1926 if let Some(country_name_code) = self.country_name_code {
1927 Ok(Some(MinimalQualifyingAddress::new_country(
1928 country_name_code,
1929 locality_name,
1930 )))
1931 } else {
1932 Ok(Some(MinimalQualifyingAddress::new_locality(locality_name)))
1933 }
1934 } else {
1935 if self.country_name_code.is_some() {
1936 return Err(
1937 EMLErrorKind::MissingBuildProperty("locality_name").without_span()
1938 );
1939 }
1940 Ok(None)
1941 }
1942 },
1943 |address| Ok(Some(address)),
1944 )?,
1945 })
1946 }
1947}
1948
1949impl Default for CandidateSelectionBuilder {
1950 fn default() -> Self {
1951 Self::new()
1952 }
1953}
1954
1955impl EMLElement for CandidateSelection {
1956 const EML_NAME: QualifiedName<'_, '_> = QualifiedName::from_static("Candidate", Some(NS_EML));
1957
1958 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
1959 Ok(collect_struct!(elem, CandidateSelection {
1960 identifier: CandidateIdentifier::EML_NAME => |elem| CandidateIdentifier::read_eml(elem)?,
1961 name as Option: ("CandidateFullName", NS_EML) => |elem| PersonNameStructure::read_eml_element(elem)?,
1962 gender as Option: ("Gender", NS_EML) => |elem| elem.string_value()?,
1963 qualifying_address as Option: MinimalQualifyingAddress::EML_NAME => |elem| MinimalQualifyingAddress::read_eml(elem)?,
1964 }))
1965 }
1966
1967 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
1968 writer
1969 .child_elem(CandidateIdentifier::EML_NAME, &self.identifier)?
1970 .child_option(
1971 ("CandidateFullName", NS_EML),
1972 self.name.as_ref(),
1973 |elem, value| value.write_eml_element(elem),
1974 )?
1975 .child_option(("Gender", NS_EML), self.gender.as_ref(), |elem, value| {
1976 elem.text(value.raw().as_ref())?.finish()
1977 })?
1978 .child_elem_option(
1979 MinimalQualifyingAddress::EML_NAME,
1980 self.qualifying_address.as_ref(),
1981 )?
1982 .finish()
1983 }
1984}
1985
1986#[derive(Debug, Clone)]
1988pub struct AffiliationSelection {
1989 pub id: StringValue<AffiliationId>,
1991
1992 pub name: Box<str>,
1994}
1995
1996impl AffiliationSelection {
1997 pub fn new(id: impl Into<AffiliationId>, name: impl Into<Box<str>>) -> Self {
1999 AffiliationSelection {
2000 id: StringValue::from_value(id.into()),
2001 name: name.into(),
2002 }
2003 }
2004}
2005
2006impl EMLElement for AffiliationSelection {
2007 const EML_NAME: QualifiedName<'_, '_> =
2008 QualifiedName::from_static("AffiliationIdentifier", Some(NS_EML));
2009
2010 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
2011 Ok(collect_struct!(elem, AffiliationSelection {
2012 id: elem.string_value_attr("Id", None)?,
2013 name: ("RegisteredName", NS_EML) => |elem| elem.text_without_children()?,
2014 }))
2015 }
2016
2017 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
2018 writer
2019 .attr("Id", self.id.raw().as_ref())?
2020 .child(("RegisteredName", NS_EML), |elem| {
2021 elem.text(self.name.as_ref())?.finish()
2022 })?
2023 .finish()
2024 }
2025}
2026
2027#[derive(Debug, Clone)]
2029pub struct ReferendumOptionSelection {
2030 pub value: Box<str>,
2032
2033 pub id: Option<Box<str>>,
2035
2036 pub display_order: Option<StringValue<NonZeroU64>>,
2038
2039 pub short_code: Option<Box<str>>,
2041
2042 pub expected_confirmation_reference: Option<Box<str>>,
2044}
2045
2046impl ReferendumOptionSelection {
2047 pub fn new(value: impl Into<Box<str>>) -> Self {
2049 ReferendumOptionSelection {
2050 value: value.into(),
2051 id: None,
2052 display_order: None,
2053 short_code: None,
2054 expected_confirmation_reference: None,
2055 }
2056 }
2057
2058 pub fn with_id(mut self, id: impl Into<Box<str>>) -> Self {
2060 self.id = Some(id.into());
2061 self
2062 }
2063
2064 pub fn with_display_order(mut self, display_order: impl Into<NonZeroU64>) -> Self {
2066 self.display_order = Some(StringValue::from_value(display_order.into()));
2067 self
2068 }
2069
2070 pub fn with_short_code(mut self, short_code: impl Into<Box<str>>) -> Self {
2072 self.short_code = Some(short_code.into());
2073 self
2074 }
2075
2076 pub fn with_expected_confirmation_reference(
2078 mut self,
2079 expected_confirmation_reference: impl Into<Box<str>>,
2080 ) -> Self {
2081 self.expected_confirmation_reference = Some(expected_confirmation_reference.into());
2082 self
2083 }
2084}
2085
2086impl EMLElement for ReferendumOptionSelection {
2087 const EML_NAME: QualifiedName<'_, '_> =
2088 QualifiedName::from_static("ReferendumOptionIdentifier", Some(NS_EML));
2089
2090 fn read_eml(elem: &mut EMLElementReader<'_, '_>) -> Result<Self, EMLError> {
2091 Ok(ReferendumOptionSelection {
2092 value: elem.text_without_children()?,
2093 id: elem.attribute_value("Id")?.map(|s| s.into()),
2094 display_order: elem.string_value_attr_opt("DisplayOrder")?,
2095 short_code: elem.attribute_value("ShortCode")?.map(|s| s.into()),
2096 expected_confirmation_reference: elem
2097 .attribute_value("ExpectedConfirmationReference")?
2098 .map(|s| s.into()),
2099 })
2100 }
2101
2102 fn write_eml(&self, writer: EMLElementWriter) -> Result<(), EMLError> {
2103 writer
2104 .attr_opt("Id", self.id.as_deref())?
2105 .attr_opt("DisplayOrder", self.display_order.as_ref().map(|s| s.raw()))?
2106 .attr_opt("ShortCode", self.short_code.as_deref())?
2107 .attr_opt(
2108 "ExpectedConfirmationReference",
2109 self.expected_confirmation_reference.as_deref(),
2110 )?
2111 .text(self.value.as_ref())?
2112 .finish()
2113 }
2114}
2115
2116#[cfg(test)]
2117mod tests {
2118 use chrono::{NaiveDate, TimeZone as _};
2119
2120 use crate::{
2121 common::{AuthorityIdentifier, PersonName},
2122 io::{EMLParsingMode, EMLRead, EMLWrite},
2123 utils::{AuthorityId, CandidateId, ReportingUnitIdentifierId},
2124 };
2125
2126 use super::*;
2127
2128 #[test]
2129 fn test_election_count_construction() {
2130 let ec = ElectionCount::builder()
2131 .count_type(CountType::Municipal)
2132 .transaction_id(TransactionId::new(1))
2133 .creation_date_time(
2134 chrono::Utc
2135 .with_ymd_and_hms(2014, 11, 28, 12, 0, 9)
2136 .unwrap(),
2137 )
2138 .managing_authority(ManagingAuthority::new(
2139 AuthorityIdentifier::new(AuthorityId::new("1234").unwrap()).with_name("Rotterdam"),
2140 ))
2141 .election_identifier(
2142 ElectionCountElectionIdentifier::builder()
2143 .id(ElectionId::new("GR2222_Cyber").unwrap())
2144 .category(ElectionCategory::GR)
2145 .election_date(NaiveDate::from_ymd_opt(2222, 11, 16).unwrap())
2146 .build_for_count()
2147 .unwrap(),
2148 )
2149 .contests([ElectionCountContest::builder()
2150 .identifier(ContestIdentifier::geen())
2151 .total_candidate_votes_count(65u64)
2152 .total_eligible_voter_count(100u64)
2153 .total_rejected_votes(RejectedVotesReason::Blank, 100u64)
2154 .total_rejected_votes(RejectedVotesReason::Invalid, 0u64)
2155 .total_uncounted_votes(UncountedVotesReason::AdmittedVoters, 10u64)
2156 .total_votes_selections([
2157 ElectionCountSelection::builder()
2158 .affiliation(AffiliationSelection::new(
2159 AffiliationId::new(NonZeroU64::new(1).unwrap()),
2160 "Example",
2161 ))
2162 .valid_votes(16u64)
2163 .build()
2164 .unwrap(),
2165 ElectionCountSelection::builder()
2166 .candidate(
2167 CandidateSelection::builder()
2168 .identifier(CandidateId::new(NonZeroU64::new(1).unwrap()))
2169 .name(PersonName::new("Smid").with_first_name("Example"))
2170 .build()
2171 .unwrap(),
2172 )
2173 .valid_votes(16u64)
2174 .build()
2175 .unwrap(),
2176 ])
2177 .reporting_unit_votes([ReportingUnitVotes::builder()
2178 .identifier(ReportingUnitIdentifier::new(
2179 ReportingUnitIdentifierId::new("SB1234").unwrap(),
2180 "Stembureau",
2181 ))
2182 .rejected_votes(RejectedVotesReason::Blank, 10u64)
2183 .rejected_votes(RejectedVotesReason::Invalid, 0u64)
2184 .uncounted_votes(UncountedVotesReason::AdmittedVoters, 5u64)
2185 .candidate_votes_count(10u64)
2186 .eligible_voter_count(100u64)
2187 .selections([
2188 ElectionCountSelection::builder()
2189 .affiliation(AffiliationSelection::new(
2190 AffiliationId::new(NonZeroU64::new(1).unwrap()),
2191 "Example",
2192 ))
2193 .valid_votes(16u64)
2194 .build()
2195 .unwrap(),
2196 ElectionCountSelection::builder()
2197 .candidate(
2198 CandidateSelection::builder()
2199 .identifier(CandidateId::new(NonZeroU64::new(1).unwrap()))
2200 .name(PersonName::new("Smid").with_first_name("Example"))
2201 .build()
2202 .unwrap(),
2203 )
2204 .valid_votes(16u64)
2205 .build()
2206 .unwrap(),
2207 ])
2208 .build()
2209 .unwrap()])
2210 .build()
2211 .unwrap()])
2212 .build()
2213 .unwrap();
2214
2215 let xml = ec.write_eml_root_str(true, true).unwrap();
2216 assert_eq!(
2217 xml,
2218 include_str!("../../test-files/election_count/eml510b_construction_output.eml.xml")
2219 );
2220
2221 let parsed = ElectionCount::parse_eml(&xml, EMLParsingMode::Strict).unwrap();
2223 let xml2 = parsed.write_eml_root_str(true, true).unwrap();
2224 assert_eq!(xml, xml2);
2225 }
2226
2227 #[test]
2228 fn test_parse_510b() {
2229 let xml = include_str!("../../test-files/election_count/deserialize_eml510b_test.eml.xml");
2230
2231 assert!(
2232 ElectionCount::parse_eml(xml, EMLParsingMode::Strict)
2233 .ok_with_errors()
2234 .is_ok()
2235 );
2236 }
2237
2238 #[test]
2239 fn test_parse_510d() {
2240 let xml = include_str!("../../test-files/election_count/deserialize_eml510d_test.eml.xml");
2241
2242 assert!(
2243 ElectionCount::parse_eml(xml, EMLParsingMode::Strict)
2244 .ok_with_errors()
2245 .is_ok()
2246 );
2247 }
2248
2249 #[test]
2250 fn test_parse_with_investigations() {
2251 let xml =
2252 include_str!("../../test-files/election_count/eml510b_with_investigations.eml.xml");
2253
2254 assert!(
2255 ElectionCount::parse_eml(xml, EMLParsingMode::Strict)
2256 .ok_with_errors()
2257 .is_ok()
2258 );
2259 }
2260
2261 #[test]
2262 fn test_total_votes_find_votes() {
2263 let xml = include_str!("../../test-files/csv/Telling_GR2022_WestMaasenWaal.eml.xml");
2264 let eml = ElectionCount::parse_eml(xml, EMLParsingMode::Strict)
2265 .ok()
2266 .unwrap();
2267
2268 let contest = eml.count.election.contests.first().unwrap();
2269 let total_votes = contest.total_votes.as_ref().unwrap();
2270
2271 assert_eq!(
2272 total_votes
2273 .find_affiliation_valid_votes(AffiliationId::from_u64(1).unwrap())
2274 .unwrap(),
2275 1893
2276 );
2277 assert_eq!(
2278 total_votes
2279 .find_candidate_valid_votes(
2280 AffiliationId::from_u64(1).unwrap(),
2281 CandidateId::from_u64(1).unwrap()
2282 )
2283 .unwrap(),
2284 581
2285 );
2286 assert_eq!(
2287 total_votes
2288 .find_affiliation_valid_votes(AffiliationId::from_u64(2).unwrap())
2289 .unwrap(),
2290 1345
2291 );
2292 assert_eq!(
2293 total_votes
2294 .find_candidate_valid_votes(
2295 AffiliationId::from_u64(2).unwrap(),
2296 CandidateId::from_u64(2).unwrap()
2297 )
2298 .unwrap(),
2299 85
2300 );
2301 }
2302
2303 #[test]
2304 fn test_reporting_unit_find_votes() {
2305 let xml = include_str!("../../test-files/csv/Telling_GR2022_WestMaasenWaal.eml.xml");
2306 let eml = ElectionCount::parse_eml(xml, EMLParsingMode::Strict)
2307 .ok()
2308 .unwrap();
2309
2310 let contest = eml.count.election.contests.first().unwrap();
2311 let first_ps = contest.reporting_unit_votes.first().unwrap();
2312
2313 assert_eq!(
2314 first_ps
2315 .find_affiliation_valid_votes(AffiliationId::from_u64(1).unwrap())
2316 .unwrap(),
2317 3
2318 );
2319 assert_eq!(
2320 first_ps
2321 .find_candidate_valid_votes(
2322 AffiliationId::from_u64(1).unwrap(),
2323 CandidateId::from_u64(1).unwrap()
2324 )
2325 .unwrap(),
2326 0
2327 );
2328 assert_eq!(
2329 first_ps
2330 .find_affiliation_valid_votes(AffiliationId::from_u64(2).unwrap())
2331 .unwrap(),
2332 181
2333 );
2334 assert_eq!(
2335 first_ps
2336 .find_candidate_valid_votes(
2337 AffiliationId::from_u64(2).unwrap(),
2338 CandidateId::from_u64(2).unwrap()
2339 )
2340 .unwrap(),
2341 0
2342 );
2343 }
2344}