For my solution, I created a Card class that holds a Set of Integers/Ranges
for acceptable prefixes and lengths. I added a prefix_of? method to both
Integer and Range for checking whether they are prefixes of a String.
Card#initialize is pretty flexible in arguments it takes thanks to an
add_set_ivar method that turns numbers/sets/arrays/ranges into the kind of
Sets that I want. From there, the Card#valid? method is straightforward -
just call any? on the prefixes and lengths and make sure both are true.
The Card.luhn_valid? method checks if a card number passes the Luhn
algorithm. I messed around with a few different ways of doing it, and
settled on a rather dense 4-liner.
#!/usr/bin/env ruby
# check_credit_card.rb
# Ruby Quiz 122: Checking Credit Cards
require 'set'
class Integer
# Determine if this number is a prefix of the given string for the given base.
def prefix_of? str, base = 10
not /^#{to_s(base)}/.match(str).nil?
end
end
class Range
# Determine if any numbers within this range is a prefix of the given string
# for the given base.
def prefix_of? str, base = 10
# We could use the first case every time, but the second is much quicker
# for large ranges.
if str[0].chr == '0'
any? { |num| num.prefix_of? str, base }
else
num = str.slice(0..(max.to_s(base).length-1)).to_i
num >= min and num <= max
end
end
end
class Card
attr_accessor :name
# Turn arg into a Set instance variable based on its class.
# This is so initialize can easily accept a few different argument types.
def add_set_ivar ivar, arg
case arg
when Set; instance_variable_set ivar, arg
when Array; instance_variable_set ivar, Set.new(arg)
else; instance_variable_set ivar, Set[arg]
end
end
# prefixes can be:
# - a single number
# - an Array of numbers
# - a Range of numbers
# - an Array of numbers and Ranges
# - a Set of numbers
# - a Set of numbers and Ranges
#
# lengths can be:
# - a single number
# - an Array of numbers
# - a Set of numbers
def initialize name, prefixes, lengths
@name = name
add_set_ivar :@prefixes, prefixes
add_set_ivar :@lengths, lengths
end
# Determine if a number is valid for this card.
def valid? num
num = num.to_s
@prefixes.any? { |pre| pre.prefix_of? num } and
@lengths.any? { |len| len == num.length }
end
# Determine if the given number passes the Luhn algorithm.
# This is pretty damn dense.. perhaps I should spread it out more..
def Card.luhn_valid? num
digits = num.to_s.split(//).map! { |d| d.to_i } # separate digits
(digits.size-2).step(0,-2) { |i| digits[i] *= 2 } # double every other
digits.map! { |d| d < 10 ? d : [1,d-10] }.flatten! # split up those > 10
(digits.inject { |sum, d| sum + d } % 10).zero? # sum divisible by 10?
end
end
if $0 == __FILE__
CardPool = Set[
Card.new('AMEX', [34,37], 15),
Card.new('Discover', 6011, 16),
Card.new('MasterCard', (51..55), 16),
Card.new('Visa', 4, [13,16]),
Card.new('JCB', (3528..3589), 16),
Card.new('Diners', [(3000..3029),(3040..3059),36,(3815..3889),389], 14)
]
card_num = $stdin.gets.chomp.gsub! /\s/, ''
cards = CardPool.select { |c| c.valid? card_num }
if cards.size.zero?
puts "Unknown card."
else
puts "Number matched #{cards.size} cards:"
cards.each { |c| puts " #{c.name}" }
end
if Card.luhn_valid?(card_num); puts 'Passed Luhn algorithm'
else; puts 'Failed Luhn algorithm'; end
end
--
Jesse Merriman
jessemerriman / warpmail.net
http://www.jessemerriman.com/