Functional Weave
Code in Python

validation.gtin@1.0.0

impl/typescript.ts

1,943 bytes · the TypeScript implementation · view raw

import { type GtinCheck } from "./validation_gtin_types.ts";

/** The four GTIN lengths, and what each is called. */
const KINDS: { readonly [length: number]: string } = { 8: "ean-8", 12: "upc-a", 13: "ean-13", 14: "gtin-14" };

function invalid(reason: string): GtinCheck {
  return { valid: false, normalised: null, kind: null, gtin14: null, reason };
}

/**
 * Check an EAN-8, UPC-A, EAN-13 or GTIN-14 against the GS1 mod-10 check digit.
 *
 * A pass means the number was typed correctly, not that GS1 issued it.
 * Validators answer rather than throw: a malformed value is the answer "no".
 */
export function validateGtin(value: string): GtinCheck {
  if (typeof value !== "string") return invalid("empty");

  // Barcodes are printed in groups, so spaces and hyphens are ignored anywhere.
  const chars: string[] = [];
  for (const ch of value) {
    if (ch !== " " && ch !== "-") chars.push(ch);
  }
  if (chars.length === 0) return invalid("empty");
  for (const ch of chars) {
    if (ch < "0" || ch > "9") return invalid("bad-character");
  }
  const digits = chars.join("");
  const kind = KINDS[digits.length];
  if (kind === undefined) return invalid("bad-length");
  // Zero everywhere satisfies the arithmetic and has never been allocated.
  if (/^0+$/.test(digits)) return invalid("all-zero");

  // Weights run 3, 1, 3, 1 ... leftwards from the digit next to the check
  // digit. Counting from the right is what makes one rule serve all four
  // lengths: EAN-13's first digit gets 1, EAN-8's and UPC-A's get 3.
  let total = 0;
  let weight = 3;
  for (let i = digits.length - 2; i >= 0; i--) {
    total += (digits.charCodeAt(i) - 48) * weight;
    weight = weight === 3 ? 1 : 3;
  }
  const check = (10 - (total % 10)) % 10;
  if (check !== digits.charCodeAt(digits.length - 1) - 48) return invalid("bad-check-digit");

  return { valid: true, normalised: digits, kind, gtin14: digits.padStart(14, "0"), reason: null };
}