1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
//! isbnid-rs
//! Rust ISBN identifier library
//!
//! isbnid is a simple crate to handle ISBN identification numbers.
//! isbnid will store, check and convert ISBNs in ISBN10, and ISBN13
//! formats and it will transform between them and output in URN form.
//! isbnid can also output ISBN numbers with the correct hyphens
//! corresponding to the actual issuance authorities. The information
//! is retrieved from https://www.isbn-international.org/. ISBN numbers
//! have a complex internal structure which roughly represents the country,
//! the language and the publisher. See also https://en.wikipedia.org/wiki/ISBN.
//!
//! # Install
//! You can find this crate on crates.io and it can be used by adding isbnid to the
//! dependencies in your project's Cargo.toml and root file.
//!
//! ```text
//! [dependencies]
//! isbnid = "0.1.0"
//! ```
//!
//! # Usage
//!
//! ```text
//! extern crate isbnid;
//!
//! use isbnid::isbn;
//!
//! let id = isbn::ISBN("9780553109535").unwrap();
//! println!("{}", id.isbn10());
//! println!("{}", id.isbn13());
//! ```
//!
//! ```text
//! 0553109537
//! 9780553109535
//! ```


use std::result;
use std::ascii::AsciiExt;
use std::str::FromStr;

use regex::Regex;
use hyphen;


#[derive(Debug)]
pub enum ISBNError {
    /// String doesn't form a valid ISBN10 or ISBN13 number encoding
    Format,
    /// ISBN Check digit is not valid
    CheckDigit,
    /// ISBN13 Bookland encoding (EAN-13) is different form 978 or 979,
    /// or it is 979 when converting to ISBN10
    Bookland,
    /// ISBN doesn't belong to the ISBN International official range
    Range
}

fn digit10(id: &str) -> u64 {
    let mut n = u64::from_str(&id[0..9]).unwrap();
    let mut d = 0u64;

    for i in 1..10 {
        d = d + (10 - i) * (n % 10);
        n = n / 10;
    }
    d % 11
}

fn digit13(id: &str) -> u64 {
    let mut n = u64::from_str(&id[0..12]).unwrap();
    let mut d = 0u64;

    for i in 1..13 {
        d = d + (1 + 2 * (i % 2)) * (n % 10);
        n = n / 10;
    }
    // Kludge for unsigned negative module
    (100000000000000000u64 - d) % 10 // 10^17
}

pub struct ISBN {
    id: String,
}

impl ISBN {
    /// Creates a new ISBN number object.
    /// It will fail if the encoding is incorrect or if the Bookland is not 978, 979
    pub fn new(id: &str) -> result::Result<ISBN, ISBNError> {
        let reif = Regex::new(r"^(\d(-| )?){9}(x|X|\d|(\d(-| )?){3}\d)$").unwrap();
        let reis = Regex::new(r"[^0-9X]").unwrap();

        if ! id.is_ascii() || ! reif.is_match(id) {
            // Invalid ISBN format
            return Err(ISBNError::Format)
        }
        let nid: String = reis.replace_all(&id.to_uppercase(), "").into();
        if nid.len() == 13 {
            if &nid[0..3] != "978" && &nid[0..3] != "979" {
                // Invalid Bookland code
                return Err(ISBNError::Bookland);
            }
            if u64::from_str(&nid[12..13]).unwrap() != digit13(&nid) {
                // Invalid ISBN check digit
                return Err(ISBNError::CheckDigit);
            }
            return Ok(ISBN{id: nid});
        }
        if nid.len() == 10 {
            let id13 = "978".to_string() + &nid[0..9];
            if &nid[9..10] == "X" && 10 != digit10(&nid) {
                 // Invalid ISBN check digit
                return Err(ISBNError::CheckDigit);
            }
            if &nid[9..10] == "X" && 10 == digit10(&nid) {
                return Ok(ISBN{id: format!("{}{}", &id13, digit13(&id13))});
            }
            if u64::from_str(&nid[9..10]).unwrap() != digit10(&nid) {
                // Invalid ISBN check digit
                return Err(ISBNError::CheckDigit);
            }
            return Ok(ISBN{id: format!("{}{}", &id13, digit13(&id13))});
        }
        // Invalid ISBN format, dead code by regex
        assert!(false);
        Err(ISBNError::Format)
    }

    /// Returns the ISBN10 encoding. It will fail if the ISBN13 Bookland is 979
    /// as ISBN10 is only defined for 978
    pub fn isbn10(&self) -> Result<String, ISBNError> {
        if &self.id[0..3] != "978" {
            // Invalid Bookland code
            return Err(ISBNError::Bookland)
        }
        let check10 = digit10(&self.id[3..12]);
        if check10 == 10 {
            Ok(format!("{}X", &self.id[3..12] ))
        }
        else {
            Ok(format!("{}{}", &self.id[3..12], check10))
        }
    }

    /// Returns the ISBN13 encoding. The internal encoding is ISBN13 so this will never fail
    pub fn isbn13(&self) -> String {
        format!("{}", &self.id)
    }

    /// Returns a hyphenated ISBN13 number. It will fail if the ISBN number is not registered
    pub fn hyphen(&self) -> Result<String, ISBNError> {
        let (grp, reg, pbl) = hyphen::segments(&self.id);
        if grp == 0 {
            return Err(ISBNError::Range);
        }
        Ok([&self.id[0..3], &self.id[3..3 + grp],  &self.id[3 + grp .. 3 + grp + reg], &self.id[12 - pbl..12], &self.id[12..13]].join("-"))
    }

    /// RFC 2888, URN Encoding of ISBN. https://www.ietf.org/rfc/rfc2288
    pub fn urn(&self) -> String {
        format!("URN:ISBN:{}", &self.id)
    }

    /// Returns doi formated ISBN. It fail if the ISBN number is not registered
    pub fn doi(&self) -> Result<String, ISBNError> {
        let (grp, reg, pbl) = hyphen::segments(&self.id);
        if grp == 0 {
            return Err(ISBNError::Range);
        }
        Ok(format!("10.{}.{}/{}", &self.id[0..3], &self.id[3..3 + grp + reg], &self.id[12 - pbl..13]))
    }

    /// Static ISBN format validation
    pub fn is_valid(id: &str) -> bool {
        match ISBN::new(id) {
            Ok(_) => true,
            Err(_) => false
        }
    }
}