Credit Card Regex in Java: Brand Check and Luhn Validation

Credit card regex for Java with Visa, Mastercard 2-series, Amex, Discover, Diners and JCB, brand detection with named groups and a Luhn check.

Credit card validation in Java in three steps, input cleanup, brand regex and Luhn checksum, with the result for test numbers at each step

Credit card number validation in Java takes three checks, namely that the input contains only digits and separators, that the number has a prefix and length that a card network uses, and that the last digit passes the Luhn checksum. A credit card regex handles the first two checks and also tells us the card brand. The Luhn checksum is arithmetic, so it needs a few lines of Java code.

We use these checks in checkout forms to catch typing errors before the payment request, and to show the card logo while the user types. All card numbers shown are the sandbox test numbers that Cybersource, a Visa company, publishes for developers, and they cannot be charged.

The following example runs the three checks on test numbers with the methods of the class CreditCards, which are built step by step in section 2 to section 6.

Optional<String> digits = CreditCards.digitsOnly("4111 1111 1111 1111");   // Optional[4111111111111111]
boolean known = CreditCards.CARD.matcher("4111111111111111").matches();    // true

Optional<String> master = CreditCards.brand("5555555555554444");           // Optional[mastercard]
Optional<String> series2 = CreditCards.brand("2222420000001113");          // Optional[mastercard], 2-series
Optional<String> amex = CreditCards.brand("378282246310005");              // Optional[amex]

boolean luhnOk = CreditCards.passesLuhn("4111111111111111");               // true
boolean luhnBad = CreditCards.passesLuhn("4111111111111112");              // false
boolean valid = CreditCards.isValidCardNumber("4111-1111-1111-1111");      // true

Notice that the brand regex alone cannot reject “4111111111111112”, because only the Luhn check looks at the last digit. We go through the prefixes of each brand, the brand regex with named groups, the input cleanup, the Luhn algorithm and the complete validator. Readers new to character classes and groups can start with our introduction to Java regex.

1. Prefixes and Lengths by Card Brand

A card number (also called the PAN, primary account number) is 8 to 19 digits long. Its first digits, the issuer identification number (IIN), identify the network and the issuing bank, and the last digit is a check digit. The numbering follows the ISO/IEC 7812 standard.

The prefixes in the table come from the IIN table in the Wikipedia article on payment card numbers. Networks add ranges from time to time, so a brand regex needs a review whenever a network announces a new range.

BrandStarts withLength
Visa413, 16 or 19
Mastercard51 to 55, or 2221 to 272016
American Express34 or 3715
Discover6011, 644 to 649, or 6516 to 19
Diners Club International300 to 305, 36 or 3814
JCB3528 to 358916 to 19

The Diners Club row covers the classic 14-digit numbers only. Wikipedia lists more prefixes and lengths for Diners Club, and in the US and Canada, Diners Club cards use Mastercard numbers.

2. A Regex for Each Card Brand

The pattern runs on digits only, after spaces and hyphens are removed as described in section 4. It has one alternative per brand, and each alternative is a named group, written (?<name>…), so that we can later ask which brand matched. The class CreditCards holds every pattern and method of this article, and the example project adds JUnit tests for each range.

final class CreditCards {
    static final Pattern CARD = Pattern.compile(
        "^(?:(?<visa>4[0-9]{12}(?:[0-9]{3}){0,2})"
            + "|(?<mastercard>(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12})"
            + "|(?<amex>3[47][0-9]{13})"
            + "|(?<discover>(?:6011|64[4-9][0-9]|65[0-9]{2})[0-9]{12,15})"
            + "|(?<diners>3(?:0[0-5]|[68][0-9])[0-9]{11})"
            + "|(?<jcb>35(?:2[89]|[3-8][0-9])[0-9]{12,15}))$");

    static final Pattern ALLOWED_INPUT = Pattern.compile("^[0-9 -]+$");

    static Optional<String> digitsOnly(String input) {
        if (input == null || !ALLOWED_INPUT.matcher(input.strip()).matches()) {
            return Optional.empty();
        }
        return Optional.of(input.replaceAll("[ -]", ""));
    }

    static Optional<String> brand(String digits) {
        Matcher m = CARD.matcher(digits);
        if (!m.matches()) {
            return Optional.empty();
        }
        return m.namedGroups().keySet().stream()
            .filter(name -> m.group(name) != null)
            .findFirst();
    }

    static boolean passesLuhn(String digits) {
        int sum = 0;
        boolean doubleIt = false;
        for (int i = digits.length() - 1; i >= 0; i--) {
            int d = digits.charAt(i) - '0';
            if (doubleIt) {
                d *= 2;
                if (d > 9) {
                    d -= 9;
                }
            }
            sum += d;
            doubleIt = !doubleIt;
        }
        return sum % 10 == 0;
    }

    static boolean isValidCardNumber(String input) {
        return digitsOnly(input)
            .filter(d -> CARD.matcher(d).matches())
            .filter(CreditCards::passesLuhn)
            .isPresent();
    }
}

Each alternative of CARD encodes the prefix and the length from the table in section 1.

GroupRegexHow it reads
visa4[0-9]{12}(?:[0-9]{3}){0,2}4, followed by 12 digits and zero, one or two blocks of 3, so 13, 16 or 19 digits
mastercard(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}A four-digit prefix from 5100 to 5599 or 2221 to 2720, followed by 12 digits
amex3[47][0-9]{13}34 or 37, followed by 13 digits
discover(?:6011|64[4-9][0-9]|65[0-9]{2})[0-9]{12,15}6011, 644x to 649x or 65xx, followed by 12 to 15 digits
diners3(?:0[0-5]|[68][0-9])[0-9]{11}300 to 305, 36x or 38x, followed by 11 digits
jcb35(?:2[89]|[3-8][0-9])[0-9]{12,15}3528 to 3589, followed by 12 to 15 digits

The Mastercard 2-series range is the hardest part, because a regex cannot compare numbers. A numeric range such as 2221 to 2720 must be split into pieces that each fit a character class, here 2221 to 2229, 2230 to 2299, 2300 to 2699, 2700 to 2719 and 2720.

boolean visa = CreditCards.CARD.matcher("4622943127013705").matches();       // true, Visa
boolean series2 = CreditCards.CARD.matcher("2222630000001125").matches();    // true, Mastercard 2-series
boolean discover = CreditCards.CARD.matcher("6011111111111117").matches();   // true, Discover
boolean jcb = CreditCards.CARD.matcher("3566111111111113").matches();        // true, JCB
boolean edge2720 = CreditCards.CARD.matcher("2720990000000000").matches();   // true, last 2-series prefix
boolean edge2721 = CreditCards.CARD.matcher("2721000000000000").matches();   // false, outside the range

3. Detecting the Card Brand

After a successful match, one named group holds text and the others are null. Since Java 20, Matcher.namedGroups() returns the group names of the pattern, so the method brand() finds the matching brand without listing the names a second time.

Optional<String> visa = CreditCards.brand("4111111111111111");       // Optional[visa]
Optional<String> master = CreditCards.brand("2222420000001113");     // Optional[mastercard]
Optional<String> discover = CreditCards.brand("6011111111111117");   // Optional[discover]
Optional<String> jcb = CreditCards.brand("3566111111111113");        // Optional[jcb]
Optional<String> zeros = CreditCards.brand("0000000000000000");      // Optional.empty

Say a ticket shop accepts Visa and Mastercard only. While the customer types, the shop shows the card logo from brand(), and it rejects an American Express number before the customer fills in the rest of the form. To accept fewer brands in the pattern itself, we delete their alternatives, and we check that no empty alternative is left behind, because a stray | at the end of the group lets the empty string match.

3.1. Two Bugs in a Widely Copied Pattern

Many tutorials and libraries still use an older one-line pattern that predates the Mastercard 2-series. It has two problems today.

  • Its Mastercard branch knows only the 51 to 55 prefixes, so it rejects every 2-series card.
  • Its Diners Club branch, 3(?:0[0-5]|[68][0-9])?[0-9]{11}, has a ? after the prefix group. That makes the prefix optional, so any 12-digit number that starts with 3 passes.
Pattern oldCard = Pattern.compile("^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}"
    + "|3(?:0[0-5]|[68][0-9])?[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35[0-9]{3})[0-9]{11})$");

boolean oldSeries2 = oldCard.matcher("2222420000001113").matches();             // false, Mastercard 2-series rejected
boolean newSeries2 = CreditCards.CARD.matcher("2222420000001113").matches();    // true
boolean oldTwelve = oldCard.matcher("312345678901").matches();                  // true, 12 digits accepted as Diners
boolean newTwelve = CreditCards.CARD.matcher("312345678901").matches();         // false

The older JCB branch also accepts the 2131 and 1800 prefixes, which the current IIN table no longer lists for JCB, and its Visa and Discover branches do not allow 19-digit numbers.

4. Cleaning the Input

Card numbers are printed in groups, and people type them with spaces or hyphens, such as “4111 1111 1111 1111” or “3782 822463 10005” for an American Express card. The method digitsOnly() first checks that the input contains only digits, spaces and hyphens, and removes the separators after that.

Optional<String> hyphens = CreditCards.digitsOnly("4111-1111-1111-1111");    // Optional[4111111111111111]
Optional<String> amex = CreditCards.digitsOnly(" 3782 822463 10005 ");        // Optional[378282246310005]
Optional<String> dots = CreditCards.digitsOnly("4111.1111.1111.1111");        // Optional.empty, dots
Optional<String> letterO = CreditCards.digitsOnly("4111 1111 1111 111O");     // Optional.empty, letter O
Optional<String> missing = CreditCards.digitsOnly(null);                      // Optional.empty

We reject other characters instead of deleting everything that is not a digit. Removing the letter from “4111 1111 1111 111O” would turn a typo into a different 15-digit number.

5. The Luhn Checksum in Java

The Luhn algorithm, also called the mod 10 algorithm, detects typing errors. A regex cannot check it, because the check digit depends on the sum of all the other digits. The rule works from right to left.

  1. Keep the rightmost digit (the check digit) as it is.
  2. Double every second digit, moving left. If the result is greater than 9, subtract 9.
  3. Add all the digits. The number is valid when the sum is divisible by 10.

For the American Express test number 378282246310005, the doubled digits are in the 2nd, 4th, 6th and later positions from the right.

Digit378282246310005
Doubled?yesyesyesyesyesyesyes
Value358484286610005

The values add up to 60, which is divisible by 10, so the number passes. The 7 became 5 because 7 x 2 = 14 and 14 – 9 = 5. The method passesLuhn() in section 2 implements these steps with a flag that flips on every digit.

boolean amex = CreditCards.passesLuhn("378282246310005");      // true
boolean changed = CreditCards.passesLuhn("378282246310006");   // false, last digit changed
boolean swapped = CreditCards.passesLuhn("372882246310005");   // false, 8 and 2 swapped
boolean zeros = CreditCards.passesLuhn("0000000000000000");    // true, but no brand matches
Credit card validation in Java in three steps, input cleanup, brand regex and Luhn checksum, with the result for test numbers at each step
Each step catches a different kind of error, and a number must pass all three

The Luhn check catches every single wrong digit and most swaps of two neighboring digits. The last line shows why it is not enough on its own, because sixteen zeros pass the checksum and only the brand regex rejects them. ISBNs use a similar weighted checksum, shown in validating ISBN-10 and ISBN-13 in Java.

6. Putting the Checks Together

The method isValidCardNumber() chains the three steps with Optional. It cleans the input, matches a brand pattern and runs the Luhn check.

boolean master = CreditCards.isValidCardNumber("5555-5555-5555-4444");   // true
boolean discover = CreditCards.isValidCardNumber("6011111111111117");    // true
boolean luhn = CreditCards.isValidCardNumber("4111111111111112");        // false, Luhn fails
boolean zeros = CreditCards.isValidCardNumber("0000000000000000");       // false, no brand
boolean tooShort = CreditCards.isValidCardNumber("4111 1111");           // false, too short

A number that passes all three checks is well formed. It can still be expired, blocked or belong to someone else, and only an authorization request through a payment provider answers that. Client-side validation exists to catch typing errors early, before the payment request.

Card numbers are also regulated data. The PCI Security Standards Council sets the rules for storing and displaying them, and in practice most applications pass the number to a payment provider and never store it.

7. Credit Card Regex FAQs

Checkout forms bring up these questions about card numbers.

7.1. Can a Regex Check the Luhn Digit?

No. The check digit depends on the sum of all other digits, and a regex cannot add numbers. We run the regex for prefix and length, and a few lines of Java for the checksum.

7.2. Do Test Card Numbers Pass the Luhn Check?

Yes. Published test numbers such as 4111111111111111 are built to pass the brand and Luhn checks, so they work in sandbox payment flows, but a live payment provider declines them.

7.3. How Do We Mask a Card Number for Display?

We keep the last four digits and replace the rest, which PCI DSS allows for display.

String masked = "4111111111111111".replaceAll("\\d(?=\\d{4})", "*");   // "************1111"

7.4. Should We Validate the CVV and Expiry Date With a Regex Too?

Only the format. A CVV is 3 digits, or 4 for American Express, so ^[0-9]{3,4}$ is enough, and the expiry date is best parsed into a YearMonth and compared with the current month. The payment provider checks whether both values are correct.

7.5. Why Does a 19-Digit Visa Number Fail With Older Patterns?

Older patterns allow only 13 or 16 digits for Visa. Visa issues 19-digit numbers as well, so (?:[0-9]{3}){0,2} in section 2 accepts 13, 16 and 19 digits.

8. Conclusion

Validating a credit card number is three separate checks. A character check and a separator cleanup handle user input, a regex with one named group per brand checks the prefix and length and tells us the brand, and the Luhn algorithm in a few lines of Java catches typing errors.

Prefix ranges change over time, as the Mastercard 2-series showed, so the brand pattern needs an occasional review against the current IIN table. None of these checks proves that a card can be charged, which only the payment provider can tell us.

9. References

Happy Learning !!

Source Code on Github

Leave a Comment

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.