use crate::exceptions::MRZError;
use crate::string_extensions::replace_angle_brackets_with_spaces;
use crate::Sex;
use chrono::{Datelike, Local, NaiveDate};
pub struct MRZFieldParser;
impl MRZFieldParser {
pub fn parse_document_number(input: &str) -> String {
trim(input)
}
pub fn parse_document_type(input: &str) -> String {
trim(input)
}
pub fn parse_country_code(input: &str) -> String {
trim(input)
}
pub fn parse_nationality(input: &str) -> String {
trim(input)
}
pub fn parse_optional_data(input: &str) -> String {
trim(input)
}
pub fn parse_names(input: &str) -> Vec<String> {
let trimmed = input.trim_end_matches('<');
let mut parts = trimmed.splitn(2, "<<");
let surname = trim(parts.next().unwrap_or(""));
let given_names = trim(parts.next().unwrap_or(""));
vec![surname, given_names]
}
pub fn parse_birth_date(input: &str) -> Result<NaiveDate, MRZError> {
let formatted = _format_date(input);
let milestone = (Local::now().year() - 2000).max(0); _parse_date(&formatted, milestone)
}
pub fn parse_expiry_date(input: &str) -> Result<NaiveDate, MRZError> {
let formatted = _format_date(input);
_parse_date(&formatted, 70)
}
pub fn parse_sex(input: &str) -> Sex {
match input.trim() {
"M" => Sex::Male,
"F" => Sex::Female,
_ => Sex::Unspecified,
}
}
}
fn trim(input: &str) -> String {
replace_angle_brackets_with_spaces(input).trim().to_string()
}
fn _format_date(input: &str) -> String {
trim(input)
}
fn _parse_date(input: &str, milestone_year: i32) -> Result<NaiveDate, MRZError> {
let s = input.trim();
if s.len() < 6 {
return Err(MRZError::invalid_mrz_input());
}
let yy_str = s.get(0..2).ok_or_else(MRZError::invalid_mrz_input)?;
let rest = s.get(2..).ok_or_else(MRZError::invalid_mrz_input)?;
let parsed_year = yy_str
.parse::<i32>()
.map_err(|_| MRZError::invalid_mrz_input())?;
let century = if parsed_year > milestone_year {
1900
} else {
2000
};
let full_year = century + parsed_year;
let full_date_str = format!("{:04}{}", full_year, rest);
NaiveDate::parse_from_str(&full_date_str, "%Y%m%d").map_err(|_| MRZError::invalid_mrz_input())
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
#[test]
fn test_trim_and_replace() {
use crate::string_extensions::trim_char;
assert_eq!(trim("ABC<<DEF"), "ABC DEF");
assert_eq!(trim("<<HELLO<<"), "HELLO");
assert_eq!(trim_char("<<ABC<<", '<'), "ABC");
assert_eq!(trim_char("ABC", '<'), "ABC");
}
#[test]
fn test_parse_names() {
let v = MRZFieldParser::parse_names("DOE<<JOHN<PAUL");
assert_eq!(v.len(), 2);
assert_eq!(v[0], "DOE"); assert_eq!(v[1], "JOHN PAUL"); }
#[test]
fn test_parse_names_empty_surname_preserved() {
let v = MRZFieldParser::parse_names("<<JOHN<PAUL<<<<<<");
assert_eq!(v.len(), 2);
assert_eq!(v[0], ""); assert_eq!(v[1], "JOHN PAUL");
}
#[test]
fn test_parse_date_rejects_non_char_boundary() {
assert!(_parse_date("€0101", 70).is_err());
}
#[test]
fn test_parse_sex() {
assert_eq!(MRZFieldParser::parse_sex("M"), Sex::Male);
assert_eq!(MRZFieldParser::parse_sex("F"), Sex::Female);
assert_eq!(MRZFieldParser::parse_sex("<"), Sex::Unspecified);
assert_eq!(MRZFieldParser::parse_sex("X"), Sex::Unspecified);
}
#[test]
fn test_parse_dates_birth_and_expiry() {
let d = _parse_date("900101", 26).expect("should parse as 1990");
assert_eq!(d, NaiveDate::from_ymd_opt(1990, 1, 1).unwrap());
let d2 = _parse_date("050505", 26).expect("should parse as 2005");
assert_eq!(d2, NaiveDate::from_ymd_opt(2005, 5, 5).unwrap());
let e1 = _parse_date("710101", 70).expect("1971");
assert_eq!(e1, NaiveDate::from_ymd_opt(1971, 1, 1).unwrap());
let e2 = _parse_date("690101", 70).expect("2069");
assert_eq!(e2, NaiveDate::from_ymd_opt(2069, 1, 1).unwrap());
}
#[test]
fn test_parse_document_fields() {
assert_eq!(MRZFieldParser::parse_document_number("ABC<123"), "ABC 123");
assert_eq!(MRZFieldParser::parse_document_type("P<"), "P");
assert_eq!(MRZFieldParser::parse_country_code("UTO"), "UTO");
}
}