Skip to content

Credit Card Validator — C++ 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 C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// credit-card-validator — Luhn checksum + brand detection for card numbers.
//
// Language: C++ (C++17, 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 main: sanitize to
// digits, run the Luhn checksum, detect the issuing brand by prefix + length,
// and group the number for display. Never throws on malformed input; mirrors
// the TypeScript contract.
//
// The brand table mirrors the TS/Python regexes with std::regex (ECMAScript
// grammar). Every pattern is "^"-anchored and matched with regex_search, so
// only the prefix is tested — exactly like Python's pattern.search().

#include <algorithm>
#include <iostream>
#include <optional>
#include <regex>
#include <string>
#include <utility>
#include <vector>

// Full validation result. `brand` is std::nullopt when the issuer is unknown
// (the C++ equivalent of TypeScript's `string | null`).
struct CardInfo {
    std::string input;
    std::string sanitized;             // digits only
    bool valid = false;                // digits present, sane length, and Luhn passes
    bool luhn_valid = false;           // Luhn checksum passes (requires >= 12 digits)
    std::optional<std::string> brand;  // issuing brand, or nullopt when unknown
    std::string formatted;             // grouped per brand spacing rules
};

// Luhn (mod-10) checksum. Empty or non-digit input returns false.
bool luhn_check(const std::string &digits) {
    static const std::regex digits_only("[0-9]*");
    if (digits.empty() || !std::regex_match(digits, digits_only)) {
        return false;
    }
    int total = 0;
    bool 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).
    for (auto it = digits.rbegin(); it != digits.rend(); ++it) {
        int d = *it - '0';
        if (should_double) {
            d *= 2;
            if (d > 9) {
                d -= 9;
            }
        }
        total += d;
        should_double = !should_double;
    }
    return total % 10 == 0;
}

// Brand detection rule: name, prefix pattern, allowed lengths.
struct BrandRule {
    const char *name;
    std::regex pattern;
    std::vector<size_t> lengths;
};

// Brand detection table. ORDER MATTERS: the first prefix+length match wins.
// Maestro is last because its broad "6" / 5x ranges overlap narrower issuers.
const std::vector<BrandRule> &brand_rules() {
    static const std::vector<BrandRule> rules = {
        {"Visa", std::regex("^4"), {13, 16, 19}},
        {"Mastercard", std::regex("^(5[1-5]|2(2[2-9]|[3-6][0-9]|7[01]|720))"), {16}},
        {"American Express", std::regex("^3[47]"), {15}},
        {"Discover",
         std::regex("^(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", std::regex("^(30[0-5]|3095|36|38|39|54|55)"), {14, 16}},
        {"JCB", std::regex("^35(2[89]|[3-8][0-9])"), {16, 17, 18, 19}},
        {"UnionPay", std::regex("^62"), {16, 17, 18, 19}},
        {"Maestro", std::regex("^(50|56|57|58|6)"), {12, 13, 14, 15, 16, 17, 18, 19}},
    };
    return rules;
}

// Detect the card brand by prefix + length. Returns nullopt when unknown.
std::optional<std::string> detect_brand(const std::string &digits) {
    if (digits.empty()) {
        return std::nullopt;
    }
    for (const auto &rule : brand_rules()) {
        if (std::regex_search(digits, rule.pattern) &&
            std::find(rule.lengths.begin(), rule.lengths.end(), digits.size()) != rule.lengths.end()) {
            return rule.name;
        }
    }
    return std::nullopt;
}

// Slice digits into runs of the given widths, joining with spaces.
std::string group(const std::string &digits, std::initializer_list<size_t> widths) {
    std::vector<std::string> parts;
    size_t i = 0;
    const size_t n = digits.size();
    for (size_t w : widths) {
        if (i >= n) {
            break;
        }
        parts.push_back(digits.substr(i, w)); // substr clamps the run at the end
        i += w;
    }
    // Any trailing remainder (e.g. a partially typed number) is appended as-is.
    if (i < n) {
        parts.push_back(digits.substr(i));
    }
    std::string out;
    for (size_t k = 0; k < parts.size(); ++k) {
        if (k > 0) {
            out += ' ';
        }
        out += parts[k];
    }
    return out;
}

// Group the number per brand spacing rules.
std::string format_card(const std::string &digits, const std::optional<std::string> &brand) {
    if (digits.empty()) {
        return "";
    }
    // Amex uses a 4-6-5 grouping; Diners Club's 14-digit variant uses 4-6-4.
    if (brand && *brand == "American Express") {
        return group(digits, {4, 6, 5});
    }
    if (brand && *brand == "Diners Club" && digits.size() == 14) {
        return group(digits, {4, 6, 4});
    }
    // Default: groups of four separated by spaces, no trailing space
    // (equivalent to the TS regex `(.{4})(?=.)` replacement).
    std::string out;
    for (size_t i = 0; i < digits.size(); ++i) {
        if (i > 0 && i % 4 == 0) {
            out += ' ';
        }
        out += digits[i];
    }
    return out;
}

// Keep ASCII digit bytes only; multibyte UTF-8 bytes are all >= 0x80, so excluded.
std::string sanitize_digits(const std::string &input) {
    std::string out;
    for (char ch : input) {
        if (ch >= '0' && ch <= '9') {
            out += ch;
        }
    }
    return out;
}

// Validate a card number. Always returns a CardInfo; never throws.
CardInfo validate_card(const std::string &input) {
    CardInfo info;
    info.input = input;
    info.sanitized = sanitize_digits(input);
    info.brand = detect_brand(info.sanitized);
    // Luhn is only meaningful once we have a plausible card length (>= 12 digits).
    info.luhn_valid = info.sanitized.size() >= 12 && luhn_check(info.sanitized);
    info.valid = info.luhn_valid;
    info.formatted = format_card(info.sanitized, info.brand);
    return info;
}

int main() {
    // Tiny showcase: validate a well-known test number (Visa).
    CardInfo info = validate_card("4111 1111 1111 1111");
    std::cout << "brand=" << (info.brand ? *info.brand : "None")
              << " valid=" << (info.valid ? "true" : "false")
              << " formatted=\"" << info.formatted << "\"\n";
    return 0;
}

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 →