Functional Weave
Code in Python

validation.luhn@1.0.0

impl/typescript.ts

2,547 bytes · the TypeScript implementation · view raw

/**
 * The Luhn (mod 10) checksum, as used by payment card numbers, IMEIs,
 * SIM ICCIDs and a long tail of national identifiers.
 *
 * Luhn is a *transcription* check. It catches a single mistyped digit and
 * most adjacent transpositions before a request leaves the building. It says
 * nothing about whether the card exists, is open, belongs to the person
 * typing it, or has any money behind it. Only the acquirer can answer that,
 * so never render "card valid" on the strength of this function.
 */
export function isLuhn(value: string): boolean {
  // Non-strings are answered rather than thrown at: "is this valid?" is a
  // question, and "no" is a complete answer to it.
  if (typeof value !== "string") return false;

  const digits: number[] = [];
  for (const ch of value) {
    // Card numbers are printed and pasted in four-digit groups, so tolerating
    // the separators here saves every caller from writing the same strip.
    if (ch === " " || ch === "-") continue;
    if (ch < "0" || ch > "9") return false;
    digits.push(ch.charCodeAt(0) - 48);
  }

  // A lone digit satisfies the arithmetic whenever it is 0, which would make
  // this function a rubber stamp for a stray keystroke. No identifier scheme
  // issues one-digit numbers, so two is the honest floor.
  if (digits.length < 2) return false;

  let total = 0;
  let position = 0;
  for (let i = digits.length - 1; i >= 0; i--, position++) {
    let d = digits[i];
    if (position % 2 === 1) {
      d *= 2;
      // Doubling can only reach 18, so subtracting 9 is the same as summing
      // the two decimal digits, without the string round trip.
      if (d > 9) d -= 9;
    }
    total += d;
  }
  return total % 10 === 0;
}

/**
 * The digit that would make `prefix` pass the Luhn check.
 *
 * Useful for generating test data and for completing a partially known
 * number; it is the inverse of the check above, not a second opinion on it.
 */
export function luhnCheckDigit(prefix: string): number {
  const digits: number[] = [];
  for (const ch of prefix) {
    if (ch === " " || ch === "-") continue;
    if (ch < "0" || ch > "9") return -1;
    digits.push(ch.charCodeAt(0) - 48);
  }
  if (digits.length === 0) return -1;

  let total = 0;
  let position = 0;
  // The check digit will occupy position 0, so the prefix starts at 1.
  for (let i = digits.length - 1; i >= 0; i--, position++) {
    let d = digits[i];
    if (position % 2 === 0) {
      d *= 2;
      if (d > 9) d -= 9;
    }
    total += d;
  }
  return (10 - (total % 10)) % 10;
}