use chrono::NaiveDate;
use super::country_patterns::get_country_pattern;
use super::exceptions::MRZError;
use super::field_parser::MRZFieldParser;
use super::field_recognition_defects_fixer::MRZFieldRecognitionDefectsFixer;
use super::result::MRZResult;
use crate::get_check_digit;
pub struct TD1FormatMRZParser;
impl TD1FormatMRZParser {
const LINES_LENGTH: usize = 30;
const LINES_COUNT: usize = 3;
pub fn is_valid_input(input: &[String]) -> bool {
input.len() == Self::LINES_COUNT && input.iter().all(|s| s.len() == Self::LINES_LENGTH)
}
pub fn parse(input: &[String]) -> Result<MRZResult, MRZError> {
if !Self::is_valid_input(input) {
return Err(MRZError::invalid_mrz_input());
}
let first_line = &input[0];
let second_line = &input[1];
let third_line = &input[2];
let document_type_raw = &first_line[0..2];
let country_code_raw = &first_line[2..5];
let document_number_raw: String;
let document_number_check_digit_raw: String;
let optional_data_raw: String;
let is_long_document_number: bool;
if let Some(country_pattern) = get_country_pattern(first_line) {
let start_idx = country_pattern.document_number_start_index;
let substring = &first_line[start_idx..];
if let Some(caps) = country_pattern.document_number_pattern.captures(substring) {
if let Some(m1) = caps.get(1) {
let matched = m1.as_str();
let match_end = start_idx + caps.get(0).unwrap().end();
let validates =
|doc: &str, cd: &str| cd.parse::<u8>().ok() == Some(get_check_digit(doc));
let full_cd = (match_end < Self::LINES_LENGTH)
.then(|| &first_line[match_end..match_end + 1]);
if let Some(cd) = full_cd.filter(|cd| validates(matched, cd)) {
document_number_raw = matched.to_string();
document_number_check_digit_raw = cd.to_string();
optional_data_raw =
first_line[match_end + 1..Self::LINES_LENGTH].to_string();
} else if matched.len() >= 2
&& validates(&matched[..matched.len() - 1], &matched[matched.len() - 1..])
{
document_number_raw = matched[..matched.len() - 1].to_string();
document_number_check_digit_raw = matched[matched.len() - 1..].to_string();
optional_data_raw = first_line[match_end..Self::LINES_LENGTH].to_string();
} else if let Some(cd) = full_cd {
document_number_raw = matched.to_string();
document_number_check_digit_raw = cd.to_string();
optional_data_raw =
first_line[match_end + 1..Self::LINES_LENGTH].to_string();
} else {
return Err(MRZError::invalid_document_number());
}
is_long_document_number = document_number_raw.len() > 9;
} else {
return Err(MRZError::invalid_document_number());
}
} else {
return Err(MRZError::invalid_document_number());
}
} else {
if &first_line[14..15] == "<" {
let tmp_string = first_line[15..28].trim_end_matches('<').to_string();
if tmp_string.is_empty() {
return Err(MRZError::invalid_document_number());
}
document_number_check_digit_raw = tmp_string[tmp_string.len() - 1..].to_string();
let part1 = &first_line[5..14];
let part2 = &tmp_string[0..tmp_string.len() - 1];
document_number_raw = format!("{}{}", part1, part2);
optional_data_raw = first_line[15 + tmp_string.len()..30].to_string();
is_long_document_number = true;
} else {
document_number_raw = first_line[5..14].to_string();
document_number_check_digit_raw = first_line[14..15].to_string();
optional_data_raw = first_line[15..30].to_string();
is_long_document_number = false;
}
}
let birth_date_raw = &second_line[0..6];
let birth_date_check_digit_raw = &second_line[6..7];
let sex_raw = &second_line[7..8];
let expiry_date_raw = &second_line[8..14];
let expiry_date_check_digit_raw = &second_line[14..15];
let nationality_raw = &second_line[15..18];
let optional_data2_raw = &second_line[18..29];
let final_check_digit_raw = &second_line[29..30];
let document_type_fixed =
MRZFieldRecognitionDefectsFixer::fix_document_type(document_type_raw);
let country_code_fixed =
MRZFieldRecognitionDefectsFixer::fix_country_code(country_code_raw);
let document_number_fixed = document_number_raw.clone();
let document_number_check_digit_fixed =
MRZFieldRecognitionDefectsFixer::fix_check_digit(&document_number_check_digit_raw);
let optional_data_fixed = optional_data_raw.clone();
let birth_date_fixed = MRZFieldRecognitionDefectsFixer::fix_date(birth_date_raw);
let birth_date_check_digit_fixed =
MRZFieldRecognitionDefectsFixer::fix_check_digit(birth_date_check_digit_raw);
let sex_fixed = MRZFieldRecognitionDefectsFixer::fix_sex(sex_raw);
let expiry_date_fixed = MRZFieldRecognitionDefectsFixer::fix_date(expiry_date_raw);
let expiry_date_check_digit_fixed =
MRZFieldRecognitionDefectsFixer::fix_check_digit(expiry_date_check_digit_raw);
let nationality_fixed = MRZFieldRecognitionDefectsFixer::fix_nationality(nationality_raw);
let optional_data2_fixed = optional_data2_raw.to_string();
let final_check_digit_fixed =
MRZFieldRecognitionDefectsFixer::fix_check_digit(final_check_digit_raw);
let doc_check_digit_parsed = document_number_check_digit_fixed.parse::<u8>().ok();
let doc_calc = get_check_digit(&document_number_fixed);
if doc_check_digit_parsed != Some(doc_calc) {
return Err(MRZError::invalid_document_number());
}
let birth_check_digit_parsed = birth_date_check_digit_fixed.parse::<u8>().ok();
let birth_calc = get_check_digit(&birth_date_fixed);
if birth_check_digit_parsed != Some(birth_calc) {
return Err(MRZError::invalid_birth_date());
}
let expiry_check_digit_parsed = expiry_date_check_digit_fixed.parse::<u8>().ok();
let expiry_calc = get_check_digit(&expiry_date_fixed);
if expiry_check_digit_parsed != Some(expiry_calc) {
return Err(MRZError::invalid_expiry_date());
}
let document_number_fixed_for_check_string = if is_long_document_number {
let first9 = &document_number_fixed[0..9];
let rest = &document_number_fixed[9..];
format!("{}<{}", first9, rest)
} else {
document_number_fixed.clone()
};
let final_check_string_fixed = format!(
"{}{}{}{}{}{}{}{}",
document_number_fixed_for_check_string,
document_number_check_digit_fixed,
optional_data_fixed,
birth_date_fixed,
birth_date_check_digit_fixed,
expiry_date_fixed,
expiry_date_check_digit_fixed,
optional_data2_fixed
);
let final_check_digit_parsed = final_check_digit_fixed.parse::<u8>().ok();
let final_calc = get_check_digit(&final_check_string_fixed) as u8;
if final_check_digit_parsed != Some(final_calc) {
return Err(MRZError::invalid_mrz_value());
}
let document_type = MRZFieldParser::parse_document_type(&document_type_fixed);
let country_code = MRZFieldParser::parse_country_code(&country_code_fixed);
let document_number = MRZFieldParser::parse_document_number(&document_number_fixed);
let optional_data = MRZFieldParser::parse_optional_data(&optional_data_fixed);
let birth_date: NaiveDate = MRZFieldParser::parse_birth_date(&birth_date_fixed)?;
let sex = MRZFieldParser::parse_sex(&sex_fixed);
let expiry_date: NaiveDate = MRZFieldParser::parse_expiry_date(&expiry_date_fixed)?;
let nationality = MRZFieldParser::parse_nationality(&nationality_fixed);
let optional_data2 = MRZFieldParser::parse_optional_data(&optional_data2_fixed);
let names = MRZFieldParser::parse_names(&third_line[0..30]);
let surnames = names.first().cloned().unwrap_or_default();
let given_names = names.get(1).cloned().unwrap_or_default();
Ok(MRZResult::new(
document_type,
country_code,
surnames,
given_names,
document_number,
nationality,
birth_date,
sex,
expiry_date,
optional_data,
Some(optional_data2),
))
}
}
pub fn is_valid_input(input: &[String]) -> bool {
TD1FormatMRZParser::is_valid_input(input)
}
pub fn parse(input: &[String]) -> Result<MRZResult, MRZError> {
TD1FormatMRZParser::parse(input)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Datelike;
#[test]
fn validate_is_valid_input() {
let lines = vec![
"I<UTOD231458907<<<<<<<<<<<<<<<".to_string(),
"7408122F1204159UTO<<<<<<<<<<<6".to_string(),
"ERIKSSON<<ANNA<MARIA<<<<<<<<<<".to_string(),
];
assert!(TD1FormatMRZParser::is_valid_input(&lines));
}
#[test]
fn parse_sample_td1() {
let lines = vec![
"I<UTOD231458907<<<<<<<<<<<<<<<".to_string(),
"7408122F1204159UTO<<<<<<<<<<<6".to_string(),
"ERIKSSON<<ANNA<MARIA<<<<<<<<<<".to_string(),
];
let res = TD1FormatMRZParser::parse(&lines).expect("should parse");
assert_eq!(res.document_type, "I");
assert_eq!(res.country_code, "UTO");
assert_eq!(res.surnames, "ERIKSSON");
assert!(res.given_names.contains("ANNA"));
assert_eq!(res.birth_date.year(), 1974);
assert_eq!(res.birth_date.month(), 8);
assert_eq!(res.birth_date.day(), 12);
}
#[test]
fn composite_check_digit_includes_optional_data2() {
let lines = vec![
"I<UTOD231458907<<<<<<<<<<<<<<<".to_string(),
"7408122F1204159UTOABC123456789".to_string(),
"ERIKSSON<<ANNA<MARIA<<<<<<<<<<".to_string(),
];
let res = TD1FormatMRZParser::parse(&lines).expect("composite must validate");
assert_eq!(res.personal_number2.as_deref(), Some("ABC12345678"));
let bad = vec![
"I<UTOD231458907<<<<<<<<<<<<<<<".to_string(),
"7408122F1204159UTOXBC123456789".to_string(),
"ERIKSSON<<ANNA<MARIA<<<<<<<<<<".to_string(),
];
assert!(TD1FormatMRZParser::parse(&bad).is_err());
}
}