Skip to content

Instantly share code, notes, and snippets.

@evs

evs/README.md Secret

Created November 29, 2011 23:27
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save evs/6ca21baf77a47d72b23c to your computer and use it in GitHub Desktop.
Save evs/6ca21baf77a47d72b23c to your computer and use it in GitHub Desktop.
EDH Developer Test

Checking Credit Cards

Introduction

Develop an application using Ruby that solves the problem described below. The aim of the problem is to allow the candidate to demonstrate their skill and experience in aspects of the development process including domain modelling, object orientated design, use of language constructs and idioms and testing (unit or otherwise).

There is provided sample data to be used for testing and the candidate should be able to demonstrate their solution using the supplied data in the form of a command line interface and or unit test.

The resulting solution should be returned by email 2 days before your interview. You will be required to talk through your solution at the interview.

Problem

Before submitting a credit card to a payment gateway it's important that we run some sanity checks on the number.

A common check that is performed upfront is to validate the card type based on the starting digits and length of card number. The main patterns that we care about are as follows:

+============+=============+===============+
| Card Type  | Begins With | Number Length |
+============+=============+===============+
| AMEX       | 34 or 37    | 15            |
+------------+-------------+---------------+
| Discover   | 6011        | 16            |
+------------+-------------+---------------+
| MasterCard | 51-55       | 16            |
+------------+-------------+---------------+
| Visa       | 4           | 13 or 16      |
+------------+-------------+---------------+

All of these card types also generate numbers such that they can be validated by the Luhn algorithm, so that's the second check systems usually try. The steps are:

  1. Starting with the next to last digit and continuing with every other digit going back to the beginning of the card, double the digit
  2. Sum all doubled and untouched digits in the number. For digits greater than 9 you will need to split them and sum the independently (i.e. "10", 1 + 0).
  3. If that total is a multiple of 10, the number is valid.

For example, given the card number 4408 0412 3456 7893:

1 8 4 0 8 0 4 2 2 6 4 10 6 14 8 18 3
2 8+4+0+8+0+4+2+2+6+4+1+0+6+1+4+8+1+8+3 = 70
3 70 % 10 == 0

Thus that card is valid.

Let's try one more, 4417 1234 5678 9112:

1 8 4 2 7 2 2 6 4 10 6 14 8 18 1 2 2
2 8+4+2+7+2+2+6+4+1+0+6+1+4+8+1+8+1+2+2 = 69
3 69 % 10 != 0

This card is not valid.

Task

Your task is to write a ruby program that accepts credit card numbers. Card numbers must be passed in line by line (one set of numbers per line). The program should print the card in the following format "TYPE: NUMBERS (VALIDITY)".

Input and Output

Given the following credit cards:

4111111111111111
4111111111111
4012888888881881
378282246310005
6011111111111117
5105105105105100
5105 1051 0510 5106
9111111111111111

I would expect the following output:

VISA: 4111111111111111       (valid)
VISA: 4111111111111          (invalid)
VISA: 4012888888881881       (valid)
AMEX: 378282246310005        (valid)
Discover: 6011111111111117   (valid)
MasterCard: 5105105105105100 (valid)
MasterCard: 5105105105105106 (invalid)
Unknown: 9111111111111111    (invalid)
@ygpark2
Copy link

ygpark2 commented Dec 5, 2013

This is OOP style solution.

But I want to think of this issue in functional style again.

class Card
  attr_accessor :card_number, :start_digit, :number_length

  def card_type
    self.check_start and self.check_length
  end

  def check_valid
    @card_number.chars.map(&:to_i).reverse.each_with_index.map { |x, idx| (idx % 2 == 0) ? x : x * 2  }.join("").chars.map(&:to_i).inject(0, :+) % 10 == 0
  end

  def check_start
    card_number.strip.start_with? *@start_digit
  end

  def check_length
    @number_length.include? card_number.strip.delete(' ').length
  end

end

class AMEX < Card
  def initialize
    @start_digit = ["34", "37"]
    @number_length = [15]
  end
end

class Discover < Card
  def initialize
    @start_digit = ["6011"]
    @number_length = [16]
  end
end

class MasterCard < Card
  def initialize
    @start_digit = (51..55).to_a.map(&:to_s)
    @number_length = [16]
  end
end

class VISA < Card
  def initialize
    @start_digit = ["4"]
    @number_length = [13, 16]
  end
end

class Unknown < Card
  def initialize

  end

  def card_type
    true
  end

  def check_valid
    false
  end
end

amex = AMEX.new
discover = Discover.new
masterCard = MasterCard.new
visa = VISA.new
unknown = Unknown.new

puts "Enter card number : "
while line = STDIN.gets
  break if line.chomp == 'quit'
  [amex, discover, masterCard, visa, unknown].each{ |c|
    c.card_number = line.strip
    if c.card_type
      valid_chk_msg = c.check_valid ? "(valid)" : "(invalid)"
      p c.class.name + " : " + c.card_number + " " + valid_chk_msg
      break
    end
  }
  puts "Enter another card number or 'quit' to exit : "
end

@ygpark2
Copy link

ygpark2 commented Dec 5, 2013

This is functional style solution of which I can think

class Proc
  def **(other)
    Proc.new { |x| self.call(x) and other.call(x) }
  end
end

card_start = Proc.new { |start_digit, card_num| card_num.strip.start_with? *start_digit }
card_length = Proc.new { |number_length, card_num| number_length.include? card_num.strip.delete(' ').length }

amex_card_start = card_start.curry[["34", "37"]]
amex_card_length = card_length.curry[[15]]

amex_card_type = amex_card_start ** amex_card_length

discover_card_start = card_start.curry[["6011"]]
discover_card_length = card_length.curry[[16]]

discover_card_type = discover_card_start ** discover_card_length

mastercard_card_start = card_start.curry[(51..55).to_a.map(&:to_s)]
mastercard_card_length = card_length.curry[[16]]

mastercard_card_type = mastercard_card_start ** mastercard_card_length

visa_card_start = card_start.curry[["4"]]
visa_card_length = card_length.curry[[13, 16]]

visa_card_type = visa_card_start ** visa_card_length

card_type = ->(card_num) {
 case card_num
   when amex_card_type
    "AMEX"
  when discover_card_type
    "Discover"
  when mastercard_card_type
    "MasterCard"
  when visa_card_type
    "Visa"
  else
    "Unknown"
 end
}

check_valid = ->(card_num) {
  card_num.chars.map(&:to_i).reverse.each_slice(2).map{ |n| n.size > 1 ?  [n[0], n[1] * 2] : [n[0]]}.flatten.join("").chars.map(&:to_i).inject(0, :+) % 10 == 0 ? "valid" : "invalid"
}

card_numbers = [
                "4111111111111111",
                "4111111111111",
                "4012888888881881",
                "378282246310005",
                "6011111111111117",
                "5105105105105100",
                "5105 1051 0510 5106",
                "9111111111111111"
]

card_numbers.each{ |c|
  p (card_type.call c) << " : " << c << " (" << (check_valid.call c) << ")"
}

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