Skip to main content

mrz_parser/
checkdigit_calculator.rs

1//!
2//! The original implementation used a static class with a private constructor and
3//! a static method `getCheckDigit(String input)`. Here we provide a simple
4//! function `get_check_digit` that computes the MRZ check digit for an input
5//! string using weights [7, 3, 1] and the standard MRZ character mapping:
6//! - 'A'..'Z' -> 10..35
7//! - '0'..'9' -> 0..9
8//! - anything else -> 0
9//!
10//! Returns the check digit as `u8` in range 0..=9.
11const WEIGHTS: [usize; 3] = [7, 3, 1];
12
13/// Compute the MRZ check digit for `input`.
14///
15/// The algorithm:
16/// 1. For each character, map it to a numeric value:
17///    - 'A'..'Z' -> 10 + (ch - 'A')
18///    - '0'..'9' -> ch - '0'
19///    - otherwise -> 0
20/// 2. Multiply each value by the repeating weights [7,3,1] at its index.
21/// 3. Sum the products and return (sum % 10) as the check digit.
22///
23/// Example:
24/// ```
25/// assert_eq!(mrz_parser::get_check_digit("L898902C3"), 6);
26/// ```
27pub fn get_check_digit(input: &str) -> u8 {
28    // Weight by character position, not UTF-8 byte position: a multi-byte
29    // character must occupy a single MRZ position. Non-ASCII (and any character
30    // outside `A-Z`/`0-9`) contributes a value of 0, mirroring the filler `<`.
31    let sum: usize = input
32        .chars()
33        .enumerate()
34        .map(|(i, c)| {
35            let value = if c.is_ascii_uppercase() {
36                // 'A' => 10
37                (c as usize - 'A' as usize) + 10
38            } else if c.is_ascii_digit() {
39                c as usize - '0' as usize
40            } else {
41                0
42            };
43            value * WEIGHTS[i % WEIGHTS.len()]
44        })
45        .sum();
46
47    (sum % 10) as u8
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_letters_and_digits() {
56        // From examples commonly used in MRZ docs:
57        // For "L898902C3" the computed check digit should be 6.
58        assert_eq!(get_check_digit("L898902C3"), 6);
59
60        // Single digits: value × weight[0] (7) % 10
61        // "0" => 0*7=0 => 0
62        // "1" => 1*7=7 => 7
63        // "9" => 9*7=63 => 3
64        assert_eq!(get_check_digit("0"), 0);
65        assert_eq!(get_check_digit("1"), 7);
66        assert_eq!(get_check_digit("9"), 3);
67
68        // Letters mapping: 'A' -> 10, weight 7 -> 70 -> 0 (70 % 10 == 0)
69        assert_eq!(get_check_digit("A"), 0);
70
71        // Mixed example
72        // Compute manually for verification:
73        // characters: 'M' (22), 'R' (27), 'Z' (35), '1' (1), '2' (2)
74        // weights:   7, 3, 1, 7, 3
75        // products: 154, 81, 35, 7, 6 => sum = 283 => 283 % 10 = 3
76        assert_eq!(get_check_digit("MRZ12"), 3);
77    }
78
79    #[test]
80    fn test_non_mrz_chars_treated_as_zero() {
81        // Characters outside A-Z and 0-9 are treated as 0 (e.g. '<')
82        // Example: "<" => 0 -> weighted 7 -> 0 mod 10
83        assert_eq!(get_check_digit("<"), 0);
84
85        // Mix of valid and fillers
86        assert_eq!(get_check_digit("ABC<123"), {
87            // Manual verification:
88            // 'A'(10)*7 = 70
89            // 'B'(11)*3 = 33
90            // 'C'(12)*1 = 12
91            // '<'(0)*7 = 0
92            // '1'(1)*3 = 3
93            // '2'(2)*1 = 2
94            // '3'(3)*7 = 21
95            // sum = 70+33+12+0+3+2+21 = 141 => 141 % 10 = 1
96            1
97        });
98    }
99
100    #[test]
101    fn test_long_input() {
102        let s = "L898902C360X"; // arbitrary sequence
103                                // Ensure function runs and returns a digit
104        let d = get_check_digit(s);
105        assert!(d <= 9);
106    }
107}