Skip to main content

mrz_parser/
string_extensions.rs

1//! Utilities for MRZ-specific string operations: validation, trimming a
2//! specific character, and OCR-friendly character replacements commonly used
3//! when processing MRZ zones.
4//!
5//! These functions are intentionally simple and avoid heap allocations where
6//! possible, but return owned `String` when a transformed string is required.
7
8/// Return `true` if `s` contains only valid MRZ characters:
9/// uppercase A-Z, digits 0-9, or the filler '<'.
10pub fn is_valid_mrz_input(s: &str) -> bool {
11    s.chars()
12        .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '<')
13}
14
15/// Trim leading and trailing occurrences of the single character `ch` from
16/// `input`. Returns an owned `String`.
17///
18/// Example:
19/// - `trim_char("<<ABC<<", '<') -> "ABC"`
20pub fn trim_char(input: &str, ch: char) -> String {
21    input.trim_matches(ch).to_string()
22}
23
24/// Replace digits that commonly get misrecognized as letters by OCR:
25/// - '0' -> 'O'
26/// - '1' -> 'I'
27/// - '2' -> 'Z'
28/// - '8' -> 'B'
29pub fn replace_similar_digits_with_letters(input: &str) -> String {
30    // Small transformation — build a new String
31    input
32        .chars()
33        .map(|c| match c {
34            '0' => 'O',
35            '1' => 'I',
36            '2' => 'Z',
37            '8' => 'B',
38            other => other,
39        })
40        .collect()
41}
42
43/// Replace letters that commonly get misrecognized as digits by OCR:
44/// - 'O','o','Q','q','U','u','D','d' -> '0'
45/// - 'I','i' -> '1'
46/// - 'Z','z' -> '2'
47/// - 'B','b' -> '8'
48pub fn replace_similar_letters_with_digits(input: &str) -> String {
49    input
50        .chars()
51        .map(|c| match c {
52            'O' | 'o' | 'Q' | 'q' | 'U' | 'u' | 'D' | 'd' => '0',
53            'I' | 'i' => '1',
54            'Z' | 'z' => '2',
55            'B' | 'b' => '8',
56            other => other,
57        })
58        .collect()
59}
60
61/// Replace MRZ angle brackets '<' with spaces.
62pub fn replace_angle_brackets_with_spaces(input: &str) -> String {
63    // Use simple replace; MRZ fields are small.
64    if input.contains('<') {
65        input.replace('<', " ")
66    } else {
67        input.to_string()
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn valid_mrz_inputs() {
77        assert!(is_valid_mrz_input("ABCDE<12345"));
78        assert!(is_valid_mrz_input("<<<<<<<<"));
79        assert!(!is_valid_mrz_input("lowercase"));
80        assert!(!is_valid_mrz_input("HAS SPACE"));
81        assert!(!is_valid_mrz_input("!@#"));
82    }
83
84    #[test]
85    fn trim_char_basic() {
86        assert_eq!(trim_char("<<ABC<<", '<'), "ABC");
87        assert_eq!(trim_char("ABC", '<'), "ABC");
88        assert_eq!(trim_char("<<<<", '<'), "");
89        assert_eq!(trim_char("", '<'), "");
90    }
91
92    #[test]
93    fn replace_digits_with_letters() {
94        assert_eq!(
95            replace_similar_digits_with_letters("0 1 2 8 ABC"),
96            "O I Z B ABC"
97        );
98        // `3` has no look-alike letter, so it passes through where `1` and `2` do not.
99        assert_eq!(replace_similar_digits_with_letters("123"), "IZ3");
100    }
101
102    #[test]
103    fn replace_letters_with_digits() {
104        // O→0, Q→0, U→0, I→1, D→0, Z→2, B→8 (both cases)
105        assert_eq!(
106            replace_similar_letters_with_digits("OQUIDZB oquidzb"),
107            "0001028 0001028"
108        );
109        // 'B' maps to '8', so "ABC123" => "A8C123"
110        assert_eq!(replace_similar_letters_with_digits("ABC123"), "A8C123");
111    }
112
113    #[test]
114    fn replace_angle_brackets() {
115        assert_eq!(replace_angle_brackets_with_spaces("A<B<C"), "A B C");
116        assert_eq!(replace_angle_brackets_with_spaces("NOANGLE"), "NOANGLE");
117    }
118}