2,957 bytes · the TypeScript implementation · view raw
import { type IsbnCheck } from"./validation_isbn_types.ts";
function invalid(reason: string): IsbnCheck {
return { valid: false, normalised: null, isbn10: null, reason };
}
function digitAt(s: string, i: number): number {
return s.charCodeAt(i) - 48;
}
/** The EAN-13 check digit for twelve digits: weights 1, 3, 1, 3 ... from the left. */function eanCheck(twelve: string): string {
let total = 0;
for (let i = 0; i < 12; i++) total += digitAt(twelve, i) * (i % 2 === 0 ? 1 : 3);
return String((10 - (total % 10)) % 10);
}
/** The ISBN-10 check character for nine digits: weights 10 down to 2, mod 11, 10 written X. */function isbn10Check(nine: string): string {
let total = 0;
for (let i = 0; i < 9; i++) total += digitAt(nine, i) * (10 - i);
const check = (11 - (total % 11)) % 11;
return check === 10 ? "X" : String(check);
}
/** * 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 throw. */exportfunction validateIsbn(value: string): IsbnCheck {
if (typeof value !== "string") return invalid("empty");
// Hyphen positions vary by publisher, so separators are ignored wholesale.let compact = "";
for (const ch of value) {
if (ch !== " " && ch !== "-") compact += ch;
}
// An optional "ISBN" label, any case, then an optional colon. Folded by// hand: toUpperCase() is Unicode-aware and the other languages are not.if (compact.length >= 4) {
const label = compact.slice(0, 4).replace(/[a-z]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 32));
if (label === "ISBN") compact = compact.slice(4);
}
if (compact.startsWith(":")) compact = compact.slice(1);
if (compact.length === 0) return invalid("empty");
let isbn = "";
for (const ch of compact) {
if (ch >= "0" && ch <= "9") isbn += ch;
elseif (ch === "X" || ch === "x") isbn += "X";
elsereturn invalid("bad-character");
}
if (isbn.length !== 10 && isbn.length !== 13) return invalid("bad-length");
// X means ten, so it can only be an ISBN-10's check digit.const xAt = isbn.indexOf("X");
if (xAt !== -1 && (isbn.length === 13 || xAt !== 9)) return invalid("bad-format");
if (isbn.length === 13) {
const prefix = isbn.slice(0, 3);
if (prefix !== "978" && prefix !== "979") return invalid("bad-prefix");
if (eanCheck(isbn) !== isbn[12]) return invalid("bad-check-digit");
// 979 ISBNs were never ISBN-10s, so they have no ISBN-10 to give back.const isbn10 = prefix === "978" ? isbn.slice(3, 12) + isbn10Check(isbn.slice(3, 12)) : null;
return { valid: true, normalised: isbn, isbn10, reason: null };
}
if (isbn10Check(isbn) !== isbn[9]) return invalid("bad-check-digit");
const body = "978" + isbn.slice(0, 9);
return { valid: true, normalised: body + eanCheck(body), isbn10: isbn, reason: null };
}