Skip to content

Instantly share code, notes, and snippets.

@igorcferreira
Last active March 6, 2017 14:12
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save igorcferreira/6779d56da9878f991bb42d88a58757ac to your computer and use it in GitHub Desktop.
Save igorcferreira/6779d56da9878f991bb42d88a58757ac to your computer and use it in GitHub Desktop.
Basic implementation to apply different regexes over a card number String and find the card type
import Foundation
enum CardTypeError:Error {
case TypeNotFound
}
enum CardType:CustomStringConvertible {
case visa
case mastercard
case amex
case diners
case elo
case jcb
fileprivate var regex:String {
switch self {
case .visa:
return "^4[0-9]{6,}$"
case .mastercard:
return "^5[0-9]{6,}$"
case .amex:
return "^3[47][0-9]{13}$"
case .diners:
return "^3(?:0[0-5]|[68][0-9])[0-9]{11}$"
case .elo:
return "^[456](?:011|38935|51416|576|04175|067|06699|36368|36297)\\d{10}(?:\\d{2})?$"
case .jcb:
return "^(?:2131|1800|35\\d{3})\\d{11}$"
}
}
var description: String {
switch self {
case .visa:
return "Visa"
case .mastercard:
return "Mastercard"
case .amex:
return "American Express"
case .diners:
return "Diners"
case .elo:
return "Elo"
case .jcb:
return "JCB"
}
}
init(cardNumber:String) throws {
self = try [CardType.visa, .mastercard, .amex, .diners, .elo, .jcb].type(of: cardNumber)
}
func includes(_ cardNumber:String) -> Bool {
let predicate = NSPredicate(format: "SELF MATCHES [c] %@", regex)
let result = predicate.evaluate(with: cardNumber)
return result
}
}
extension Sequence where Iterator.Element == CardType {
func type(of input:String) throws -> Iterator.Element {
guard let cardType = self.lazy.first(where: { $0.includes(input) }) else {
throw CardTypeError.TypeNotFound
}
return cardType
}
}
let cardNumber = "4172368939385121"
let supportedTypes = [CardType.visa, .mastercard, .amex, .diners, .elo, .jcb]
if let cardType = supportedTypes.lazy.first(where: { $0.includes(cardNumber) }) {
print("Print card type: \(cardType)") //Prints Visa
}
//: Or Even
if let cardType = try? CardType(cardNumber: cardNumber) {
print("Print card type: \(cardType)") //Prints Visa
}
@chrisfsampaio
Copy link

chrisfsampaio commented Jan 12, 2017

@igorcferreira, here's how I'd do it (if you're still interested):

import Foundation

//: Basic implementation to apply different regexes over a card number String and find the card type
//: This code will crash if run in the Playground. As today (Xcode Version 8.2.1 (8C1002))
enum CardType: CustomStringConvertible {
    case visa
    case mastercard
    case amex
    case diners
    case elo
    case jcb

    var description: String {
        switch self {
        case .visa:
            return "Visa"
        case .mastercard:
            return "Mastercard"
        case .amex:
            return "American Express"
        case .diners:
            return "Diners"
        case .elo:
            return "Elo"
        case .jcb:
            return "JCB"
        }
    }

    fileprivate var regex: String {
        switch self {
        case .visa: return "^4[0-9]{6,}$"
        case .mastercard: return "^5[0-9]{6,}$"
        case .amex: return "^3[47][0-9]{13}$"
        case .diners: return "^3(?:0[0-5]|[68][0-9])[0-9]{11}$"
        case .elo: return "^[456](?:011|38935|51416|576|04175|067|06699|36368|36297)\\d{10}(?:\\d{2})?$"
        case .jcb: return "^(?:2131|1800|35\\d{3})\\d{11}$"
        }
    }

    init(cardNumber: String) throws {
        let allowedTypes: [CardType] = [.visa, .mastercard, .amex, .diners, .elo, .jcb]
        guard let type = allowedTypes.lazy.first(where: cardNumber.validateWith(cardType:)) else {
            throw Error.TypeNotFound
        }
        self = type
    }
}

extension CardType {
    enum Error: Swift.Error {
        case TypeNotFound
    }
}

fileprivate extension String {
    func validateWith(cardType: CardType) -> Bool {
        let predicate = NSPredicate(format: "SELF MATCHES [c] %@", cardType.regex)
        let result = predicate.evaluate(with: cardNumber)
        return result
    }
}

let cardNumber = "4172368939385121"
let cardType = try? CardType(cardNumber: cardNumber)
print("\(cardType)")

let fooSupportedTypes: Set<CardType> = [.visa, .mastercard, .amex, .diners, .elo, .jcb]
let barSupportedTypes: Set<CardType> = [.mastercard, .amex, .diners, .elo, .jcb]
let cardIsSupportedInFoo = cardType.map(fooSupportedTypes.contains)
let cardIsSupportedInBar = cardType.map(barSupportedTypes.contains)
print("\(cardIsSupportedInFoo)")
print("\(cardIsSupportedInBar)")

The main upshots are:

  • Removing CardType: String inheritance, it was kind of weird being able to initialize a CardType with a rawValue that was actually a regex.
  • Using an initializer, which is the most preferred way of constructing instance whenever possible.

@igorcferreira
Copy link
Author

@chrisfsampaio Based on your suggestion, I removed the String inheritance. It really do not make sense to init a rawValue

@klevison
Copy link

My version:

enum CardTypeError: Error {
    case brandNotFound
}

enum CardResult {
    case success(CardProtocol)
    case failure(CardTypeError)
}

protocol CardProtocol {
    var regex: String { get }
    func validCardNumber(_ cardNumber: String) -> Bool
}

extension CardProtocol {
    
    func validCardNumber(_ cardNumber: String) -> Bool {
        let predicate = NSPredicate(format: "SELF MATCHES [c] %@", regex)
        let result = predicate.evaluate(with: cardNumber)
        
        return result
    }
    
}

extension Array where Element: CardProtocol {
    func brand(of cardNumber: String) -> CardResult {
        guard let cardType = self.lazy.first(where: { $0.validCardNumber(cardNumber) }) else {
            return .failure(.brandNotFound)
        }
        return .success(cardType)
    }
}

enum CardType: CardProtocol, CustomStringConvertible {
    case visa
    case mastercard
    case amex
    case diners
    case elo
    case jcb
    case unknown
    
     var regex: String {
        switch self {
            case .visa:
                return "^4[0-9]{6,}$"
            case .mastercard:
                return "^5[0-9]{6,}$"
            case .amex:
                return "^3[47][0-9]{13}$"
            case .diners:
                return "^3(?:0[0-5]|[68][0-9])[0-9]{11}$"
            case .elo:
                return "^[456](?:011|38935|51416|576|04175|067|06699|36368|36297)\\d{10}(?:\\d{2})?$"
            case .jcb:
                return "^(?:2131|1800|35\\d{3})\\d{11}$"
            default:
                return ""
        }
    }
    
    var description: String {
        switch self {
            case .visa:
                return "Visa"
            case .mastercard:
                return "Mastercard"
            case .amex:
                return "American Express"
            case .diners:
                return "Diners"
            case .elo:
                return "Elo"
            case .jcb:
                return "JCB"
            default:
                return "Unknown"
        }
    }
    
    init(number: String) {
        let cards: [CardType] = [.visa, .mastercard, .amex, .diners, .elo, .jcb]
        let result = cards.brand(of: number)
        switch result {
            case .success(let card):
                self = card as! CardType
            case .failure:
                self = .unknown
        }
    }

}

struct CreditCard {
    let brand: CardType
    
    init(number: String) {
        self.brand = CardType(number: number)
    }
}

let creditCard = CreditCard(number: "5126076601851353")
creditCard.brand.description

if creditCard.brand == .mastercard {
    print("Mastercard")
}

@fpg1503
Copy link

fpg1503 commented Mar 6, 2017

I also added Hipercard ("^(606282\\d{10}(\\d{3})?)|(3841\\d{15})$").

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment