Here is my solution, including unit tests. I'm pretty pleased with my
luhn check.
require 'enumerator'
class CardType < Struct.new(:name, :pattern, :lengths)
def match(cc)
(cc =~ pattern) and lengths.include?(cc.length)
end
def to_s
name
end
end
class CardValidator
@types = [
CardType.new('AMEX', /^(34|37)/, [15]),
CardType.new('Discover', /^6011/, [16]),
CardType.new('MasterCard', /^5[1-5]/, [16]),
CardType.new('Visa', /^4/, [13,16])
]
def self.card_type(cc)
@types.find {|type| type.match(cc) }
end
def self.luhn_check(cc)
# I like functional-style code (though this may be a bit over the top)
(cc.split('').reverse.enum_for(:each_slice, 2).inject('') do |s, (a, b)|
s << a + (b.to_i * 2).to_s
end.split('').inject(0) {|sum, n| sum + n.to_i}) % 10 == 0
end
end
require 'test/unit'
class CardValidatorTest < Test::Unit::TestCase
def test_card_type
assert_equal('AMEX', CardValidator.card_type('341122567979797').name)
assert_equal('AMEX', CardValidator.card_type('371122567979797').name)
assert_equal('Discover', CardValidator.card_type('6011123456781122').name)
assert_equal('MasterCard', CardValidator.card_type('5115666677779999').name)
assert_equal('MasterCard', CardValidator.card_type('5315666677779999').name)
assert_equal('Visa', CardValidator.card_type('4408041234567893').name)
assert_equal('Visa', CardValidator.card_type('4417123456789112').name)
assert_equal('Visa', CardValidator.card_type('4417123456789').name)
assert_nil(CardValidator.card_type('3411225679797973'))
assert_nil(CardValidator.card_type('601112345678112'))
assert_nil(CardValidator.card_type('51156666777799'))
assert_nil(CardValidator.card_type('5615666677779989'))
assert_nil(CardValidator.card_type('1111222233334444'))
assert_nil(CardValidator.card_type('44171234567898'))
end
def test_luhn_check
assert(CardValidator.luhn_check('1111222233334444'))
assert(CardValidator.luhn_check('4408041234567893'))
assert(!CardValidator.luhn_check('4417123456789112'))
assert(!CardValidator.luhn_check('6011484800032882'))
end
end
if $0 == __FILE__
abort("Usage: #$0 <credit card number> or -t to run unit tests") if
ARGV.length < 1
if not ARGV.delete('-t')
Test::Unit.run = true
cc = ARGV.join.gsub(/\s*/, '')
type = CardValidator.card_type(cc)
puts "Card type is: #{type ? type : 'Unknown'}"
puts "The card is #{CardValidator.luhn_check(cc) ? 'Valid' : 'Invalid'}"
end
end