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 →