2,894 bytes · the Python implementation · view raw
from typing import List, Optional
from .validation_isbn_types import IsbnCheck
def _invalid(reason: str) -> IsbnCheck:
return IsbnCheck(valid=False, normalised=None, isbn10=None, reason=reason)
def _ean_check(twelve: str) -> str:
"""The EAN-13 check digit for twelve digits: weights 1, 3, 1, 3 ... from the left."""
total = sum((ord(twelve[i]) - 48) * (1if i % 2 == 0else3) for i in range(12))
return str((10 - total % 10) % 10)
def _isbn10_check(nine: str) -> str:
"""The ISBN-10 check character for nine digits: weights 10 down to 2, mod 11, 10 written X."""
total = sum((ord(nine[i]) - 48) * (10 - i) for i in range(9))
check = (11 - total % 11) % 11return"X"if check == 10else str(check)
def validate_isbn(value: str) -> IsbnCheck:
"""Check an ISBN-10 or ISBN-13 and return it as an ISBN-13, with its ISBN-10 where one exists. The two forms use different check digits, so conversion recomputes rather than copies. Validators answer rather than raise. """ifnot isinstance(value, str):
return _invalid("empty")
# Hyphen positions vary by publisher, so separators are ignored wholesale.
compact = "".join(ch for ch in value if ch != " "and ch != "-")
# An optional "ISBN" label, any case, then an optional colon. Folded by# hand: str.upper() is Unicode-aware and the Rust sibling is not.if len(compact) >= 4:
label = "".join(chr(ord(c) - 32) if"a" <= c <= "z"else c for c in compact[:4])
if label == "ISBN":
compact = compact[4:]
if compact.startswith(":"):
compact = compact[1:]
ifnot compact:
return _invalid("empty")
out: List[str] = []
for ch in compact:
if"0" <= ch <= "9":
out.append(ch)
elif ch == "X"or ch == "x":
out.append("X")
else:
return _invalid("bad-character")
isbn = "".join(out)
if len(isbn) != 10and len(isbn) != 13:
return _invalid("bad-length")
# X means ten, so it can only be an ISBN-10's check digit.
x_at = isbn.find("X")
if x_at != -1and (len(isbn) == 13or x_at != 9):
return _invalid("bad-format")
if len(isbn) == 13:
prefix = isbn[:3]
if prefix != "978"and prefix != "979":
return _invalid("bad-prefix")
if _ean_check(isbn) != isbn[12]:
return _invalid("bad-check-digit")
# 979 ISBNs were never ISBN-10s, so they have no ISBN-10 to give back.
isbn10: Optional[str] = isbn[3:12] + _isbn10_check(isbn[3:12]) if prefix == "978"elseNonereturn IsbnCheck(valid=True, normalised=isbn, isbn10=isbn10, reason=None)
if _isbn10_check(isbn) != isbn[9]:
return _invalid("bad-check-digit")
body = "978" + isbn[:9]
return IsbnCheck(valid=True, normalised=body + _ean_check(body), isbn10=isbn, reason=None)