#![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);
core::mem::swap(&mut self.0, &mut self.1);
}
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,
}
#[inline(always)]
fn copy_from_small_slice(buf: &mut [u8; 8], c: &[u8]) {
match c.len() {
8 => *buf = <[u8; 8]>::try_from(c).unwrap(),
7 => buf[1..].copy_from_slice(&<[u8; 7]>::try_from(c).unwrap()),
6 => buf[2..].copy_from_slice(&<[u8; 6]>::try_from(c).unwrap()),
5 => buf[3..].copy_from_slice(&<[u8; 5]>::try_from(c).unwrap()),
4 => buf[4..].copy_from_slice(&<[u8; 4]>::try_from(c).unwrap()),
3 => buf[5..].copy_from_slice(&<[u8; 3]>::try_from(c).unwrap()),
2 => buf[6..].copy_from_slice(&<[u8; 2]>::try_from(c).unwrap()),
1 => buf[7..].copy_from_slice(&<[u8; 1]>::try_from(c).unwrap()),
_ => unreachable!(),
}
}
#[inline(always)]
fn fold10_swar(mask1: u64, mask2: u64, raw: &[u8]) -> Option<u64> {
let mut sum = 0;
for c in raw.rchunks(8) {
let mut buf = [b'0'; 8];
copy_from_small_slice(&mut buf, c);
let mut v = u64::from_le_bytes(buf);
let a = v.wrapping_add(0x4646464646464646);
v = v.wrapping_sub(0x3030303030303030);
if (a | v) & 0x8080808080808080 == 0 {
sum += u64::from((mask2.wrapping_sub(v) & 0x8080808080808080).count_ones());
sum += v.wrapping_mul(mask1) >> 56;
} else {
return None;
}
}
Some(sum)
}
#[inline(always)]
fn fold36(mut correct: bool, raw: &[u8]) -> Option<usize> {
const LUT_DIGIT: [u8; 10] = [0, 1, 2, 3, 4, 6, 7, 8, 9, 0];
const LUT_LETTER_T: [u8; 26] = [
1, 3, 5, 7, 9, 2, 4, 6, 8, 10, 2, 4, 6, 8, 10, 3, 5, 7, 9, 11, 3, 5, 7, 9, 11, 4,
];
const LUT_LETTER_F: [u8; 26] = [
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 6, 7, 8, 9, 10, 11,
];
let mut acc = 0;
for c in raw.iter().copied().rev() {
match c {
b'0'..=b'9' => {
let digit = (c - b'0') as usize;
acc += digit;
if correct {
acc += LUT_DIGIT[digit] as usize;
}
correct = !correct;
}
b'A'..=b'Z' => {
let letter = (c - b'A') as usize;
if correct {
acc += LUT_LETTER_T[letter] as usize;
} else {
acc += LUT_LETTER_F[letter] as usize;
}
}
_ => return None,
}
}
Some(acc)
}
pub mod decimal {
use crate::*;
pub fn valid(ascii: &[u8]) -> bool {
match fold10_swar(0x0201020102010201, 0x7f047f047f047f04, ascii) {
Some(d) => d % 10 == 0,
None => false,
}
}
pub fn valid_arr<const W: usize>(ascii: &[u8; W]) -> bool {
match fold10_swar(0x0201020102010201, 0x7f047f047f047f04, ascii) {
Some(d) => d % 10 == 0,
None => false,
}
}
pub fn checksum(ascii: &[u8]) -> Option<u8> {
let sum = fold10_swar(0x0102010201020102, 0x047f047f047f047f, ascii)?;
Some(b'0' + ((10 - (sum % 10)) % 10) as u8)
}
}
pub mod alphanum {
use crate::*;
pub fn valid(ascii: &[u8]) -> bool {
match fold36(false, ascii) {
Some(v) => v % 10 == 0,
None => false,
}
}
pub fn valid_arr<const T: usize>(ascii: &[u8; T]) -> bool {
match fold36(false, ascii) {
Some(v) => v % 10 == 0,
None => false,
}
}
pub fn checksum(ascii: &[u8]) -> Option<u8> {
let sum = fold36(true, ascii)?;
Some(b'0' + ((10 - (sum % 10)) % 10) as u8)
}
}
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));
}
}
}