codes_check_digits/
luhn.rs

1/*!
2The type [CheckDigitAlgorithm] provides an implementation of the
3[Luhn Algorithm](https://en.wikipedia.org/wiki/Luhn_algorithm).
4
5# Example
6
7```rust
8use codes_check_digits::{luhn, Calculator};
9
10let calculator = luhn::get_algorithm_instance();
11assert!(calculator.is_valid("US0378331005"));
12assert!(calculator.validate("US0378331005").is_ok());
13assert_eq!(calculator.calculate("US037833100"), Ok(5));
14```
15
16*/
17
18use crate::{common::is_ascii_alphanumeric_upper, error::CheckDigitError, Calculator};
19
20// ------------------------------------------------------------------------------------------------
21// Public Types
22// ------------------------------------------------------------------------------------------------
23///
24/// This algorithm is known by many names: the "Luhn Formula", "The IBM Check",
25/// "Mod 10", and is officially specified in "Annex B to ISO/IEC 7812, Part 1"
26/// and in "ANSI X4.13". It is used as the check scheme on credit cards such
27/// as Visa, Master Card, and American Express.
28///
29/// # Issues
30///
31/// It catches all single digit errors, but does not catch transposition errors
32/// with "0" and "9" (meaning "09" → "90" and "90" → "09" are not caught).
33///
34#[derive(Clone, Copy, Debug, Default)]
35pub struct CheckDigitAlgorithm {}
36
37// ------------------------------------------------------------------------------------------------
38// Public Functions
39// ------------------------------------------------------------------------------------------------
40
41const SHARED_INSTANCE: CheckDigitAlgorithm = CheckDigitAlgorithm {};
42
43pub const fn get_algorithm_instance() -> &'static CheckDigitAlgorithm {
44    &SHARED_INSTANCE
45}
46
47// ------------------------------------------------------------------------------------------------
48// Implementations
49// ------------------------------------------------------------------------------------------------
50
51impl Calculator<u8> for CheckDigitAlgorithm {
52    fn name(&self) -> &'static str {
53        "Luhn Algorithm (ISO/IEC 7812, Part 1, Annex B)"
54    }
55
56    fn calculate(&self, s: &str) -> Result<u8, CheckDigitError> {
57        is_ascii_alphanumeric_upper(s)?;
58        let sum: u8 = s
59            .chars()
60            .flat_map(luhn_alphanum_to_vec)
61            .rev()
62            .enumerate()
63            .map(|(i, n)| luhn_double_odd(i & 1 == 1, n))
64            .sum();
65
66        Ok((10 - (sum % 10)) % 10)
67    }
68}
69
70// ------------------------------------------------------------------------------------------------
71// Private Functions
72// ------------------------------------------------------------------------------------------------
73
74#[inline(always)]
75fn luhn_double_odd(is_odd: bool, d: u8) -> u8 {
76    if is_odd {
77        d
78    } else {
79        match d {
80            0..=4 => d * 2,
81            5 => 1,
82            6 => 3,
83            7 => 5,
84            8 => 7,
85            9 => 9,
86            _ => panic!(),
87        }
88    }
89}
90
91#[inline(always)]
92fn luhn_alphanum_to_vec(c: char) -> Vec<u8> {
93    let d = c as u8;
94    match d {
95        b'0'..=b'9' => vec![d - b'0'],
96        b'A'..=b'Z' => {
97            let d = d - 55;
98            vec![d / 10, d % 10]
99        }
100        _ => panic!(),
101    }
102}
103
104// ------------------------------------------------------------------------------------------------
105// Unit Tests
106// ------------------------------------------------------------------------------------------------
107
108#[cfg(test)]
109mod tests {
110    use crate::luhn::CheckDigitAlgorithm;
111    use crate::Calculator;
112
113    #[test]
114    fn test_check_digits() {
115        let calculator: CheckDigitAlgorithm = Default::default();
116        assert!(calculator.validate("US0378331005").is_ok());
117        assert!(calculator.validate("037833100").is_ok());
118    }
119}