Skip to main content

setzer_model/
isbn.rs

1//! ISBNs as Kernsetzer reads them, one rule for print pre-flight, the EPUB's
2//! `dc:identifier` and the app's book details.
3
4/// The digits of a valid ISBN-10 or ISBN-13, an ISBN-10's check digit `X`
5/// in capitals: hyphens, spaces and an `ISBN` prefix allowed, check digit
6/// verified. `None` for anything else.
7pub fn digits(s: &str) -> Option<String> {
8    let s = s.trim();
9    let s = match s.get(..4) {
10        Some(p) if p.eq_ignore_ascii_case("isbn") => &s[4..],
11        _ => s,
12    };
13    let s = s.trim_start_matches([':', ' ']);
14    let mut digits = String::new();
15    for c in s.chars() {
16        match c {
17            '0'..='9' => digits.push(c),
18            'X' | 'x' => digits.push('X'),
19            '-' | ' ' => {}
20            _ => return None,
21        }
22    }
23    let value = |c: char| c.to_digit(10);
24    let ok = match digits.len() {
25        13 => {
26            let mut sum = 0;
27            for (i, c) in digits.chars().enumerate() {
28                sum += value(c)? * if i % 2 == 0 { 1 } else { 3 };
29            }
30            sum % 10 == 0
31        }
32        10 => {
33            let mut sum = 0;
34            for (i, c) in digits.chars().enumerate() {
35                // `X` (10) only as the check digit.
36                let v = match c {
37                    'X' if i == 9 => 10,
38                    c => value(c)?,
39                };
40                sum += v * (10 - i as u32);
41            }
42            sum % 11 == 0
43        }
44        _ => false,
45    };
46    ok.then_some(digits)
47}
48
49/// Whether `s` is a valid ISBN-10 or ISBN-13 as [`digits`] reads one.
50pub fn is_valid(s: &str) -> bool {
51    digits(s).is_some()
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn digits_of_valid_isbns() {
60        assert_eq!(
61            digits("978-3-16-148410-0").as_deref(),
62            Some("9783161484100")
63        );
64        assert_eq!(
65            digits("ISBN: 978 3 16 148410 0").as_deref(),
66            Some("9783161484100")
67        );
68        assert_eq!(digits("isbn 0-306-40615-2").as_deref(), Some("0306406152"));
69        assert_eq!(digits("0-8044-2957-X").as_deref(), Some("080442957X"));
70        assert_eq!(digits("0-8044-2957-x").as_deref(), Some("080442957X"));
71        assert_eq!(digits("978-3-16-148410-1"), None, "check digit");
72        assert_eq!(digits("X-306-40615-2"), None);
73        assert_eq!(digits("978316148410"), None);
74        assert_eq!(digits("tbd"), None);
75        assert_eq!(digits(""), None);
76    }
77
78    #[test]
79    fn validity() {
80        for isbn in [
81            "978-3-16-148410-0",
82            "9783161484100",
83            "ISBN 0-306-40615-2",
84            "080442957X",
85        ] {
86            assert!(is_valid(isbn), "{isbn}");
87        }
88        for isbn in [
89            "978-3-16-148410-1",
90            "0-306-40615-3",
91            "97831614841",
92            "X783161484100",
93            "978-3-16-l48410-0",
94            "978316148410X",
95        ] {
96            assert!(!is_valid(isbn), "{isbn}");
97        }
98    }
99}