Skip to main content

mrz_parser/
result.rs

1use chrono::NaiveDate;
2use std::fmt;
3
4use crate::Sex;
5
6/// Parsed contents of an MRZ (Machine Readable Zone) — the values typically
7/// extracted from passport / ID MRZ lines.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct MRZResult {
10    pub document_type: String,
11    pub country_code: String,
12    pub surnames: String,
13    pub given_names: String,
14    pub document_number: String,
15    pub nationality_country_code: String,
16    pub birth_date: NaiveDate,
17    pub sex: Sex,
18    pub expiry_date: NaiveDate,
19    pub personal_number: String,
20    pub personal_number2: Option<String>,
21}
22
23impl MRZResult {
24    /// Create a new `MRZResult`.
25    ///
26    /// `birth_date` and `expiry_date` are `NaiveDate` — date-only values are
27    /// all MRZ carries for these fields.
28    // The count comes from the document, not from the design: these are the fields a
29    // machine-readable zone carries. Grouping them behind a builder would add a layer
30    // whose only purpose is to make the number smaller.
31    #[allow(clippy::too_many_arguments)]
32    pub fn new(
33        document_type: impl Into<String>,
34        country_code: impl Into<String>,
35        surnames: impl Into<String>,
36        given_names: impl Into<String>,
37        document_number: impl Into<String>,
38        nationality_country_code: impl Into<String>,
39        birth_date: NaiveDate,
40        sex: Sex,
41        expiry_date: NaiveDate,
42        personal_number: impl Into<String>,
43        personal_number2: Option<impl Into<String>>,
44    ) -> Self {
45        MRZResult {
46            document_type: document_type.into(),
47            country_code: country_code.into(),
48            surnames: surnames.into(),
49            given_names: given_names.into(),
50            document_number: document_number.into(),
51            nationality_country_code: nationality_country_code.into(),
52            birth_date,
53            sex,
54            expiry_date,
55            personal_number: personal_number.into(),
56            personal_number2: personal_number2.map(|s| s.into()),
57        }
58    }
59}
60
61impl fmt::Display for MRZResult {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(
64            f,
65            "MRZResult {{ document_type: {}, country_code: {}, surnames: {}, given_names: {}, document_number: {}, nationality: {}, birth_date: {}, sex: {:?}, expiry_date: {}, personal_number: {}, personal_number2: {:?} }}",
66            self.document_type,
67            self.country_code,
68            self.surnames,
69            self.given_names,
70            self.document_number,
71            self.nationality_country_code,
72            self.birth_date,
73            self.sex,
74            self.expiry_date,
75            self.personal_number,
76            self.personal_number2,
77        )
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::Sex;
85    use chrono::NaiveDate;
86
87    #[test]
88    fn create_and_compare_results() {
89        let birth = NaiveDate::from_ymd_opt(1990, 5, 20).expect("valid date");
90        let expiry = NaiveDate::from_ymd_opt(2030, 5, 20).expect("valid date");
91
92        let r1 = MRZResult::new(
93            "P",
94            "UTO",
95            "DOE",
96            "JOHN",
97            "123456789",
98            "UTO",
99            birth,
100            Sex::Male,
101            expiry,
102            "ABC123<<<",
103            Some("SECOND".to_string()),
104        );
105
106        let r2 = MRZResult {
107            document_type: "P".into(),
108            country_code: "UTO".into(),
109            surnames: "DOE".into(),
110            given_names: "JOHN".into(),
111            document_number: "123456789".into(),
112            nationality_country_code: "UTO".into(),
113            birth_date: birth,
114            sex: Sex::Male,
115            expiry_date: expiry,
116            personal_number: "ABC123<<<".into(),
117            personal_number2: Some("SECOND".into()),
118        };
119
120        assert_eq!(r1, r2);
121        assert!(r1.personal_number2.is_some());
122    }
123
124    #[test]
125    fn display_contains_key_fields() {
126        let birth = NaiveDate::from_ymd_opt(1985, 1, 1).unwrap();
127        let expiry = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
128
129        let r = MRZResult::new(
130            "ID",
131            "GBR",
132            "SMITH",
133            "JANE",
134            "X9876543",
135            "GBR",
136            birth,
137            Sex::Female,
138            expiry,
139            "ZZZ000<<<",
140            None::<String>,
141        );
142
143        let s = format!("{}", r);
144        assert!(s.contains("SMITH"));
145        assert!(s.contains("JANE"));
146        assert!(s.contains("GBR"));
147    }
148}