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}