Credit Card Validator — Ruby 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 Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# credit-card-validator — Luhn checksum + brand detection for card numbers.
#
# Language: Ruby (3.2, standard library only)
# Source: CosmoDev polyglot showcase port of the Credit Card Validator tool,
# ported from src/lib/creditCard.ts (the canonical TypeScript lib) and
# held in lock-step with cli/credit-card-validator/credit-card-validator.go.
# License: display source — part of CosmoDev's polyglot tool pages.
#
# Pure credit-card validation with no I/O beyond the demo at the bottom:
# sanitize to digits, run the Luhn checksum, detect the issuing brand by
# prefix + length, and group the number for display. Never raises on
# malformed input; mirrors the TypeScript contract.
#
# NOTE on \d: Ruby's Regexp treats \d as ASCII [0-9] by default (unlike
# Python), so the patterns here could use \d — they stick to explicit
# [0-9] ranges to stay byte-for-byte identical to the TypeScript source.
# Full validation result. `brand` is nil when the issuer is unknown.
CardInfo = Struct.new(
:input,
:sanitized, # digits only
:valid, # digits present, sane length, and Luhn passes
:luhn_valid, # Luhn checksum passes (requires >= 12 digits)
:brand, # issuing brand, or nil when unknown
:formatted, # grouped per brand spacing rules
keyword_init: true
)
# Luhn (mod-10) checksum. Empty or non-digit input returns false.
def luhn_check(digits)
return false if digits.empty? || digits.match?(/[^0-9]/)
total = 0
should_double = 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).
digits.reverse.each_char do |ch|
d = ch.ord - 48 # ord("0") == 48
if should_double
d *= 2
d -= 9 if d > 9
end
total += d
should_double = !should_double
end
total % 10 == 0
end
# Brand detection table. ORDER MATTERS: the first prefix+length match wins.
# Maestro is last because its broad "6" / 5x ranges overlap narrower issuers.
BRAND_RULES = [
['Visa', /^4/, [13, 16, 19]],
['Mastercard', /^(5[1-5]|2(2[2-9]|[3-6][0-9]|7[01]|720))/, [16]],
['American Express', /^3[47]/, [15]],
['Discover', /^(6011|65|64[4-9]|622(12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|9([01][0-9]|2[0-5])))/, [16, 19]],
['Diners Club', /^(30[0-5]|3095|36|38|39|54|55)/, [14, 16]],
['JCB', /^35(2[89]|[3-8][0-9])/, [16, 17, 18, 19]],
['UnionPay', /^62/, [16, 17, 18, 19]],
['Maestro', /^(50|56|57|58|6)/, (12..19).to_a],
].freeze
# Detect the card brand by prefix + length. Returns nil when unknown.
def detect_brand(digits)
return nil if digits.empty?
BRAND_RULES.each do |name, pattern, lengths|
return name if digits.match?(pattern) && lengths.include?(digits.length)
end
nil
end
# Slice digits into runs of the given widths, joining with spaces.
def group(digits, widths)
parts = []
i = 0
n = digits.length
widths.each do |w|
break if i >= n
parts << digits[i, w] # String#[](start, length) clamps the run at the end
i += w
end
# Any trailing remainder (e.g. a partially typed number) is appended as-is.
parts << digits[i..] if i < n
parts.join(' ')
end
# Group the number per brand spacing rules.
def format_card(digits, brand)
return '' if digits.empty?
# Amex uses a 4-6-5 grouping; Diners Club's 14-digit variant uses 4-6-4.
return group(digits, [4, 6, 5]) if brand == 'American Express'
return group(digits, [4, 6, 4]) if brand == 'Diners Club' && digits.length == 14
# Default: groups of four separated by spaces; the lookahead (?=.) drops the trailing space.
digits.gsub(/(.{4})(?=.)/, '\1 ').strip
end
# Validate a card number. Always returns a CardInfo; never raises.
def validate_card(input)
sanitized = input.gsub(/[^0-9]+/, '')
brand = detect_brand(sanitized)
# Luhn is only meaningful once we have a plausible card length (>= 12 digits).
luhn_valid = sanitized.length >= 12 && luhn_check(sanitized)
CardInfo.new(
input: input,
sanitized: sanitized,
valid: luhn_valid,
luhn_valid: luhn_valid,
brand: brand,
formatted: format_card(sanitized, brand)
)
end
if $PROGRAM_NAME == __FILE__
# Tiny showcase: validate a well-known test number (Visa).
info = validate_card('4111 1111 1111 1111')
puts "brand=#{info.brand || 'None'} valid=#{info.valid} formatted=\"#{info.formatted}\""
end
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 →