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 →