This quiz reminded me of my days in credit card processing. The
weighted checksum for routing numbers is more interesting, but the
sort-of-two-pass aspect of the Luhn algorithm is a great stumbling
block. My solution follows. You’ll notice I liked your ARGV.join
trick for the input.
def initialize(*args)
super
gsub!(/\D/, ‘’) @factors = [1,2] @factors.cycle! if (length % 2) == 1
end
def type
return @type if @type
PATTERNS.each do |name, pat| @type = name if [pat[:start]].flatten.any? { |s| match /
^#{s}/ } and [pat[:length]].flatten.any? { |l| length == l }
end @type ||= ‘Unknown’
end
def luhn_sum @luhn_sum ||= split(’’).inject(0) do |sum, digit| @factors.cycle!
sum += (digit.to_i * @factors.first).to_s.split(’’).inject(0) { |
s,d| s += d.to_i }
end
end
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
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 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' :
I took the library writing approach and since I have recently been using
Austin’s excellent mime-types library, I took a similar approach with
CreditCard Types. That is, a global registration of types that are
described in a here document.
If there is interest I’ll polish it up a bit and release it as a gem.
class CreditCard
class CardType < Struct.new(:name, :regex, :accepted_lengths)
def valid_length?(length)
if accepted_lengths.is_a?(Array)
return accepted_lengths.include?(length)
else
return accepted_lengths == length
end
end
end
def initialize(number) @number = number @card_type = CARD_TYPES.find {|t| @number =~ t.regex }
end
def card_type @card_type.name
end
def valid?
return false unless @card_type.valid_length?(@number.length)
numbers = @number.split(//).collect {|x| x.to_i}
i = numbers.length - 2
while i >= 0
numbers[i] *= 2
i -= 2
end
numbers = numbers.to_s.split(//)
sum = 0; numbers.each {|x| sum += x.to_i}
sum % 10 == 0
end
end
abort “Usage: #{$0} card_number […]” if ARGV.empty?
ARGV.each do |card_number|
c = CreditCard.new(card_number)
out = "#{card_number}: "
out += (c.valid? ? "Valid " : "Invalid ")
out += “#{c.card_type}”
puts out
end
def initialize(number) @number = number.gsub(/\D/,’’)
end
def valid?
adjusted_numbers = ‘’ @number.reverse.split(’’).each_with_index do |x, i|
adjusted_numbers << (i % 2 == 0 ? x : (x.to_i * 2).to_s)
end
adjusted_numbers.split(’’).inject(0) {|sum, x| sum += x.to_i} % 10
== 0
end
def card_type
TYPES.each do |type, criteria|
if criteria[:start].any? {|n| @number.match(Regexp.compile(’^’+n.to_s))}
if criteria[:length].include? @number.length
return type
end
end
end
:unknown
end
end
if FILE == $0
test_card = CreditCard.new(ARGV.join(’’))
puts “Card type: #{test_card.card_type}”
print test_card.valid? ? “Passes” : “Fails”
puts " the Luhn algorithm."
end
# Disallow card "numbers" with non-digits
if num =~ /\D/
@type = "Unknown"
@valid = false
return
end
# See which of the patterns match the string
@type = @@types.find {|name, regexp| num =~ regexp }[0]
# See if the card number is valid according to the Luhn algorithm
@valid = num.reverse.split('').enum_slice(2).inject(0) do
|sum, (odd, even)|
sum + odd.to_i + (even.to_i*2).digits.sum
end % 10 == 0
=begin
#
# This works, too. But it seems awfully long and complicated.
#
# The idea is to combine the digits of the credit card number with
# a sequence of 1’s and 2’s so that every other digit gets doubled.
# Then sum up the digits of each product.
#
# BTW, the “[1,2]*num.length” construct builds an array that’s
twice
# as long as necessary. The entire array only needs num.length
# elements, but having more is OK. This was the easy way of making
# sure it was big enough.
# @valid = num.reverse.to_i.digits.pairwise_product([1,2]
*num.length).
map{|x| x.digits.sum}.sum % 10 == 0
=end
end
def valid? @valid
end
end
if FILE == $0
cc = CreditCard.new(ARGV.join)
print cc.valid? ? “Valid” : “Invalid”, " #{cc.type}\n"
end
This is my first quiz submission. The only testing I did was running
the
cards I had in my wallet. It seems to work. I’m sure there’s a much
better
way to implement this, but here it is:
#card_check.rb
def check_type(number) #Returns a string representing the type of card if the length and
leading
digits are valid. Otherwise returns “Unknown”.
valid = {
/^(34|37)/ => [15,“AMEX”],
/^6011/ => [16,“Discover”],
/^(51|52|53|54|55)/ => [16,“MasterCard”],
/^4/ => [13,16,“Visa”]
}
number.gsub!(/ /,"")
valid.each_key do |i|
if number =~ i
return valid[i][-1] if valid[i].include? number.length
end
end
return “Unknown”
end
def luhn(number)
Returns “valid” if the number passes the Luhn algorihm criteria.
Returns
“invalid” if the algorithm fails.
number = number.gsub(/ /,"").split(//).reverse
new_number = “”
number.each_index do |i|
new_number << (number[i].to_i*2).to_s if (i+1) % 2 == 0
new_number << number[i] if (i+1) % 2 == 1
end
new_number = new_number.split(//)
sum = 0
new_number.each_index { |i| sum += new_number[i].to_i }
return “valid” if sum % 10 == 0 unless number.length == 0
return “invalid”
end
def validate_card(number)
puts “Type: #{check_type(number)}”
puts “Status: #{luhn(number)}”
end
Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby T. follow the discussion. Please reply to the original quiz message,
if you can.
All of these card types also generate numbers such that they can be validated by
Step 1: 8 4 0 8 0 4 2 2 6 4 10 6 14 8 18 3
That card is not valid.
This week’s Ruby Q. 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.
Here is my solution.
#!/usr/bin/ruby
credit_card_number = ARGV.join
case
when (credit_card_number=~/^(34|37)\d{13}$/): print 'AMEX ’
when (credit_card_number=~/^6011\d{12}$/): print 'Discover ’
when (credit_card_number=~/^5[1-5]\d{14}$/): print 'MasterCard ’
when (credit_card_number=~/^4(\d{12}|\d{15})$/): print 'Visa ’
else print 'Unknown ’
end
i = 0
luhl_number = ‘’
credit_card_number.reverse.each_byte {|char|
if (i%2==1) then
char = (char.chr.to_i * 2).to_s
else
char = char.chr
end
luhl_number = char + luhl_number
i += 1
}
I’ll wait with my solution for a further hour but a comment to your
solution:
try out: ruby ccc.rb 5508 0412 3456 7893
it should show: Diners Club US & Canada or Mastercard
but shows just Mastercaards
Fixed, and resubmitting. Thanks for the lookout. I didn’t include that
initially because I didn’t think there would be overlap. I didn’t even
bother to look myself and see if that assumption was true.
ccc.rb
Checking Credit Cards
class String
def begins_with?(str)
temp = self.slice(0…str.length)
temp == str
end
end
class Array
def collect_with_index
self.each_with_index do |x, i|
self[i] = yield(x, i)
end
end
end
#number should be an array of numbers as strings e.g. [“1”, “2”, “3”]
def initialize(array) @number = array.collect{|num| num.to_i}
end
def type
names = Array.new
TYPES.each do |company|
company[‘key’].each do |key|
if company[‘length’].include?(@number.length) and @number.join.begins_with?(key.to_s)
names << company[‘type’]
end
end
end
names.empty? ? [“Unknown”] : names
end
This week’s Ruby Q. 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.
This is just a class, with a bit of starter code at the bottom to get
going.
It will take any number of CC numbers on the command line, but they must
be
quoted. The luhn method seems tacky, but seems to work.
#################################
class CCNumberError < StandardError
end
class CardValidate
attr_reader :cc_number, :cc_type, :luhn_valid
def initialize(cc_number) @cc_number = cc_number
normalise_cc_number
card_type
luhn
end
private
def normalise_cc_number @cc_number = @cc_number.gsub(" ", “”)
if @cc_number =~ /\D/
raise CCNumberError, “Credit Card numbers may not contain non
digit
characters except spaces”, caller
end @cc_length = @cc_number.length
end
def card_type
if @cc_length == 15
if @cc_number[0…1].to_i == 34 or @cc_number[0…1].to_i == 37 @cc_type = “American Express”
end
elsif @cc_length == 16 and @cc_number[0…3] == 6011 @cc_type = “Discover”
elsif @cc_length == 16 and (51…55) === @cc_number[0…1].to_i @cc_type = “MasterCard”
elsif @cc_length == 16 or @cc_length == 13
if @cc_number.index(“4”) == 0 @cc_type = “Visa”
end
else @cc_type = “Unknown”
end
end
def luhn
ccn = @cc_number.reverse.scan(/\d/)
ccn_luhn_sum = 0
i = 0
ccn.length.times do
if i % 2 == 0
ccn_luhn_sum += ccn[i].to_i
else
if ccn[i].to_i * 2 >= 10
n = (ccn[i].to_i * 2).to_s
ccn_luhn_sum += n[0].chr.to_i
ccn_luhn_sum += n[1].chr.to_i
else
ccn_luhn_sum += ccn[i].to_i * 2
end
end
i += 1
end
ccn_luhn_sum % 10 == 0 ? @luhn_valid = true : @luhn_valid = false
end
end
determine if card is valid based on Luhn algorithm
def valid?
digits = ‘’
# double every other number starting with the next to last
# and working backwards @number.split(’’).reverse.each_with_index do |d,i|
digits += d if i%2 == 0
digits += (d.to_i*2).to_s if i%2 == 1
end
# sum the resulting digits, mod with ten, check against 0
digits.split('').inject(0){|sum,d| sum+d.to_i}%10 == 0
end
end
if FILE == $0
card = CreditCard.new(ARGV.join.chomp)
puts “Card Type: #{card.type}”
if card.valid?
puts “Valid Card”
else
puts “Invalid Card”
end
end
My second ever Ruby Q…
TIA for any suggestions for making it more Ruby-like.
/Bob
#!/usr/bin/env ruby -w
class CreditCard
attr_reader :number, :type, :validity
def initialize(cardnumber) @number = cardnumber.gsub(/\s/,’’) @type = case @number
when /^3[47]\d{13}$/ then “AMEX”
when /^6011\d{12}$/ then “Discover”
when /^5[12345]\d{14}$/ then “Mastercard”
when /^4\d{12}$/ then “VISA”
when /^4\d{15}$/ then “VISA”
else “Unknown”
end
sum = 0
digits = @number.to_s.split(’’).reverse.map {|i| i.to_i}
digits.each_index {|i| i%2==0 ? sum+=add_digits(digits[i]) : sum
+=add_digits(digits[i]*2)} @validity = sum%10 == 0 ? “Valid” : “Invalid”
end
def add_digits(n)
return n.to_s.split(’’).inject(0) {|sum, i| sum += i.to_i}
end
end #CreditCard
c = CreditCard.new(ARGV.join)
puts “#{c.number}: #{c.type}\t#{c.validity}”
This week’s Ruby Q. 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.
#!ruby
def cardtype(n)
case n.delete(“^0-9”)
when /\A3[37]\d{13}\z/: “AMEX”
when /\A6011\d{12}\z/: “Discover”
when /\A5[1-4]\d{14}\z/: “Master Card”
when /\A4\d{12}\d{3}?\z/: “Visa”
else “Unknown”
end
end
def luhn?(n)
f = 2
(n.delete(“^0-9”).reverse.split(//).map{|d|d.to_i}.
inject(0) { |a,e| f=3-f; a + (ef > 9 ? ef-9 : e*f) } % 10).zero?
end
def initialize(number) @number = []
# split credit card number and store in array
number.scan(/\d/){|c| @number.push c.to_i}
# Check Provider Infos
@provider = "Unknown"
PROVIDERINFO.each_pair {|k, v| @provider = k if
@number.join.match(v) }
end
def luhn_passed?
sum = 0 @number.reverse.each_with_index do |num, i|
# double the nummer if necessary and subtract 9 if the result
# consists of 2 numbers (here same as summing up both numbers)
num = num * 2 - ((num > 4) ? 9 : 0) if i % 2 == 1
sum += num
end
sum % 10 == 0
end
def to_s
“Creditcard number #{@number}\n” +
" Provider: #{self.provider}\n" +
" Luhn Algorithm #{'not ’ unless self.luhn_passed?}passed"
end
end
My solution is a more verbose version of the metaprogramming-based
routing used in why’s excellent camping. But instead of regexen
matching urls to controller classes, here they are matching card
numbers to the card classes.
What I like about this approach is that you can add more card patterns
just by adding more classes with the appropriate regexen, and you can
override validation or other functionality as needed.
Regards,
Paul.
cardvalidator.rb
require ‘metaid’
module Card
@cards=[]
def Card.Base *u
c = @cards
Class.new {
meta_def(:patterns){u}
meta_def(:validity){|x|Card.validity(x)}
meta_def(:inherited){|x|c<<x}
}
end
def Card.validate cardnum @cards.map { |k|
k.patterns.map { |x|
if cardnum =~ /^#{x}/?$/
return [k.name.upcase, k.validity(cardnum)].join( " “)
end
}
}
raise “Unexpected Card Number pattern: ‘#{cardnum}’”
end
def Card.sum_of_digits s
s.split(”").inject(0){|sum,x|sum + Integer(x)}
end
def Card.luhnSum s
temp = “”
r = 0…s.size
a = s.split("").reverse
r.each do |i|
if i%2==1
x = (Integer(a[i])*2).to_s
else
x = a[i]
end
temp << x.to_s
end
sum_of_digits temp
end
def Card.validity cardnum
if (Card.luhnSum(cardnum) % 10)==0
return “Valid”
else
return “Invalid”
end
end
end
patterns will be tested for match in order defined
class Visa < Card.Base /4[0-9]{12,15}/
end
class Amex < Card.Base /3[4,7][0-9]{13}/
end
class Mastercard < Card.Base /5[1-5][0-9]{13}/
end
class Discover < Card.Base /6011[0-9]{12}/
end