From: Sebastian Hungerecker on
Ruby Quiz wrote:
> This week's Ruby Quiz is to write a program that accepts a credit card
> number as a command-line argument. The program should print the card's
> type (or Unknown) as well a Valid/Invalid indication of whether or not the
> card passes the Luhn algorithm.

I'm rather new to ruby and this is the first time I participated in this quiz.
If found this one to be rather easy and fun to work with.
Here's my solution:

#!/usr/bin/env ruby

class CreditCard
attr_reader :number
CardTypes = [
{ :name => "AMEX", :regex => /(34|37)\d{13}/, :luhn => true},
{ :name => "Bankcard", :regex => /5610\d{12}/, :luhn => true},
{ :name => "Bankcard", :regex => /56022[1-5]\d{10}/, :luhn => true},
{ :name => "China Union Pay", :regex => /622\d{13}/, :luhn => false},
{ :name => "DC-CB", :regex => /30[0-5]\d{11}/, :luhn => true},
{ :name => "DC-eR", :regex => /2(014|149)\d{11}/, :luhn => false},
{ :name => "DC-Int", :regex => /36\d{12}/, :luhn => true},
{ :name => "DC-UC or MasterCard", :regex => /55\d{14}/, :luhn => true},
{ :name => "Discover", :regex => /6011\d{12}/, :luhn => true},
{ :name => "MasterCard", :regex => /5[1-4]\d{14}/, :luhn => true},
{ :name =>"Maestro", :regex => /(5020|5038|6759)\d{12}/, :luhn => true},
{ :name => "Visa", :regex => /4(\d{13}|\d{16})/, :luhn => true},
{ :name => "Unknown", :regex => //, :luhn => true} ]
# If the credit card is of unknown type, we'll just assume
# that it can be verified using the Luhn algorithm.

def initialize(num)
self.number=num
end

def number=(num)
raise ArgumentError, "Supplied argument is not a number" unless
num.to_s =~ /^[-_\s\d]+$/
@number=num.to_s.gsub(/(\s|_|-)/,'')
@type=nil
@validity=nil
end

def card_type
@type||=CardTypes.detect {|i| i[:regex].match @number}
end

def to_s
"Number: #{@number}, Type: #{card_type[:name]}, Valid: #{valid?}"
end

def valid?
return @validity unless @validity.nil?
return @validity="unknown" unless card_type[:luhn]
arr=(a)number.split(//).reverse.map {|x| x.to_i}
arr.each_with_index{|v,i| arr[i]=v*2 if i%2==1}
sum=arr.join.split(//).map do |x| x.to_i end.inject {|s,i| i+s}
@validity = sum%10==0
end
end

if __FILE__==$0
card=CreditCard.new(if ARGV.empty?
puts "Please enter your credit card number:"
gets.chomp
else
ARGV.join
end)
puts card
end

From: Robert Dober on
Yet another Great Ruby Quiz ;)
Well I thought this was a Quiz particularly suited to write *nice*
code. I guess I somehow failed as it is too long, but I put some of
the features I like most of Ruby, well I guess it is a solution which
is consistent with my style ;)
And as I feel that this is not said frequently enough, I'll just say
it: "Thank you James for all these Quizzes, and thanks to the Quiz
creators too of course."

Cheers
Robert

#!/usr/bin/ruby
# vim: sts=2 sw=2 expandtab nu tw=0:

class String
def to_rgx
Regexp.new self
end

def ccc
Checker.new{
amex [34,37], 15
discover 6011, 16
master 50..55, 16
visa 4, [13,16]
jcb 3528..3589, 16
}.check self
end
end

class Checker
UsageException = Class.new Exception
def initialize &blk
@cards = {}
instance_eval &blk
end

def check str
s = str.gsub(/\s/,"")
@cards.each do
|card, check_values|
return [ luhn( s ), card.to_s.capitalize ] if
check_values.first === s && check_values.last.include?( s.length )
end
[ nil, "Unknown" ]
end

def luhn s
sum = 0
s.split(//).reverse.each_with_index{
| digit, idx |
sum += (idx%2).succ * digit.to_i
}
(sum % 10).zero? ? " Valid" : "n Invalid"
end
# This is one of the rare examples where
# the method_missing parametes are not
# id, *args, &blk, trust me I know what
# I am doing ;)
def method_missing credit_card_name, regs, lens
raise UsageException, "#{card_name} defined twice" if
@cards[credit_card_name]
### Unifying Integer, Array and Range parameters
lens = [lens] if Integer === lens
lens = lens.to_a
### Genereating regular expressions
regs = [regs] if Integer === regs
regs = regs.map{ |r| "^#{r}" }.join("|").to_rgx
@cards[credit_card_name] = [ regs, lens ]
end
end

ARGV.each do
| number |
puts "Card with number #{number} is a%s %s card" %
number.ccc

end # ARGV.each do

From: Gordon Thiesfeld on
Here's my solution.


require 'enumerator'

class CardProcessor

CARDS = {'visa' => {:length => [13,16], :begin => [4]},
'amex' => {:length => [15], :begin => [34,37]},
'discover' => {:length => [16], :begin => [6011]},
'mastercard' => {:length => [16], :begin => (51..55)},
'jcb' => {:length => [16], :begin => (3528..3589)},
'diners club' => {:length => [14], :begin =>
[(3000..3029).to_a, (3040..3059).to_a, 36, (3815..3889).to_a,
389].flatten}
}

def initialize(name, number)
@name = name.downcase
@number = number.gsub(/\D/,'')
end

def luhn_valid?
a = ''
@number.split('').reverse.each_slice(2){ |leave, double| a <<
leave << (double.to_i * 2).to_s }
a.split('').inject(0){|s,v| s + v.to_i } % 10 == 0
end

def length_valid?
CARDS[@name][:length].include? @number.size
end

def beginning_valid?
@number =~ /^#{CARDS[@name][:begin].to_a.join('|')}/
end

def valid?
beginning_valid? && length_valid? && luhn_valid?
end

def self.cards
CARDS.keys
end

end

if __FILE__ == $0

if ARGV.empty?
puts "Usage ruby #{File.basename($0)} <cardnumber>"
exit 0
end

number = ARGV.join

if CardProcessor.new('', number).luhn_valid?
puts "Your card appears to be a valid card."
result = CardProcessor.cards.map {|card| card if
CardProcessor.new(card, number).valid? }.compact
puts "Vendor: #{(result.empty? ? 'unknown' :
result.first).capitalize}"
else
puts "Your card doesn't appear to be valid."
end

end


From: Ari Brown on

On Apr 29, 2007, at 1:14 AM, Mark Day wrote:
<snip>

> This confused me at first, too. Let's take just the last four
> digits: 7893. Counting from the end, double every second digit,
> leaving the others unchanged. That gives you: 14, 8, 18, 3 (where
> 14=7*2 and 18=9*2; the 8 and the 3 are unchanged). Now add up every
> digit of the resulting sequence: 1+4 + 8 + 1+8 + 3. Note the
> "1+4" comes from the 14, and the "1+8" comes from the 18.

Tons, your and philip's responses have definitely cleared this up.
Now I can begin writing it.
--------------------------------------------|
If you're not living on the edge,
then you're just wasting space.



From: Ryan Leavengood on
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