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}