#[must_use]
pub fn check_digit(body: &str) -> Option<char> {
if body.is_empty() || !body.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let digits: Vec<u32> = body.bytes().rev().map(|b| u32::from(b - b'0')).collect();
let odd: String = digits
.iter()
.step_by(2)
.map(|d| char::from(b'0' + u8::try_from(*d).unwrap_or(0)))
.collect();
let even: String = digits
.iter()
.skip(1)
.step_by(2)
.map(|d| char::from(b'0' + u8::try_from(*d).unwrap_or(0)))
.collect();
let doubled = odd.parse::<u64>().ok()?.checked_mul(2)?;
let joined = format!("{doubled}{even}");
let sum: u32 = joined.bytes().map(|b| u32::from(b - b'0')).sum();
let check = (10 - sum % 10) % 10;
Some(char::from(b'0' + u8::try_from(check).unwrap_or(0)))
}
#[must_use]
pub fn is_valid(code: &str) -> bool {
let Some((head, check)) = code.rsplit_once('-') else {
return false;
};
let body = head.trim_start_matches(|c: char| c.is_ascii_uppercase());
let prefix = head.strip_suffix(body).unwrap_or_default();
let shape = !body.is_empty()
&& body.bytes().all(|b| b.is_ascii_digit())
&& check.len() == 1
&& check.bytes().all(|b| b.is_ascii_digit());
match prefix {
"" => shape && check_digit(body).is_some_and(|expected| check.starts_with(expected)),
"LP" | "LL" | "LA" => shape,
_ => false,
}
}
#[must_use]
pub fn is_well_formed(code: &str) -> bool {
let Some((head, check)) = code.rsplit_once('-') else {
return false;
};
let body = head.trim_start_matches(|c: char| c.is_ascii_uppercase());
let prefix = head.strip_suffix(body).unwrap_or_default();
matches!(prefix, "" | "LP" | "LL" | "LA")
&& !body.is_empty()
&& body.bytes().all(|b| b.is_ascii_digit())
&& check.len() == 1
&& check.bytes().all(|b| b.is_ascii_digit())
}
#[must_use]
pub fn with_check_digit(body: &str) -> Option<String> {
check_digit(body).map(|c| format!("{body}-{c}"))
}
#[cfg(test)]
mod tests {
use super::{check_digit, is_valid, is_well_formed, with_check_digit};
#[test]
fn the_users_guide_example_and_published_codes_check() {
assert_eq!(check_digit("12345"), Some('5'));
assert!(is_valid("2345-7"), "glucose in blood");
assert!(is_valid("LP31755-9"), "a part code is checked for shape");
assert!(is_valid("LL715-4"));
assert!(is_valid("8480-6"), "systolic blood pressure");
assert!(!is_valid("LP31755-99"));
assert!(!is_valid("2345-8"));
assert!(!is_valid("2345"));
assert!(!is_valid("XX2345-7"));
assert!(
is_well_formed("11491-6"),
"a published code with a failing check digit"
);
assert!(!is_well_formed("11491-66"));
assert!(!is_well_formed("XX2345-7"));
assert_eq!(with_check_digit("2345").as_deref(), Some("2345-7"));
}
}