Credit Card Validator — Swift 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 Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// credit-card-validator — Luhn checksum + brand detection for card numbers.
//
// Language: Swift (5.9, standard library only — native Regex literals, Swift 5.7+)
// 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 crashes on
// malformed input; mirrors the TypeScript contract.
//
// The brand patterns use native Regex literals with prefixMatch(of:), which
// anchors at the start — mirroring the ^-anchored TS/Python patterns. They
// use the extended `#/.../#` delimiters: bare `/.../` literals parse
// ambiguously after an argument label. Each rule stores a closure so patterns
// with different capture shapes share one table type. Run as a script with
// `swift swift.swift` (top-level code needs a main.swift-style entry file
// under `swift build`).
/// Full validation result. `brand` is nil when the issuer is unknown
/// (the Swift equivalent of TypeScript's `string | null`).
struct CardInfo: Equatable {
var input: String
var sanitized: String // digits only
var valid: Bool // digits present, sane length, and Luhn passes
var luhnValid: Bool // Luhn checksum passes (requires >= 12 digits)
var brand: String? // issuing brand, or nil when unknown
var formatted: String // grouped per brand spacing rules
}
/// Luhn (mod-10) checksum. Empty or non-digit input returns false.
func luhnCheck(_ digits: String) -> Bool {
// Empty or any non-ASCII-digit input is invalid — matching /^[0-9]*$/ semantics.
guard !digits.isEmpty, digits.allSatisfy({ ("0"..."9").contains($0) }) else {
return false
}
var total = 0
var 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 ch in digits.reversed() {
var d = ch.wholeNumberValue ?? 0 // allSatisfy above guarantees a digit
if shouldDouble {
d *= 2
if d > 9 {
d -= 9
}
}
total += d
shouldDouble.toggle()
}
return total % 10 == 0
}
/// Brand detection rule: name, prefix test, allowed lengths.
/// The prefix test is a closure because Regex literals with capture groups
/// produce different generic outputs — a closure erases that difference.
private struct BrandRule {
let name: String
let lengths: Set<Int>
let matchesPrefix: (String) -> Bool
init(name: String, lengths: [Int], matchesPrefix: @escaping (String) -> Bool) {
self.name = name
self.lengths = Set(lengths)
self.matchesPrefix = matchesPrefix
}
}
// Brand detection table. ORDER MATTERS: the first prefix+length match wins.
// Maestro is last because its broad "6" / 5x ranges overlap narrower issuers.
private let brandRules: [BrandRule] = [
BrandRule(name: "Visa", lengths: [13, 16, 19]) { $0.prefixMatch(of: #/^4/#) != nil },
BrandRule(name: "Mastercard", lengths: [16]) {
$0.prefixMatch(of: #/^(5[1-5]|2(2[2-9]|[3-6][0-9]|7[01]|720))/#) != nil
},
BrandRule(name: "American Express", lengths: [15]) { $0.prefixMatch(of: #/^3[47]/#) != nil },
BrandRule(name: "Discover", lengths: [16, 19]) {
$0.prefixMatch(of: #/^(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])))/#) != nil
},
BrandRule(name: "Diners Club", lengths: [14, 16]) {
$0.prefixMatch(of: #/^(30[0-5]|3095|36|38|39|54|55)/#) != nil
},
BrandRule(name: "JCB", lengths: [16, 17, 18, 19]) {
$0.prefixMatch(of: #/^35(2[89]|[3-8][0-9])/#) != nil
},
BrandRule(name: "UnionPay", lengths: [16, 17, 18, 19]) { $0.prefixMatch(of: #/^62/#) != nil },
BrandRule(name: "Maestro", lengths: Array(12...19)) {
$0.prefixMatch(of: #/^(50|56|57|58|6)/#) != nil
},
]
/// Detect the card brand by prefix + length. Returns nil when unknown.
func detectBrand(_ digits: String) -> String? {
guard !digits.isEmpty else { return nil }
for rule in brandRules where rule.matchesPrefix(digits) && rule.lengths.contains(digits.count) {
return rule.name
}
return nil
}
/// Slice digits into runs of the given widths, joining with spaces.
private func group(_ digits: String, _ widths: [Int]) -> String {
let chars = Array(digits)
let n = chars.count
var parts: [String] = []
var i = 0
for w in widths {
if i >= n { break }
parts.append(String(chars[i..<min(i + w, n)]))
i += w
}
// Any trailing remainder (e.g. a partially typed number) is appended as-is.
if i < n {
parts.append(String(chars[i...]))
}
return parts.joined(separator: " ")
}
/// Group the number per brand spacing rules.
func formatCard(_ digits: String, _ brand: String?) -> String {
if digits.isEmpty { 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.count == 14 {
return group(digits, [4, 6, 4])
}
// Default: groups of four separated by spaces, no trailing space
// (equivalent to the TS regex `(.{4})(?=.)` replacement).
var out = ""
for (i, ch) in digits.enumerated() {
if i > 0 && i % 4 == 0 {
out.append(" ")
}
out.append(ch)
}
return out
}
/// Keep ASCII digit characters only; multibyte UTF-8 scalars never equal "0"..."9".
private func sanitizeDigits(_ input: String) -> String {
input.filter { ("0"..."9").contains($0) }
}
/// Validate a card number. Always returns a CardInfo; never crashes.
func validateCard(_ input: String) -> CardInfo {
let sanitized = sanitizeDigits(input)
let brand = detectBrand(sanitized)
// Luhn is only meaningful once we have a plausible card length (>= 12 digits).
let luhnValid = sanitized.count >= 12 && luhnCheck(sanitized)
return CardInfo(
input: input,
sanitized: sanitized,
valid: luhnValid,
luhnValid: luhnValid,
brand: brand,
formatted: formatCard(sanitized, brand)
)
}
// Tiny showcase: validate a well-known test number (Visa).
let info = validateCard("4111 1111 1111 1111")
print("brand=\(info.brand ?? "None") valid=\(info.valid) formatted=\"\(info.formatted)\"")
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 →