Skip to content

Credit Card Validator — JavaScript source

Validate credit card numbers with the Luhn checksum and detect the issuing brand (Visa, Mastercard, Amex, Discover, Diners, JCB, UnionPay, Maestro). 100% client-side - nothing is transmitted.

This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// credit-card-validator - JavaScript.
// CosmoDev polyglot showcase port of the credit-card-validator tool,
// ported from src/lib/creditCard.ts.
//
// Display source - part of CosmoDev's polyglot tool pages.
//
// Pure credit-card validation logic with no DOM, no React, no I/O:
// sanitize to digits, run the Luhn checksum, detect the issuing brand
// by prefix + length, and group the number for display. Never throws.

/**
 * @typedef {Object} CardInfo
 * @property {string} input       - Original, untouched input.
 * @property {string} sanitized   - Digits only.
 * @property {boolean} valid      - True when digits are present, length sane, Luhn passes.
 * @property {boolean} luhnValid  - Luhn checksum passes (requires >= 12 digits).
 * @property {string|null} brand  - Issuing brand, or null when unknown.
 * @property {string} formatted   - Grouped per brand spacing rules.
 */

/** Non-empty and entirely decimal digits (JS \d is ASCII [0-9]). */
function isAllDigits(s) {
  return s.length > 0 && /^\d*$/.test(s);
}

/**
 * Luhn checksum (mod-10). Empty or non-digit input returns false rather
 * than throwing, mirroring the TypeScript contract.
 */
export function luhnCheck(digits) {
  if (!isAllDigits(digits)) return false;
  let sum = 0;
  let shouldDouble = false;
  // Walk right-to-left; double every second digit and collapse >9 to its digit-sum
  // (e.g. 7*2=14 → 14-9=5, identical to summing 1+4).
  for (let i = digits.length - 1; i >= 0; i--) {
    let d = digits.charCodeAt(i) - 48; // '0'.charCodeAt(0) === 48
    if (shouldDouble) {
      d *= 2;
      if (d > 9) d -= 9;
    }
    sum += d;
    shouldDouble = !shouldDouble;
  }
  return sum % 10 === 0;
}

/**
 * Brand detection table. ORDER MATTERS: the first prefix+length match wins.
 * Maestro is last because its broad "6" / 5x ranges overlap narrower issuers.
 */
const BRAND_RULES = [
  { name: 'Visa', re: /^4/, lengths: [13, 16, 19] },
  { name: 'Mastercard', re: /^(5[1-5]|2(2[2-9]|[3-6]\d|7[01]|720))/, lengths: [16] },
  { name: 'American Express', re: /^3[47]/, lengths: [15] },
  {
    name: 'Discover',
    re: /^(6011|65|64[4-9]|622(12[6-9]|1[3-9]\d|[2-8]\d\d|9([01]\d|2[0-5])))/,
    lengths: [16, 19],
  },
  { name: 'Diners Club', re: /^(30[0-5]|3095|36|38|39|54|55)/, lengths: [14, 16] },
  { name: 'JCB', re: /^35(2[89]|[3-8]\d)/, lengths: [16, 17, 18, 19] },
  { name: 'UnionPay', re: /^62/, lengths: [16, 17, 18, 19] },
  { name: 'Maestro', re: /^(50|56|57|58|6)/, lengths: [12, 13, 14, 15, 16, 17, 18, 19] },
];

/** Detect the card brand by prefix + length. Returns null when unknown. */
export function detectBrand(digits) {
  if (!digits) return null;
  for (const rule of BRAND_RULES) {
    if (rule.re.test(digits) && rule.lengths.includes(digits.length)) {
      return rule.name;
    }
  }
  return null;
}

/** Slice `digits` into groups of the given widths, joining with spaces. */
function group(digits, widths) {
  const parts = [];
  let i = 0;
  for (const w of widths) {
    if (i >= digits.length) break;
    parts.push(digits.slice(i, i + w));
    i += w;
  }
  // Any trailing remainder (e.g. a partially typed number) is appended as-is.
  if (i < digits.length) parts.push(digits.slice(i));
  return parts.join(' ');
}

/** Group the number per brand spacing rules. */
export function formatCard(digits, brand) {
  if (!digits) return '';
  // Amex uses a 4-6-5 grouping; Diners Club's 14-digit variant uses 4-6-4.
  if (brand === 'American Express') return group(digits, [4, 6, 5]);
  if (brand === 'Diners Club' && digits.length === 14) return group(digits, [4, 6, 4]);
  // Default: groups of four separated by spaces; the lookahead (?=.) drops the trailing space.
  return digits.replace(/(.{4})(?=.)/g, '$1 ').trim();
}

/**
 * Validate a card number. Always returns a CardInfo object; never throws.
 * `valid` mirrors `luhnValid` (Luhn + plausible length).
 */
export function validateCard(input) {
  const safe = input ?? '';
  const sanitized = String(safe).replace(/\D+/g, '');
  const brand = detectBrand(sanitized);
  // Luhn is only meaningful once we have a plausible card length (>= 12 digits).
  const luhnValid = sanitized.length >= 12 && luhnCheck(sanitized);
  return {
    input: String(safe),
    sanitized,
    valid: luhnValid,
    luhnValid,
    brand,
    formatted: formatCard(sanitized, brand),
  };
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →