Skip to main content

eml_nl/documents/
election_count.rs

1//! Document variant for the EML_NL Count (`510a`, `510b`, `510c` or `510d`) document.
2
3use 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/// Representing a `510a`, `510b`, `510c` or `510d` document, containing a count.
25#[derive(Debug, Clone)]
26pub struct ElectionCount {
27    /// Type of count document.
28    pub count_type: CountType,
29
30    /// Transaction ID of the document.
31    pub transaction_id: TransactionId,
32
33    /// Managing authority of the document.
34    pub managing_authority: ManagingAuthority,
35
36    /// Creation date and time of the document.
37    pub creation_date_time: CreationDateTime,
38
39    /// Canonicalization method used in this document, if present.
40    pub canonicalization_method: Option<CanonicalizationMethod>,
41
42    /// The actual count data.
43    pub count: ElectionCountCount,
44}
45
46impl ElectionCount {
47    /// Create a builder for the [`ElectionCount`] document.
48    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/// Builder for [`ElectionCount`].
81#[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    /// Create a new ElectionCountBuilder for building [`ElectionCount`] documents.
95    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    /// Set the count type for the document.
109    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    /// Set the transaction id for the document.
115    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    /// Set the managing authority for the document.
121    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    /// Set the creation date and time for the document.
127    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    /// Set the canonicalization method for the document.
133    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    /// Set the count for the document.
142    ///
143    /// You may either set the entire election count at once using this method,
144    /// or use any of [`Self::election_identifier`], [`Self::contests`] and/or
145    /// [`Self::push_contest`] to construct the individual components of the
146    /// count document.
147    pub fn count(mut self, count: impl Into<ElectionCountCount>) -> Self {
148        self.count = Some(count.into());
149        self
150    }
151
152    /// Set the election identifier for the document.
153    ///
154    /// This only has effect if the count was not set using the  [`Self::count`]
155    /// method on this builder.
156    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    /// Set the contests for the document. This overrides any previously set contests.
165    ///
166    /// This only has effect if the count was not set using the  [`Self::count`]
167    /// method on this builder.
168    pub fn contests(mut self, contests: impl Into<Vec<ElectionCountContest>>) -> Self {
169        self.contests = contests.into();
170        self
171    }
172
173    /// Add a contest to the document.
174    ///
175    /// This only has effect if the count was not set using the  [`Self::count`]
176    /// method on this builder.
177    pub fn push_contest(mut self, contest: impl Into<ElectionCountContest>) -> Self {
178        self.contests.push(contest.into());
179        self
180    }
181
182    /// Build the [`ElectionCount`] document, returning an error if any of the required fields are missing.
183    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            // Note: we don't output the CanonicalizationMethod because we aren't canonicalizing our output
251            // .child_elem_option(
252            //     CanonicalizationMethod::EML_NAME,
253            //     self.canonicalization_method.as_ref(),
254            // )?
255            .child_elem(ElectionCountCount::EML_NAME, &self.count)?
256            .finish()
257    }
258}
259
260/// EML document ID for Count of polling stationdocuments.
261pub(crate) const EML_COUNT_POLLING_STATION_ID: &str = "510a";
262
263/// EML document ID for Count of municipality documents.
264pub(crate) const EML_COUNT_MUNICIPAL_ID: &str = "510b";
265
266/// EML document ID for Count of district documents.
267pub(crate) const EML_COUNT_DISTRICT_ID: &str = "510c";
268
269/// EML document ID for central Count documents.
270pub(crate) const EML_COUNT_CENTRAL_ID: &str = "510d";
271
272/// Type of Count document.
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum CountType {
275    /// Representing a `510a` document, containing the count for a polling station.
276    PollingStation,
277    /// Representing a `510b` document, containing the count for a local region (= municipality).
278    Municipal,
279    /// Representing a `510c` document, containing the count for a district (= HSB).
280    District,
281    /// Representing a `510d` document, containing the count for the entire election.
282    Central,
283}
284
285impl CountType {
286    /// Create a CountType from an EML document ID string.
287    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    /// Get the EML document ID string for this CountType.
302    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    /// Get a friendly name for this CountType.
312    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    /// Returns if the given EML document ID string is a valid CountType ID.
322    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/// The actual count data.
334#[derive(Debug, Clone)]
335pub struct ElectionCountCount {
336    /// The election for this count.
337    pub election: ElectionCountElection,
338}
339
340impl ElectionCountCount {
341    /// Create a new count for the election count document.
342    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/// The election for this count.
374#[derive(Debug, Clone)]
375pub struct ElectionCountElection {
376    /// Identifier
377    pub identifier: ElectionCountElectionIdentifier,
378
379    /// Contests within this election.
380    pub contests: Vec<ElectionCountContest>,
381}
382
383impl ElectionCountElection {
384    /// Create a new election for the election count document.
385    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/// Identifier for the election in this count.
441#[derive(Debug, Clone)]
442pub struct ElectionCountElectionIdentifier {
443    /// Id of the election
444    pub id: StringValue<ElectionId>,
445
446    /// Name of the election
447    pub name: Option<Box<str>>,
448
449    /// Category of the election
450    pub category: StringValue<ElectionCategory>,
451
452    /// Subcategory of the election
453    pub subcategory: Option<StringValue<ElectionSubcategory>>,
454
455    /// The (top level) region where the election takes place.
456    pub domain: Option<ElectionDomain>,
457
458    /// Date of the election
459    pub election_date: StringValue<XsDate>,
460}
461
462impl ElectionCountElectionIdentifier {
463    /// Create a builder for the [`ElectionCountElectionIdentifier`].
464    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/// A contest within the election count.
509#[derive(Debug, Clone)]
510pub struct ElectionCountContest {
511    /// Identifier for the contest.
512    pub identifier: ContestIdentifier,
513
514    /// Total votes in this contest, if present.
515    pub total_votes: Option<TotalVotes>,
516
517    /// Votes per reporting unit in this contest.
518    pub reporting_unit_votes: Vec<ReportingUnitVotes>,
519}
520
521impl ElectionCountContest {
522    /// Create a builder for the [`ElectionCountContest`].
523    pub fn builder() -> ElectionCountContestBuilder {
524        ElectionCountContestBuilder::new()
525    }
526}
527
528/// A builder for [`ElectionCountContest`].
529#[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    /// Create a new ElectionCountContestBuilder for building [`ElectionCountContest`] documents.
543    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    /// Set the identifier for the contest.
557    pub fn identifier(mut self, identifier: impl Into<ContestIdentifier>) -> Self {
558        self.identifier = Some(identifier.into());
559        self
560    }
561
562    /// Set the total votes for the contest.
563    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    /// Set the selections within the total votes for the contest. This overrides any previously set selections.
569    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    /// Add a selection to the selections within the total votes for the contest.
578    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    /// Set the total number of eligible voters within the contest.
587    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    /// Set the total number of votes on candidates within the contest.
593    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    /// Set the total number of rejected votes within the contest for a given reason.
599    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    /// Set the total number of uncounted votes within the contest for a given reason.
610    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    /// Set the details for all the reporting units within the contest. This
621    /// overrides any previously set reporting unit votes.
622    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    /// Add the details for a reporting unit within the contest.
631    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    /// Build the [`ElectionCountContest`] document, returning an error if any of the required fields are missing.
640    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/// Total votes in a contest.
737#[derive(Debug, Clone)]
738pub struct TotalVotes {
739    /// Selections within the total votes.
740    pub selections: Vec<ElectionCountSelection>,
741
742    /// Total number of eligible voters within the reporting unit votes.
743    ///
744    /// This element is called `Cast` within EML_NL, but is renamed here to
745    /// `eligible_voter_count` for clarity. This element was repurposed in
746    /// EML_NL to represent the total number of eligible voters instead of the
747    /// actual number of cast votes.
748    pub eligible_voter_count: StringValue<u64>,
749
750    /// Total number of votes on candidates.
751    ///
752    /// This element is called `TotalCounted` within EML_NL, but is renamed here
753    /// to `candidate_votes_count` for clarity. This element was repurposed in
754    /// EML_NL to represent the total number of votes on candidates instead of
755    /// the actual total number of counted votes.
756    pub candidate_votes_count: StringValue<u64>,
757
758    /// Rejected votes within the reporting unit votes.
759    ///
760    /// Contains blank and invalid votes.
761    pub rejected_votes: BTreeMap<RejectedVotesReason, StringValue<u64>>,
762
763    /// Uncounted votes within the reporting unit votes.
764    pub uncounted_votes: BTreeMap<UncountedVotesReason, StringValue<u64>>,
765}
766
767impl TotalVotes {
768    /// Return the votes of each affiliation with each of their candidates and
769    /// the amount of votes they received.
770    ///
771    /// This errors if any Selections of type ReferendumOption are encountered.
772    pub fn selections_per_affiliation(
773        &self,
774    ) -> Result<Vec<SelectionAffiliationVotes<'_>>, EMLError> {
775        selections_per_affiliation(&self.selections)
776    }
777
778    /// Return the number of valid votes for the given candidate.
779    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    /// Return the number of valid votes for the given affiliation.
788    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    /// Return the total number of blank votes.
796    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    /// Return the total number of invalid votes.
803    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/// Votes per reporting unit.
897#[derive(Debug, Clone)]
898pub struct ReportingUnitVotes {
899    /// Identifier for the reporting unit votes.
900    pub identifier: ReportingUnitIdentifier,
901
902    /// Selections within the reporting unit votes.
903    pub selections: Vec<ElectionCountSelection>,
904
905    /// Total number of eligible voters within the reporting unit votes.
906    ///
907    /// This element is called `Cast` within EML_NL, but is renamed here to
908    /// `eligible_voter_count` for clarity. This element was repurposed in
909    /// EML_NL to represent the total number of eligible voters instead of the
910    /// actual number of cast votes.
911    pub eligible_voter_count: StringValue<u64>,
912
913    /// Total number of votes on candidates.
914    ///
915    /// This element is called `TotalCounted` within EML_NL, but is renamed here
916    /// to `candidate_votes_count` for clarity. This element was repurposed in
917    /// EML_NL to represent the total number of votes on candidates instead of
918    /// the actual total number of counted votes.
919    pub candidate_votes_count: StringValue<u64>,
920
921    /// Rejected votes within the reporting unit votes.
922    ///
923    /// Contains blank and invalid votes.
924    pub rejected_votes: BTreeMap<RejectedVotesReason, StringValue<u64>>,
925
926    /// Uncounted votes within the reporting unit votes.
927    pub uncounted_votes: BTreeMap<UncountedVotesReason, StringValue<u64>>,
928
929    /// Investigations within the reporting unit votes.
930    pub investigations: BTreeMap<InvestigationReason, StringValue<bool>>,
931}
932
933/// Gathered votes and candidates for an affiliation selection within a reporting unit or total votes for a contest.
934pub struct SelectionAffiliationVotes<'a> {
935    /// The affiliation selection for which the votes were gathered.
936    pub affiliation: &'a AffiliationSelection,
937
938    /// The total number of valid votes for this affiliation.
939    pub valid_votes: u64,
940
941    /// The candidates for this affiliation and the number of votes they received.
942    pub candidates: Vec<SelectionCandidateVotes<'a>>,
943}
944
945/// Gathered votes for a candidate selection within some affiliation.
946pub struct SelectionCandidateVotes<'a> {
947    /// The affiliation to which the candidate belongs.
948    pub affiliation: &'a AffiliationSelection,
949
950    /// The candidate selection for which the votes were gathered.
951    pub candidate: &'a CandidateSelection,
952
953    /// The total number of valid votes for this candidate.
954    pub valid_votes: u64,
955}
956
957impl ReportingUnitVotes {
958    /// Create a builder for the [`ReportingUnitVotes`].
959    pub fn builder() -> ReportingUnitVotesBuilder {
960        ReportingUnitVotesBuilder::new()
961    }
962
963    /// Return the votes of each affiliation with each of their candidates and
964    /// the amount of votes they received.
965    ///
966    /// This errors if any Selections of type ReferendumOption are encountered.
967    pub fn selections_per_affiliation(
968        &self,
969    ) -> Result<Vec<SelectionAffiliationVotes<'_>>, EMLError> {
970        selections_per_affiliation(&self.selections)
971    }
972
973    /// Return the number of valid votes for the given candidate.
974    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    /// Return the number of valid votes for the given affiliation.
983    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    /// Return the number of blank votes for this reporting unit.
991    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    /// Return the number of invalid votes for this reporting unit.
998    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    // Selections consist an affiliation selection followed by zero or more
1011    // candidate selections for that affiliation.
1012    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                // Candidate selections should only be encountered after an affiliation selection.
1037                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            // Referendum option selections should not be encountered at all.
1050            ElectionCountSelectionType::ReferendumOption(_) => {
1051                return Err(EMLErrorKind::UnexpectedReferendumOptionSelection.without_span());
1052            }
1053        }
1054    }
1055
1056    // push the last affiliation and its candidates if it exists
1057    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/// A builder for [`ReportingUnitVotes`].
1109#[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    /// Create a new ReportingUnitVotesBuilder for building [`ReportingUnitVotes`] documents.
1122    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    /// Set the identifier for the reporting unit votes.
1135    pub fn identifier(mut self, identifier: impl Into<ReportingUnitIdentifier>) -> Self {
1136        self.identifier = Some(identifier.into());
1137        self
1138    }
1139
1140    /// Set the selections within the reporting unit votes. This overrides any previously set selections.
1141    pub fn selections(mut self, selections: impl Into<Vec<ElectionCountSelection>>) -> Self {
1142        self.selections = selections.into();
1143        self
1144    }
1145
1146    /// Add a selection to the selections within the reporting unit votes.
1147    pub fn push_selection(mut self, selection: impl Into<ElectionCountSelection>) -> Self {
1148        self.selections.push(selection.into());
1149        self
1150    }
1151
1152    /// Set the total number of eligible voters within the reporting unit votes.
1153    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    /// Set the total number of votes on candidates within the reporting unit votes.
1159    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    /// Set the total number of rejected votes within the reporting unit votes for a given reason.
1165    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    /// Set the total number of uncounted votes within the reporting unit votes for a given reason.
1172    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    /// Set the investigations within the reporting unit votes for a given reason.
1179    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    /// Build the [`ReportingUnitVotes`] document, returning an error if any of the required fields are missing.
1186    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/// Reason code for a specific investigation.
1382#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
1383pub enum InvestigationReason {
1384    /// onderzocht vanwege onverklaard verschil
1385    UnexplainedDifference,
1386    /// onderzocht vanwege andere fout
1387    OtherError,
1388    /// uitslag gecorrigeerd
1389    ResultCorrected,
1390    /// toegelaten kiezers opnieuw vastgesteld
1391    AdmittedVotersReestablished,
1392    /// onderzocht vanwege andere reden
1393    OtherReason,
1394    /// stembiljetten deels herteld
1395    PartiallyRecountedBallots,
1396}
1397
1398impl InvestigationReason {
1399    /// Create an InvestigationReason from an EML reason code string.
1400    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    /// Get the EML reason code string for this InvestigationReason.
1418    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/// Error indicating an invalid InvestigationReason.
1433#[derive(Debug, Clone, thiserror::Error)]
1434#[error("Invalid investigation reason: {0}")]
1435pub struct InvalidInvestigationReasonError(String);
1436
1437/// Reason code for a specific uncounted votes entry.
1438#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
1439pub enum UncountedVotesReason {
1440    /// geldige stempassen
1441    ValidPollCards,
1442    /// geldige volmachtbewijzen
1443    ValidProxyCertificates,
1444    /// geldige kiezerspassen
1445    ValidVoterCards,
1446    /// toegelaten kiezers
1447    AdmittedVoters,
1448    /// meer getelde stembiljetten
1449    MoreBallotsCounted,
1450    /// minder getelde stembiljetten
1451    FewerBallotsCounted,
1452    /// meegenomen stembiljetten
1453    BallotsTaken,
1454    /// te weinig uitgereikte stembiljetten
1455    TooFewBallotsIssued,
1456    /// te veel uitgereikte stembiljetten
1457    TooManyBallotsIssued,
1458    /// geen briefstembiljetten
1459    NoPostalBallots,
1460    /// te veel briefstembiljetten
1461    TooManyPostalBallots,
1462    /// kwijtgeraakte stembiljetten
1463    LostBallots,
1464    /// geen verklaring
1465    NoExplanation,
1466    /// andere verklaring
1467    OtherExplanation,
1468}
1469
1470impl UncountedVotesReason {
1471    /// Create an UncountedVotesReason from an EML reason code string.
1472    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    /// Get the EML reason code string for this UncountedVotesReason.
1493    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/// Error indicating an invalid uncounted votes reason.
1514#[derive(Debug, Clone, thiserror::Error)]
1515#[error("Invalid uncounted votes reason: {0}")]
1516pub struct InvalidUncountedVotesReasonError(String);
1517
1518/// Reason code for rejected votes entry.
1519#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
1520pub enum RejectedVotesReason {
1521    /// Blank votes ("blanco")
1522    Blank,
1523    /// Invalid votes ("ongeldig")
1524    Invalid,
1525}
1526
1527impl RejectedVotesReason {
1528    /// Create a RejectedVotesReason from an EML reason code string.
1529    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    /// Get the EML reason code string for this RejectedVotesReason.
1539    pub fn to_eml_value(&self) -> &'static str {
1540        match self {
1541            RejectedVotesReason::Blank => "blanco",
1542            RejectedVotesReason::Invalid => "ongeldig",
1543        }
1544    }
1545}
1546
1547/// Error indicating an invalid rejected votes reason.
1548#[derive(Debug, Clone, thiserror::Error)]
1549#[error("Invalid rejected votes reason: {0}")]
1550pub struct InvalidRejectedVotesReasonError(String);
1551
1552/// A selection within the reporting unit votes.
1553#[derive(Debug, Clone)]
1554pub struct ElectionCountSelection {
1555    /// Type of selection.
1556    pub selection_type: ElectionCountSelectionType,
1557
1558    /// Number of valid votes for this selection.
1559    pub valid_votes: StringValue<u64>,
1560
1561    /// Value of the `Value` attribute, if present.
1562    pub value: Option<Box<str>>,
1563
1564    /// Value of the `Category` attribute, if present.
1565    pub category: Option<Box<str>>,
1566}
1567
1568impl ElectionCountSelection {
1569    /// Create a builder for the [`ElectionCountSelection`].
1570    pub fn builder() -> ElectionCountSelectionBuilder {
1571        ElectionCountSelectionBuilder::new()
1572    }
1573}
1574
1575/// A builder for [`ElectionCountSelection`].
1576#[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    /// Create a new ElectionCountSelectionBuilder for building [`ElectionCountSelection`] documents.
1586    pub fn new() -> Self {
1587        Self {
1588            selection_type: None,
1589            valid_votes: None,
1590            value: None,
1591            category: None,
1592        }
1593    }
1594
1595    /// Set the selection type to a candidate selection with the given candidate.
1596    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    /// Set the selection type to an affiliation selection with the given affiliation.
1604    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    /// Set the selection type to a referendum option selection with the given referendum option.
1612    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    /// Set the number of valid votes for the election count selection.
1623    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    /// Set the Value attribute of the election count selection.
1629    pub fn value(mut self, value: impl Into<Box<str>>) -> Self {
1630        self.value = Some(value.into());
1631        self
1632    }
1633
1634    /// Set the Category attribute of the election count selection.
1635    pub fn category(mut self, category: impl Into<Box<str>>) -> Self {
1636        self.category = Some(category.into());
1637        self
1638    }
1639
1640    /// Build the [`ElectionCountSelection`] document, returning an error if any of the required fields are missing.
1641    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/// The type of selection.
1747#[derive(Debug, Clone)]
1748pub enum ElectionCountSelectionType {
1749    /// Selection of a candidate.
1750    Candidate(Box<CandidateSelection>),
1751
1752    /// Selection of an affiliation.
1753    Affiliation(Box<AffiliationSelection>),
1754
1755    /// Selection of a referendum option.
1756    ReferendumOption(Box<ReferendumOptionSelection>),
1757}
1758
1759impl ElectionCountSelectionType {
1760    /// Check if the selection type is a candidate selection.
1761    pub fn is_candidate(&self) -> bool {
1762        matches!(self, ElectionCountSelectionType::Candidate(_))
1763    }
1764
1765    /// Check if the selection type is an affiliation selection.
1766    pub fn is_affiliation(&self) -> bool {
1767        matches!(self, ElectionCountSelectionType::Affiliation(_))
1768    }
1769
1770    /// Check if the selection type is a referendum option selection.
1771    pub fn is_referendum_option(&self) -> bool {
1772        matches!(self, ElectionCountSelectionType::ReferendumOption(_))
1773    }
1774
1775    /// Get the candidate selection if the selection type is a candidate selection.
1776    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    /// Get the affiliation selection if the selection type is an affiliation selection.
1785    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    /// Get the referendum option selection if the selection type is a referendum option selection.
1794    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/// Selection of a candidate.
1804#[derive(Debug, Clone)]
1805pub struct CandidateSelection {
1806    /// Identifier of the candidate.
1807    pub identifier: CandidateIdentifier,
1808
1809    /// Name of the candidate.
1810    pub name: Option<PersonNameStructure>,
1811
1812    /// Gender of the candidate.
1813    pub gender: Option<StringValue<Gender>>,
1814
1815    /// Qualified address of the candidate, if present.
1816    pub qualifying_address: Option<MinimalQualifyingAddress>,
1817}
1818
1819impl CandidateSelection {
1820    /// Create a new builder for building an instance.
1821    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/// A builder for [`CandidateSelection`].
1844#[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    /// Create a new CandidateSelectionBuilder for building [`CandidateSelection`] instances.
1856    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    /// Set the identifier of the candidate selection.
1868    pub fn identifier(mut self, identifier: impl Into<CandidateIdentifier>) -> Self {
1869        self.identifier = Some(identifier.into());
1870        self
1871    }
1872
1873    /// Set the name of the candidate selection.
1874    pub fn name(mut self, name: impl Into<PersonNameStructure>) -> Self {
1875        self.name = Some(name.into());
1876        self
1877    }
1878
1879    /// Set the gender of the candidate selection.
1880    pub fn gender(mut self, gender: impl Into<Gender>) -> Self {
1881        self.gender = Some(StringValue::from_value(gender.into()));
1882        self
1883    }
1884
1885    /// Set the minimal qualifying address of the candidate selection.
1886    ///
1887    /// You may also set the locality name and country name code separately
1888    /// using the [`Self::locality_name`] and [`Self::country_name_code`] methods.
1889    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    /// Set the locality name for the candidate selection.
1898    ///
1899    /// Has no effect if the qualifying address is already set using the
1900    /// [`Self::qualifying_address`] method.
1901    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    /// Set the country name code for the candidate selection.
1907    ///
1908    /// Has no effect if the qualifying address is already set using the
1909    /// [`Self::qualifying_address`] method.
1910    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    /// Build a CandidateSelection, returning an error if any required properties are missing.
1916    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/// Selection of an affiliation.
1987#[derive(Debug, Clone)]
1988pub struct AffiliationSelection {
1989    /// Id of the affiliation.
1990    pub id: StringValue<AffiliationId>,
1991
1992    /// Name of the affiliation.
1993    pub name: Box<str>,
1994}
1995
1996impl AffiliationSelection {
1997    /// Create a new AffiliationSelection with the given id and name.
1998    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/// Selection of a referendum option.
2028#[derive(Debug, Clone)]
2029pub struct ReferendumOptionSelection {
2030    /// Value of the referendum option.
2031    pub value: Box<str>,
2032
2033    /// Id of the referendum option, if present.
2034    pub id: Option<Box<str>>,
2035
2036    /// Display order of the referendum option, if present.
2037    pub display_order: Option<StringValue<NonZeroU64>>,
2038
2039    /// Short code of the referendum option, if present.
2040    pub short_code: Option<Box<str>>,
2041
2042    /// Expected confirmation reference of the referendum option, if present.
2043    pub expected_confirmation_reference: Option<Box<str>>,
2044}
2045
2046impl ReferendumOptionSelection {
2047    /// Create a new ReferendumOptionSelection with the given value.
2048    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    /// Set the Id attribute of the referendum option.
2059    pub fn with_id(mut self, id: impl Into<Box<str>>) -> Self {
2060        self.id = Some(id.into());
2061        self
2062    }
2063
2064    /// Set the DisplayOrder attribute of the referendum option.
2065    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    /// Set the ShortCode attribute of the referendum option.
2071    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    /// Set the ExpectedConfirmationReference attribute of the referendum option.
2077    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        // check if it still is the same after a second parse and write
2222        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}