pub fn digits(s: &str) -> Option<String> {
let s = s.trim();
let s = match s.get(..4) {
Some(p) if p.eq_ignore_ascii_case("isbn") => &s[4..],
_ => s,
};
let s = s.trim_start_matches([':', ' ']);
let mut digits = String::new();
for c in s.chars() {
match c {
'0'..='9' => digits.push(c),
'X' | 'x' => digits.push('X'),
'-' | ' ' => {}
_ => return None,
}
}
let value = |c: char| c.to_digit(10);
let ok = match digits.len() {
13 => {
let mut sum = 0;
for (i, c) in digits.chars().enumerate() {
sum += value(c)? * if i % 2 == 0 { 1 } else { 3 };
}
sum % 10 == 0
}
10 => {
let mut sum = 0;
for (i, c) in digits.chars().enumerate() {
let v = match c {
'X' if i == 9 => 10,
c => value(c)?,
};
sum += v * (10 - i as u32);
}
sum % 11 == 0
}
_ => false,
};
ok.then_some(digits)
}
pub fn is_valid(s: &str) -> bool {
digits(s).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn digits_of_valid_isbns() {
assert_eq!(
digits("978-3-16-148410-0").as_deref(),
Some("9783161484100")
);
assert_eq!(
digits("ISBN: 978 3 16 148410 0").as_deref(),
Some("9783161484100")
);
assert_eq!(digits("isbn 0-306-40615-2").as_deref(), Some("0306406152"));
assert_eq!(digits("0-8044-2957-X").as_deref(), Some("080442957X"));
assert_eq!(digits("0-8044-2957-x").as_deref(), Some("080442957X"));
assert_eq!(digits("978-3-16-148410-1"), None, "check digit");
assert_eq!(digits("X-306-40615-2"), None);
assert_eq!(digits("978316148410"), None);
assert_eq!(digits("tbd"), None);
assert_eq!(digits(""), None);
}
#[test]
fn validity() {
for isbn in [
"978-3-16-148410-0",
"9783161484100",
"ISBN 0-306-40615-2",
"080442957X",
] {
assert!(is_valid(isbn), "{isbn}");
}
for isbn in [
"978-3-16-148410-1",
"0-306-40615-3",
"97831614841",
"X783161484100",
"978-3-16-l48410-0",
"978316148410X",
] {
assert!(!is_valid(isbn), "{isbn}");
}
}
}