Current section
Files
Jump to
Current section
Files
src/glisbn.gleam
//// A ISBN utility library.
import gleam/bool
import gleam/int
import gleam/list
import gleam/string
import regions
pub type IsbnError {
InvalidIsbn
RegistrantNotFound
}
/// Takes an ISBN (10 or 13) and checks its validity by checking the checkdigit, length and characters.
///
/// ## Examples
///
/// ```gleam
/// is_valid("978-5-12345-678-1")
/// // -> True
/// ```
///
/// ```gleam
/// is_valid("85-359-0277-5")
/// // -> True
/// ```
///
/// ```gleam
/// is_valid("85-359-0277")
/// // -> False
/// ```
///
/// ```gleam
/// is_valid("str")
/// // -> False
/// ```
///
pub fn is_valid(isbn: String) -> Bool {
is_chars_correct(isbn) && is_checkdigit_valid(isbn)
}
/// Takes an ISBN (10 or 13) and checks its validity by its checkdigit.
///
/// ## Examples
///
/// ```gleam
/// is_checkdigit_valid("85-359-0277-5")
/// // -> True
/// ```
///
/// ```gleam
/// is_checkdigit_valid("978-5-12345-678-1")
/// // -> True
/// ```
///
/// ```gleam
/// is_checkdigit_valid("978-5-12345-678")
/// // -> False
/// ```
///
pub fn is_checkdigit_valid(isbn: String) -> Bool {
let normalized_isbn = normalize(isbn)
let length: Int = string.length(normalized_isbn)
use <- bool.guard(when: length != 10 && length != 13, return: False)
let assert Ok(digit) = case length {
10 -> isbn10_checkdigit(normalized_isbn)
_ -> isbn13_checkdigit(normalized_isbn)
}
let assert Ok(last_digit) = string.last(normalized_isbn)
digit == last_digit
}
/// Takes an ISBN 10 code as string, returns its check digit.
///
/// ## Examples
///
/// ```gleam
/// isbn10_checkdigit("85-359-0277")
/// // -> Ok("5")
/// ```
///
/// ```gleam
/// isbn10_checkdigit("5-02-013850")
/// // -> Ok("9")
/// ```
///
/// ```gleam
/// isbn10_checkdigit("0str")
/// // -> Error(InvalidIsbn)
/// ```
///
/// ```gleam
/// isbn10_checkdigit("887385107")
/// // -> Ok("X")
/// ```
///
pub fn isbn10_checkdigit(isbn: String) -> Result(String, IsbnError) {
let normalized_isbn: String = normalize(isbn)
let length: Int = string.length(normalized_isbn)
use <- bool.guard(when: length < 8 || length > 10, return: Error(InvalidIsbn))
let assert Ok(nsum) =
normalized_isbn
|> string.slice(0, 9)
|> string.split("")
|> list.map(fn(num) {
let assert Ok(res) = int.parse(num)
res
})
|> list.index_map(fn(digit, pos) { { 10 - pos } * digit })
|> list.reduce(fn(acc, x) { acc + x })
let assert Ok(first_part) = int.modulo(nsum, 11)
let assert Ok(final_part) = int.modulo(11 - first_part, 11)
case final_part {
10 -> Ok("X")
_ -> Ok(int.to_string(final_part))
}
}
/// Takes an ISBN 13 code as string, returns its check digit.
///
/// ## Examples
///
/// ```gleam
/// isbn13_checkdigit("978-5-12345-678")
/// // -> Ok("1")
/// ```
///
/// ```gleam
/// isbn13_checkdigit("978-0-306-40615")
/// // -> Ok("7")
/// ```
///
/// ```gleam
/// isbn13_checkdigit("1str")
/// // -> Error(InvalidIsbn)
/// ```
///
pub fn isbn13_checkdigit(isbn: String) -> Result(String, IsbnError) {
let normalized_isbn: String = normalize(isbn)
let length: Int = string.length(normalized_isbn)
use <- bool.guard(
when: length < 11 || length > 13,
return: Error(InvalidIsbn),
)
let assert Ok(nsum) =
normalized_isbn
|> string.slice(0, 12)
|> string.split("")
|> list.map(fn(num) {
let assert Ok(res) = int.parse(num)
res
})
|> list.index_map(fn(digit, pos) {
case int.is_odd(pos) {
True -> digit * 3
False -> digit
}
})
|> list.reduce(fn(acc, x) { acc + x })
let assert Ok(first_part) = int.modulo(nsum, 10)
let final_part: Int = 10 - first_part
case final_part {
10 -> Ok("0")
_ -> Ok(int.to_string(final_part))
}
}
/// Takes an ISBN 10 and converts it to ISBN 13.
///
/// ## Examples
///
/// ```gleam
/// isbn10_to_13("85-359-0277-5")
/// // -> Ok("9788535902778")
/// ```
///
/// ```gleam
/// isbn10_to_13("0306406152")
/// // -> Ok("9780306406157")
/// ```
///
/// ```gleam
/// isbn10_to_13("1str")
/// // -> Error(InvalidIsbn)
/// ```
///
pub fn isbn10_to_13(isbn: String) -> Result(String, IsbnError) {
let normalized = normalize(isbn)
case string.length(normalized) == 10 && is_valid(normalized) {
True -> {
let first_chars = "978" <> string.slice(normalized, 0, 9)
let assert Ok(checkdigit) = isbn13_checkdigit(first_chars)
Ok(first_chars <> checkdigit)
}
False -> Error(InvalidIsbn)
}
}
/// Takes an ISBN 13 and converts it to ISBN 10.
/// Only ISBNs with the 978 prefix can be converted;
/// 979-prefixed ISBNs have no ISBN-10 equivalent.
///
/// ## Examples
///
/// ```gleam
/// isbn13_to_10("9788535902778")
/// // -> Ok("8535902775")
/// ```
///
/// ```gleam
/// isbn13_to_10("9780306406157")
/// // -> Ok("0306406152")
/// ```
///
/// ```gleam
/// isbn13_to_10("1str")
/// // -> Error(InvalidIsbn)
/// ```
///
pub fn isbn13_to_10(isbn: String) -> Result(String, IsbnError) {
let normalized = normalize(isbn)
case
string.length(normalized) == 13
&& string.starts_with(normalized, "978")
&& is_valid(normalized)
{
True -> {
let first_chars = string.slice(normalized, 3, 9)
let assert Ok(checkdigit) = isbn10_checkdigit(first_chars)
Ok(first_chars <> checkdigit)
}
False -> Error(InvalidIsbn)
}
}
/// Takes an ISBN and returns its prefix.
///
/// ## Examples
///
/// ```gleam
/// get_prefix("9788535902778")
/// // -> Ok("978-85")
/// ```
///
/// ```gleam
/// get_prefix("2-1234-5680-2")
/// // -> Ok("978-2")
/// ```
///
/// ```gleam
/// get_prefix("1str")
/// // -> Error(InvalidIsbn)
/// ```
///
pub fn get_prefix(isbn: String) -> Result(String, IsbnError) {
case is_correct(isbn) {
True -> {
let prepared_isbn = to_isbn13(isbn)
search_prefix(
string.slice(prepared_isbn, 0, 3),
drop_chars(prepared_isbn, 3),
0,
)
}
False -> Error(InvalidIsbn)
}
}
/// Takes an ISBN and returns its checkdigit.
///
/// ## Examples
///
/// ```gleam
/// get_checkdigit("9788535902778")
/// // -> Ok("8")
/// ```
///
/// ```gleam
/// get_checkdigit("2-1234-5680-2")
/// // -> Ok("2")
/// ```
///
/// ```gleam
/// get_checkdigit("1str")
/// // -> Error(InvalidIsbn)
/// ```
///
/// ```gleam
/// get_checkdigit("887385107X")
/// // -> Ok("X")
/// ```
///
pub fn get_checkdigit(isbn: String) -> Result(String, IsbnError) {
case is_correct(isbn) {
True -> {
let assert Ok(result) = string.last(isbn)
Ok(result)
}
False -> Error(InvalidIsbn)
}
}
/// Takes an ISBN and returns its publisher zone.
///
/// ## Examples
///
/// ```gleam
/// get_publisher_zone("9788535902778")
/// // -> Ok("Brazil")
/// ```
///
/// ```gleam
/// get_publisher_zone("2-1234-5680-2")
/// // -> Ok("French language")
/// ```
///
/// ```gleam
/// get_publisher_zone("1str")
/// // -> Error(InvalidIsbn)
/// ```
///
pub fn get_publisher_zone(isbn: String) -> Result(String, IsbnError) {
case is_correct(isbn) {
True ->
case get_info(to_isbn13(isbn)) {
Ok(region) -> Ok(region.name)
Error(e) -> Error(e)
}
False -> Error(InvalidIsbn)
}
}
/// Takes an ISBN and returns its registrant element.
///
/// ## Examples
///
/// ```gleam
/// get_registrant_element("9788535902778")
/// // -> Ok("359")
/// ```
///
/// ```gleam
/// get_registrant_element("978-1-86197-876-9")
/// // -> Ok("86197")
/// ```
///
/// ```gleam
/// get_registrant_element("1str")
/// // -> Error(InvalidIsbn)
/// ```
///
pub fn get_registrant_element(isbn: String) -> Result(String, IsbnError) {
case is_correct(isbn) {
True ->
case find_elements(to_isbn13(isbn)) {
Ok(#(registrant, _)) -> Ok(registrant)
Error(e) -> Error(e)
}
False -> Error(InvalidIsbn)
}
}
/// Takes an ISBN and returns its publication element.
///
/// ## Examples
///
/// ```gleam
/// get_publication_element("978-1-86197-876-9")
/// // -> Ok("876")
/// ```
///
/// ```gleam
/// get_publication_element("9789529351787")
/// // -> Ok("5178")
/// ```
///
/// ```gleam
/// get_publication_element("1str")
/// // -> Error(InvalidIsbn)
/// ```
///
pub fn get_publication_element(isbn: String) -> Result(String, IsbnError) {
case is_correct(isbn) {
True ->
case find_elements(to_isbn13(isbn)) {
Ok(#(_, publication)) -> Ok(publication)
Error(e) -> Error(e)
}
False -> Error(InvalidIsbn)
}
}
/// Takes an ISBN (10 or 13) and hyphenates it.
///
/// ## Examples
///
/// ```gleam
/// hyphenate("9788535902778")
/// // -> Ok("978-85-359-0277-8")
/// ```
///
/// ```gleam
/// hyphenate("0306406152")
/// // -> Ok("0-306-40615-2")
/// ```
///
/// ```gleam
/// hyphenate("978-5-12345-678")
/// // -> Error(InvalidIsbn)
/// ```
///
pub fn hyphenate(isbn: String) -> Result(String, IsbnError) {
case is_correct(isbn) {
True ->
case is_isbn10(isbn) {
True -> hyphenate_isbn10(isbn)
False -> hyphenate_isbn13(isbn)
}
False -> Error(InvalidIsbn)
}
}
/// Checks if an ISBN (10 or 13) code is correctly hyphenated. If ISBN incorrect, that count as no.
///
/// ## Examples
///
/// ```gleam
/// is_hyphens_correct("978-85-359-0277-8")
/// // -> True
/// ```
///
/// ```gleam
/// is_hyphens_correct("0-306-40615-2")
/// // -> True
/// ```
///
/// ```gleam
/// is_hyphens_correct("97-8853590277-8")
/// // -> False
/// ```
///
/// ```gleam
/// is_hyphens_correct("03-064-06152")
/// // -> False
/// ```
///
/// ```gleam
/// is_hyphens_correct("str")
/// // -> False
/// ```
///
pub fn is_hyphens_correct(isbn: String) -> Bool {
case is_correct(isbn) {
True ->
case hyphenate(isbn) {
Ok(result) -> isbn == result
Error(_) -> False
}
False -> False
}
}
fn is_length_correct(isbn: String) -> Bool {
let length: Int = string.length(isbn)
length == 10 || length == 13
}
fn drop_chars(str: String, amount: Int) -> String {
string.slice(from: str, at_index: amount, length: string.length(str))
}
fn is_digit(ch: String) -> Bool {
"0123456789"
|> string.contains(ch)
}
fn normalize(isbn: String) -> String {
isbn
|> string.split("")
|> list.filter(fn(ch) { is_digit(ch) || ch == "X" })
|> string.join("")
}
fn is_chars_correct(isbn: String) -> Bool {
isbn
|> normalize()
|> is_length_correct()
}
fn is_isbn10(isbn: String) -> Bool {
let length: Int =
isbn
|> normalize()
|> string.length()
length == 10
}
fn is_correct(isbn: String) -> Bool {
isbn
|> normalize()
|> is_valid()
}
fn to_isbn13(isbn: String) -> String {
case is_isbn10(isbn) {
True -> {
let assert Ok(result) = isbn10_to_13(isbn)
result
}
False -> normalize(isbn)
}
}
fn search_prefix(
prefix: String,
body: String,
search_length: Int,
) -> Result(String, IsbnError) {
use <- bool.guard(
when: search_length > string.length(body),
return: Error(InvalidIsbn),
)
let search_candidate: String =
prefix <> "-" <> string.slice(body, 0, search_length)
case list.find(regions.dataset, fn(x) { x.code == search_candidate }) {
Ok(_) -> Ok(search_candidate)
Error(Nil) -> search_prefix(prefix, body, search_length + 1)
}
}
fn get_body(isbn: String, prefix: String) -> String {
let start = string.length(prefix) - 1
string.slice(isbn, start, string.length(isbn) - start - 1)
}
fn get_info(isbn: String) -> Result(regions.Dataset, IsbnError) {
case get_prefix(isbn) {
Ok(prefix) ->
case list.find(regions.dataset, fn(x) { x.code == prefix }) {
Ok(region) -> Ok(region)
Error(_) -> Error(InvalidIsbn)
}
Error(e) -> Error(e)
}
}
fn find_elements(
prepared_isbn: String,
) -> Result(#(String, String), IsbnError) {
case get_prefix(prepared_isbn) {
Error(e) -> Error(e)
Ok(prefix) -> {
let body = get_body(prepared_isbn, prefix)
case list.find(regions.dataset, fn(x) { x.code == prefix }) {
Error(_) -> Error(InvalidIsbn)
Ok(region) ->
case
list.find(region.ranges, fn(range) {
let assert Ok(first_part) = list.first(range)
let assert Ok(begin) = int.parse(first_part)
let assert Ok(last_part) = list.last(range)
let assert Ok(end) = int.parse(last_part)
let length = string.length(last_part)
let range_part = string.slice(body, 0, length)
let assert Ok(area) = int.parse(range_part)
begin <= area && area <= end
})
{
Ok(range) -> {
let assert Ok(last_part) = list.last(range)
let length = string.length(last_part)
let registrant = string.slice(body, 0, length)
let publication = drop_chars(body, length)
Ok(#(registrant, publication))
}
Error(_) -> Error(RegistrantNotFound)
}
}
}
}
}
fn hyphenate_isbn13(isbn: String) -> Result(String, IsbnError) {
let normalized = normalize(isbn)
case get_prefix(normalized) {
Error(e) -> Error(e)
Ok(prefix) -> {
let assert Ok(checkdigit) = string.last(normalized)
case find_elements(normalized) {
Ok(#(registrant, publication)) ->
Ok(string.join([prefix, registrant, publication, checkdigit], "-"))
Error(e) -> Error(e)
}
}
}
}
fn hyphenate_isbn10(isbn: String) -> Result(String, IsbnError) {
case isbn10_to_13(isbn) {
Error(e) -> Error(e)
Ok(isbn13) ->
case get_prefix(isbn13) {
Error(e) -> Error(e)
Ok(fullprefix) -> {
let assert Ok(isbn10_prefix) =
string.split(fullprefix, "-")
|> list.last()
let assert Ok(checkdigit) = string.last(normalize(isbn))
case find_elements(isbn13) {
Ok(#(registrant, publication)) ->
Ok(string.join(
[isbn10_prefix, registrant, publication, checkdigit],
"-",
))
Error(e) -> Error(e)
}
}
}
}
}