Skip to content

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 →