Functional Weave
Code in Python

validation.isbn@1.0.0

impl/typescript.ts

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.
 */
export function 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;
    else if (ch === "X" || ch === "x") isbn += "X";
    else return 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 };
}