#![doc = include_str!("../README.md")]
#![cfg_attr(not(test), no_std)]
#[derive(Default)]
pub struct Mixer(Blob, Blob);
impl Mixer {
#[inline(always)]
pub fn push(&mut self, digit: u8) {
debug_assert!(digit < 10);
if digit >= 5 {
self.0.five_or_higher += 1;
}
self.0.sum += usize::from(digit);
let t = self.0;
self.0 = self.1;
self.1 = t;
}
pub fn valid(&self) -> bool {
(self.0.sum * 2 - self.0.five_or_higher * 9 + self.1.sum) % 10 == 0
}
pub fn checksum(&self) -> u8 {
let checksum = self.1.sum * 2 - self.1.five_or_higher * 9 + self.0.sum;
b'0' + ((10 - (checksum % 10)) % 10) as u8
}
}
#[derive(Default, Copy, Clone)]
struct Blob {
sum: usize,
five_or_higher: usize,
}
fn fold_decimal(raw: &[u8]) -> Option<Mixer> {
let mut mixer = Mixer::default();
for c in raw.iter().copied() {
match c {
b'0'..=b'9' => mixer.push(c - b'0'),
_ => return None,
}
}
Some(mixer)
}
fn fold_base36(raw: &[u8]) -> Option<Mixer> {
let mut mixer = Mixer::default();
for c in raw.iter().copied() {
match c {
b'0'..=b'9' => mixer.push(c - b'0'),
b'A'..=b'Z' => {
let b36 = c - b'A' + 10;
mixer.push(b36 / 10);
mixer.push(b36 % 10);
}
_ => return None,
}
}
Some(mixer)
}
pub mod decimal {
use crate::*;
pub fn valid(ascii: &[u8]) -> bool {
match fold_decimal(ascii) {
Some(mixer) => mixer.valid(),
None => false,
}
}
pub fn checksum(ascii: &[u8]) -> Option<u8> {
Some(fold_decimal(ascii)?.checksum())
}
}
pub mod alphanum {
use crate::*;
pub fn valid(ascii: &[u8]) -> bool {
match fold_base36(ascii) {
Some(mixer) => mixer.valid(),
None => false,
}
}
pub fn checksum(ascii: &[u8]) -> Option<u8> {
Some(fold_base36(ascii)?.checksum())
}
}
pub use crate::alphanum::*;
#[cfg(test)]
mod test {
const DECIMAL_LUHN_SAMPLES: &'static [&str] = &[
"378282246310005", "371449635398431", "378734493671000", "5610591081018250", "30569309025904", "38520000023237", "6011111111111117", "6011000990139424", "3530111333300000", "3566002020360505", "5555555555554444", "5105105105105100", "4111111111111111", "4012888888881881", "4222222222222", "5019717010103742", "6331101999990016", "358771054102508", "867103029110602", "358625057927511", "359513063006075", "351813076684290", ];
fn change_digit(digit: u8) -> u8 {
if digit == b'9' {
b'0'
} else {
digit + 1
}
}
#[test]
fn test_ae_checksum() {
let (&check, body) = b"378282246310005".split_last().unwrap();
assert_eq!(Some(check), crate::decimal::checksum(body));
}
#[test]
fn test_decimal_luhn_checksum() {
for sample in DECIMAL_LUHN_SAMPLES {
assert!(crate::decimal::valid(sample.as_bytes()));
let mut s = Vec::from(*sample);
s[3] = change_digit(s[3]);
assert!(!crate::decimal::valid(&s));
let mut s = Vec::from(*sample);
if s[3] != s[4] {
s.swap(3, 4);
assert!(!crate::decimal::valid(&s));
}
let (checksum, body) = sample.as_bytes().split_last().unwrap();
assert_eq!(Some(*checksum), crate::decimal::checksum(body));
let mut s = Vec::from(*sample);
s[3] = b'x';
assert!(!crate::decimal::valid(&s));
}
}
const ALPHANUM_LUHN_SAMPLES: &'static [&str] = &[
"US5949181045", "US38259P5089", "US0378331005", "BMG491BT1088", "IE00B4BNMY34", "US0231351067", "US64110L1061", "US30303M1027", "CH0031240127", "CA9861913023", "KR4101R60000",
"KR4201QB2551",
"KR4201RC3102",
"KR4201Q92623",
"KR4205QB2904",
"KR4301R12825",
"KR4301QC2906",
"KR4205Q92327",
"KR4301QB3228",
"KR4301Q93579",
];
#[test]
fn test_alphanum_luhn_samples() {
for sample in ALPHANUM_LUHN_SAMPLES {
assert!(crate::alphanum::valid(sample.as_bytes()));
let mut s = Vec::from(*sample);
s[3] = change_digit(s[3]);
assert!(!crate::alphanum::valid(&s));
let (checksum, body) = sample.as_bytes().split_last().unwrap();
assert_eq!(Some(*checksum), crate::alphanum::checksum(body));
let mut s = Vec::from(*sample);
s[3] = b'x';
assert!(!crate::alphanum::valid(&s));
}
}
}