Credit Card Validator — Java 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 Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// credit-card-validator — Luhn checksum + brand detection for card numbers.
//
// Language: Java (Java 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 regexes with java.util.regex (part of the
// JDK). For a standalone compile, save this file as CreditCardValidator.java —
// the public class name must match the file name.
import java.util.List;
import java.util.regex.Pattern;
/** Pure credit-card validation logic (Luhn + brand detection + grouping). */
public final class CreditCardValidator {
/** Full validation result. {@code brand} is null when the issuer is unknown. */
public record CardInfo(
String input,
String sanitized, // digits only
boolean valid, // digits present, sane length, and Luhn passes
boolean luhnValid, // Luhn checksum passes (requires >= 12 digits)
String brand, // issuing brand, or null when unknown
String formatted // grouped per brand spacing rules
) {}
/** Luhn (mod-10) checksum. Empty or non-digit input returns false. */
public static boolean luhnCheck(String digits) {
if (digits.isEmpty() || !digits.chars().allMatch(c -> c >= '0' && c <= '9')) {
return false; // non-digit -> invalid, matching /^[0-9]*$/
}
int total = 0;
boolean 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.charAt(i) - '0';
if (shouldDouble) {
d *= 2;
if (d > 9) {
d -= 9;
}
}
total += d;
shouldDouble = !shouldDouble;
}
return total % 10 == 0;
}
/** Brand detection rule: name, prefix pattern, allowed lengths. */
private record BrandRule(String name, Pattern pattern, List<Integer> lengths) {}
// 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 final List<BrandRule> BRAND_RULES = List.of(
new BrandRule("Visa", Pattern.compile("^4"), List.of(13, 16, 19)),
new BrandRule("Mastercard",
Pattern.compile("^(5[1-5]|2(2[2-9]|[3-6][0-9]|7[01]|720))"), List.of(16)),
new BrandRule("American Express", Pattern.compile("^3[47]"), List.of(15)),
new BrandRule("Discover",
Pattern.compile("^(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])))"),
List.of(16, 19)),
new BrandRule("Diners Club", Pattern.compile("^(30[0-5]|3095|36|38|39|54|55)"), List.of(14, 16)),
new BrandRule("JCB", Pattern.compile("^35(2[89]|[3-8][0-9])"), List.of(16, 17, 18, 19)),
new BrandRule("UnionPay", Pattern.compile("^62"), List.of(16, 17, 18, 19)),
new BrandRule("Maestro", Pattern.compile("^(50|56|57|58|6)"),
List.of(12, 13, 14, 15, 16, 17, 18, 19)));
/** Detect the card brand by prefix + length. Returns null when unknown. */
public static String detectBrand(String digits) {
if (digits.isEmpty()) {
return null;
}
for (BrandRule rule : BRAND_RULES) {
// find() with an anchored "^" pattern looks only at the prefix,
// mirroring Python's pattern.search on these anchored regexes.
if (rule.pattern().matcher(digits).find() && rule.lengths().contains(digits.length())) {
return rule.name();
}
}
return null;
}
// Slice digits into runs of the given widths, joining with spaces.
private static String group(String digits, int... widths) {
StringBuilder out = new StringBuilder();
int i = 0;
int n = digits.length();
for (int w : widths) {
if (i >= n) {
break;
}
int end = Math.min(i + w, n);
if (out.length() > 0) {
out.append(' ');
}
out.append(digits, i, end);
i += w;
}
// Any trailing remainder (e.g. a partially typed number) is appended as-is.
if (i < n) {
if (out.length() > 0) {
out.append(' ');
}
out.append(digits, i, n);
}
return out.toString();
}
/** Group the number per brand spacing rules. */
public static String formatCard(String digits, String brand) {
if (digits.isEmpty()) {
return "";
}
// Amex uses a 4-6-5 grouping; Diners Club's 14-digit variant uses 4-6-4.
if ("American Express".equals(brand)) {
return group(digits, 4, 6, 5);
}
if ("Diners Club".equals(brand) && digits.length() == 14) {
return group(digits, 4, 6, 4);
}
// Default: groups of four separated by spaces, no trailing space
// (equivalent to the TS regex `(.{4})(?=.)` replacement).
StringBuilder out = new StringBuilder();
for (int i = 0; i < digits.length(); i++) {
if (i > 0 && i % 4 == 0) {
out.append(' ');
}
out.append(digits.charAt(i));
}
return out.toString();
}
/** Validate a card number. Always returns a CardInfo; never throws. */
public static CardInfo validateCard(String input) {
String sanitized = input.replaceAll("[^0-9]+", "");
String brand = detectBrand(sanitized);
// Luhn is only meaningful once we have a plausible card length (>= 12 digits).
boolean luhnValid = sanitized.length() >= 12 && luhnCheck(sanitized);
return new CardInfo(input, sanitized, luhnValid, luhnValid, brand, formatCard(sanitized, brand));
}
public static void main(String[] args) {
// Tiny showcase: validate a well-known test number (Visa).
CardInfo info = validateCard("4111 1111 1111 1111");
String brand = info.brand() != null ? info.brand() : "None";
System.out.println("brand=" + brand + " 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 →