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# 12 / .NET 8, 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 regexes with System.Text.RegularExpressions
// (part of the .NET base class library).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace CreditCardValidator;

/// <summary>Full validation result. <see cref="Brand"/> is null when the issuer is unknown.</summary>
public sealed record CardInfo(
    string Input,
    string Sanitized,             // digits only
    bool Valid,                   // digits present, sane length, and Luhn passes
    bool LuhnValid,               // Luhn checksum passes (requires >= 12 digits)
    string? Brand,                // issuing brand, or null when unknown
    string Formatted);            // grouped per brand spacing rules

/// <summary>Pure credit-card validation logic (Luhn + brand detection + grouping).</summary>
public static class CreditCard
{
    /// <summary>Luhn (mod-10) checksum. Empty or non-digit input returns false.</summary>
    public static bool LuhnCheck(string digits)
    {
        if (digits.Length == 0 || !Regex.IsMatch(digits, "^[0-9]*$"))
        {
            return false;
        }
        int total = 0;
        bool 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 (int i = digits.Length - 1; i >= 0; i--)
        {
            int d = digits[i] - '0';
            if (shouldDouble)
            {
                d *= 2;
                if (d > 9)
                {
                    d -= 9;
                }
            }
            total += d;
            shouldDouble = !shouldDouble;
        }
        return total % 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.
    private static readonly (string Name, Regex Pattern, int[] Lengths)[] BrandRules =
    {
        ("Visa", new Regex("^4"), new[] { 13, 16, 19 }),
        ("Mastercard", new Regex("^(5[1-5]|2(2[2-9]|[3-6][0-9]|7[01]|720))"), new[] { 16 }),
        ("American Express", new Regex("^3[47]"), new[] { 15 }),
        ("Discover",
            new 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])))"),
            new[] { 16, 19 }),
        ("Diners Club", new Regex("^(30[0-5]|3095|36|38|39|54|55)"), new[] { 14, 16 }),
        ("JCB", new Regex("^35(2[89]|[3-8][0-9])"), new[] { 16, 17, 18, 19 }),
        ("UnionPay", new Regex("^62"), new[] { 16, 17, 18, 19 }),
        ("Maestro", new Regex("^(50|56|57|58|6)"), new[] { 12, 13, 14, 15, 16, 17, 18, 19 }),
    };

    /// <summary>Detect the card brand by prefix + length. Returns null when unknown.</summary>
    public static string? DetectBrand(string digits)
    {
        if (digits.Length == 0)
        {
            return null;
        }
        foreach (var (name, pattern, lengths) in BrandRules)
        {
            if (pattern.IsMatch(digits) && lengths.Contains(digits.Length))
            {
                return name;
            }
        }
        return null;
    }

    // Slice digits into runs of the given widths, joining with spaces.
    private static string Group(string digits, params int[] widths)
    {
        var parts = new List<string>();
        int i = 0;
        foreach (int w in widths)
        {
            if (i >= digits.Length)
            {
                break;
            }
            parts.Add(digits.Substring(i, Math.Min(w, digits.Length - i)));
            i += w;
        }
        // Any trailing remainder (e.g. a partially typed number) is appended as-is.
        if (i < digits.Length)
        {
            parts.Add(digits[i..]);
        }
        return string.Join(" ", parts);
    }

    /// <summary>Group the number per brand spacing rules.</summary>
    public static string FormatCard(string digits, string? brand)
    {
        if (digits.Length == 0)
        {
            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 Regex.Replace(digits, "(.{4})(?=.)", "$1 ").Trim();
    }

    /// <summary>Validate a card number. Always returns a CardInfo; never throws.</summary>
    public static CardInfo ValidateCard(string input)
    {
        string sanitized = Regex.Replace(input, "[^0-9]+", "");
        string? brand = DetectBrand(sanitized);
        // Luhn is only meaningful once we have a plausible card length (>= 12 digits).
        bool luhnValid = sanitized.Length >= 12 && LuhnCheck(sanitized);
        return new CardInfo(
            Input: input,
            Sanitized: sanitized,
            Valid: luhnValid,
            LuhnValid: luhnValid,
            Brand: brand,
            Formatted: FormatCard(sanitized, brand));
    }
}

public static class Program
{
    public static void Main()
    {
        // Tiny showcase: validate a well-known test number (Visa).
        var info = CreditCard.ValidateCard("4111 1111 1111 1111");
        Console.WriteLine($"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 →