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". */exportfunction 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 };
}