From the blog

Cleaner Credit Card Detection in Swift

When developing in Swift, I will often find myself wondering, “is there a better way to express this?” More often than not, there is a Swift language feature that allows for clearer expression. I encountered this recently when working on credit card type detection and validation.

Rather than making your users enter their card number and specify Visa/Discover/MasterCard/American Express, you can detect the type from the number. Auto-type detection is a nice UX touch for making a form feel responsive and quickly informing the user of validation issues. For example, you could tell them, “Stop inputting that Discover card, since we don’t accept it.”

Card detection rules are pretty straightforward:

  • The first six digits of a card number make up the Issuer Identification Number (IIN).
  • Issuing Networks are allocated specific ranges of IINs.
  • Issuing Networks have specific valid card lengths.

Wikipedia nicely organizes these requirements into a simple table. Consider four major card providers:

Issuing Network IIN ranges Card Number Length
American Express 34, 37 15
MasterCard 51–55,
2221–2720
16
Visa 4 13, 16, 19
Discover 65,
6011,
644–649,
622126–622925
16, 19

In order to extract card type, we just match the prefix of the in-progress input against the known IIN ranges. Easy, right?

Well…sort of. It’s the organization that can get a bit hairy.

Option 1: The big ol’ if statement

We first try brute force:

“If it starts with a 4, it’s a Visa. Otherwise, If it starts with a 3 it might be Amex, Diners, or JCB. If it starts with a 6 it could be Discover…”

Consider this exerpt from Stripe’s PaymentKit, which I have converted to Swift for this example:

What we’re left with is an unruly method with validation requirements strewn between String, Int, and // inline comments. Some requirements are far from their returned identifying type. Maintenance looks like could be a bit laborious, especially when adding issuers or when issuers add IIN ranges (like MasterCard did in October).

Option 2: The big ol’ NSRegularExpression

Instead of the big ol’ if statement, we move to neatly connect the validation requirements with their corresponding card type. So we whip up a var regexPattern: String {...}:

Cool. .amex is looking okay. But then we add in .masterCard:

Blehhh.

NSRegularExpression is not a great tool for expressing numerical ranges, let alone multiple variable-length numerical ranges. The comment is concise and human-readable. The pattern is not.

A More Expressive Option

This is one of those “Swift moments.” There must be a better way to express this; the Wikipedia table above is so nice and clear cut. Let’s forget about the implementation for a second and just daydream about the contract we want. Consider the comment in the regex example above:

  • starts with 51 through 55 or 2221 through 2720.
  • has 16 digits.

That’s all we’re trying to express, right? Can we get our code to read that way?
Let’s type out some pseudocode and ignore the scorn of the compiler:

Finally. Beautiful. Our validation rules live in one clear, concise, and unified place. So, how do we make it work? Let’s dive in. You can follow along with the code here: CardParser.swift.

Expressive Enums

Something like CardType is screaming for an enum. Swift’s powerful enums can encase both the cases and their validation/formatting rules:

This enables a nice contract:

Expressive Protocols

We want to store our prefixes of type String and prefix ranges of type ClosedRange<String> in a usable and type-safe array, e.g. ["65", "622126"..."622925"]

Let’s define a protocol:

and let’s sketch out our expected behavior

This will require adding protocol conformance with extensions on both String and ClosedRange.

String conformance is very straightforward: Check if either string is a prefix of the other.

Expressive Pattern Matching

For ClosedRange, we’ll lean on Swift’s expressive pattern matching syntax:

The caveat here is that we’ll have to make sure to trim the input and the range’s upperBound and lowerBound to matching lengths.

The conformance to PrefixContainable for both String and ClosedRange allows us to store our prefix validation requirements in an Array<PrefixContainable>

Putting it all together

We can organize our validation into an internal struct:

Now, our pretty validation definitions can go in a computed var

We’ll publicly expose a prefix validation:

Now, to find the matching CardType for a given input, we can query

Great! But how do we handle zero or multiple matches?

Enums with Associated Values

We may be tempted to add extra cases:

But that feels a little wrong, doesn’t it? If a card type could be either .amex or .diners based on its prefix, we lose that information by returning .indeterminate. What if we don’t allow either? We could inform the user.

Let’s encapsulate state in its own enum

and we’ll make CardState our point of entry:

Consuming CardState

Finally, we can cleanly consume the contract on the client side:

Or, if we only care about its validity:

Voila! Now we have the information we need to create an inviting and responsive credit card form.

Conclusions

Swift is a powerful language, both in terms of performance and expression. It is entirely possible (in fact, common) to go a down deep rabbit hole here and there. Sometimes, it helps to take a step back and consider the contract for the consuming or maintaining developer, and design from there.

Check out CardParser.Swift on our Github.

CardParser.swift houses both the CardType and CardState enums, and can be integrated into your project.

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *