Skip to content

Instantly share code, notes, and snippets.

@cwagdev
Last active June 27, 2019 10:04
Show Gist options
  • Select an option

  • Save cwagdev/e66d4806c1f63fe9387a to your computer and use it in GitHub Desktop.

Select an option

Save cwagdev/e66d4806c1f63fe9387a to your computer and use it in GitHub Desktop.

Revisions

  1. cwagdev revised this gist Jul 17, 2015. 1 changed file with 71 additions and 59 deletions.
    130 changes: 71 additions & 59 deletions CreditCard.swift
    Original file line number Diff line number Diff line change
    @@ -40,6 +40,76 @@ enum CreditCardType: Printable {
    return []
    }
    }

    init(number: String) {

    if let first = number.prefixAsInt(1) where first == 4 {
    self = .Visa
    return
    }

    if let firstTwo = number.prefixAsInt(2) {
    if firstTwo == 35 {
    self = .JCB
    return
    }

    if contains([30, 36, 38, 39], firstTwo) {
    self = .DinersClub
    return
    }

    if 50...55 ~= firstTwo {
    self = .MasterCard
    return
    }

    if firstTwo == 34 || firstTwo == 37 {
    self = .Amex
    return
    }

    if firstTwo == 65 {
    self = .Discover
    return
    }
    }

    if let firstThree = number.prefixAsInt(3) where 644...649 ~= firstThree {
    self = .Discover
    return
    }

    if let firstFour = number.prefixAsInt(4) where firstFour == 6011 {
    self = .Discover
    return
    }

    if let firstSix = number.prefixAsInt(6) where 622126...622925 ~= firstSix {
    self = .Discover
    return
    }

    self = .Unknown
    }
    }

    private extension String {

    /**
    The first `length` characters of the String as an Int.

    :param: length The number of characters to return

    :returns: The first `length` characters of the String as an Int. `nil` if `length` exceed the length of the String or is not representable as Int.
    */
    func prefixAsInt(length: Int) -> Int? {
    if count(self) < length {
    return nil
    }

    return substringWithRange(startIndex..<advance(startIndex, length)).toInt()
    }
    }

    /// Represents a String as a Credit Card
    @@ -49,7 +119,7 @@ struct CreditCard {
    let number: String

    /// The type of credit card, this is generally accurate once the first two numbers are provided
    var type: CreditCardType { return cardType() }
    var type: CreditCardType { return CreditCardType(number: number) }

    /// The last 4 numbers of the card, nil if the length is < 4
    var last4: String? {
    @@ -103,62 +173,4 @@ struct CreditCard {
    init(string: String) {
    self.number = string
    }

    /**
    Return the first `length` characters in the number as an Int

    :param: length The number of characters to return as an Int

    :returns: The Int value or nil if the length is longer than the number or conversion to Int fails
    */
    private func numberPrefix(length: Int) -> Int? {
    if count(number) < length {
    return nil
    }

    return number.substringWithRange(number.startIndex..<advance(number.startIndex, length)).toInt()
    }

    private func cardType() -> CreditCardType {

    if let first = numberPrefix(1) where first == 4 {
    return .Visa
    }

    if let firstTwo = numberPrefix(2) {
    if firstTwo == 35 {
    return .JCB
    }

    if contains([30, 36, 38, 39], firstTwo) {
    return .DinersClub
    }

    if 50...55 ~= firstTwo {
    return .MasterCard
    }

    if firstTwo == 34 || firstTwo == 37 {
    return .Amex
    }

    if firstTwo == 65 {
    return .Discover
    }
    }

    if let firstThree = numberPrefix(3) where 644...649 ~= firstThree {
    return .Discover
    }

    if let firstFour = numberPrefix(4) where firstFour == 6011 {
    return .Discover
    }

    if let firstSix = numberPrefix(6) where 622126...622925 ~= firstSix {
    return .Discover
    }

    return .Unknown
    }
    }
  2. cwagdev created this gist Jul 17, 2015.
    164 changes: 164 additions & 0 deletions CreditCard.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,164 @@
    /// Describes a type of credit card for a subset of known types.
    enum CreditCardType: Printable {
    case Amex, DinersClub, Discover, JCB, MasterCard, Visa, Unknown

    var description: String {
    switch self {
    case .Amex:
    return "Amex"
    case .DinersClub:
    return "Diners Club"
    case .Discover:
    return "Discover"
    case .JCB:
    return "JCB"
    case .MasterCard:
    return "MasterCard"
    case .Visa:
    return "Visa"
    case .Unknown:
    return "Unknown"
    }
    }

    /// A set of lengths that are considered valid for the type
    var validLengths: Set<Int> {
    switch self {
    case .Amex:
    return [15]
    case .DinersClub:
    return [14, 15, 16]
    case .Discover:
    return [16]
    case .JCB:
    return [16]
    case .MasterCard:
    return [16]
    case .Visa:
    return [13, 16]
    case .Unknown:
    return []
    }
    }
    }

    /// Represents a String as a Credit Card
    struct CreditCard {

    /// The credit card number represented as a String
    let number: String

    /// The type of credit card, this is generally accurate once the first two numbers are provided
    var type: CreditCardType { return cardType() }

    /// The last 4 numbers of the card, nil if the length is < 4
    var last4: String? {
    if count(number) < 4 {
    return nil
    }

    return number.substringWithRange(advance(number.endIndex, -4)..<number.endIndex)
    }

    /// A display version of the credit card number
    var formattedString: String {
    return number
    }

    /// True when both `isValidLength` and `isValidLuhn` are true
    var isValid: Bool {
    return isValidLength && isValidLuhn
    }

    /// True when the length of the card number meets a required length for the card type
    var isValidLength: Bool {
    return contains(type.validLengths, count(number))
    }

    /// True when the Luhn algorithm https://en.wikipedia.org/wiki/Luhn_algorithm succeeds
    var isValidLuhn: Bool {
    var sum = 0
    let digitStrings = reverse(number).map { String($0) }

    for tuple in enumerate(digitStrings) {
    if let digit = tuple.element.toInt() {
    let odd = tuple.index % 2 == 1

    switch (odd, digit) {
    case (true, 9):
    sum += 9
    case (true, 0...8):
    sum += (digit * 2) % 9
    default:
    sum += digit
    }
    } else {
    return false
    }
    }

    return sum % 10 == 0
    }

    init(string: String) {
    self.number = string
    }

    /**
    Return the first `length` characters in the number as an Int

    :param: length The number of characters to return as an Int

    :returns: The Int value or nil if the length is longer than the number or conversion to Int fails
    */
    private func numberPrefix(length: Int) -> Int? {
    if count(number) < length {
    return nil
    }

    return number.substringWithRange(number.startIndex..<advance(number.startIndex, length)).toInt()
    }

    private func cardType() -> CreditCardType {

    if let first = numberPrefix(1) where first == 4 {
    return .Visa
    }

    if let firstTwo = numberPrefix(2) {
    if firstTwo == 35 {
    return .JCB
    }

    if contains([30, 36, 38, 39], firstTwo) {
    return .DinersClub
    }

    if 50...55 ~= firstTwo {
    return .MasterCard
    }

    if firstTwo == 34 || firstTwo == 37 {
    return .Amex
    }

    if firstTwo == 65 {
    return .Discover
    }
    }

    if let firstThree = numberPrefix(3) where 644...649 ~= firstThree {
    return .Discover
    }

    if let firstFour = numberPrefix(4) where firstFour == 6011 {
    return .Discover
    }

    if let firstSix = numberPrefix(6) where 622126...622925 ~= firstSix {
    return .Discover
    }

    return .Unknown
    }
    }