From: Yossef Mendelssohn on 29 Apr 2007 11:54 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. class Array def cycle! push(shift) end end class CCNum < String PATTERNS = { 'AMEX' => { :start => ['34', '37'], :length => 15 }, 'Discover' => { :start => ['6011', '65'], :length => 16 }, 'MasterCard' => { :start => (51..55).to_a.collect { |n| n.to_s }, :length => 16 }, 'Visa' => { :start => '4', :length => [13, 16] }, }.freeze 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 luhn_valid? (luhn_sum % 10) == 0 end end card_num = CCNum.new(ARGV.join) puts "#{card_num} is a(n) #{card_num.luhn_valid? ? 'V' : 'Inv' }alid #{card_num.type}" From: Jeremy Hinegardner on 29 Apr 2007 13:04 Hi all, 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. Comments are welcome. enjoy, -jeremy -- ======================================================================== Jeremy Hinegardner jeremy(a)hinegardner.org From: Philip Gatt on 29 Apr 2007 13:40 This is my first rubyquiz. Here is my solution. 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 CARD_TYPES = [CardType.new('AMEX', /^3[47]/, 15), CardType.new('Discover', /^6011/, 16), CardType.new('MasterCard', /^5[1-5]/, 16), CardType.new('Visa', /^4/, [13, 16]), CardType.new('Unknown', /.*/, 0)] 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 From: Chris Shea on 29 Apr 2007 13:46 Here's what I came up with. I hope it's as short and to the point as I think it is. Chris #!/usr/local/bin/ruby class CreditCard TYPES = { :visa => {:length => [13, 16], :start => [4]}, :discover => {:length => [16], :start => [6011]}, :mastercard => {:length => [16], :start => 51..55}, :amex => {:length => [15], :start => [34, 37]} } 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 -- Posted via http://www.ruby-forum.com/. From: Mark Day on 29 Apr 2007 14:16 Thanks for another simple but fun quiz! James, thanks for your critiques on last week's quiz; they were educational. I did two versions of the Luhn algorithm. The first one seems more readable to me (though I'm a Ruby nuby), but the second was fun to put together. Comments are welcome. -Mark # # Ruby Quiz #122: Credit card validation # require 'enumerator' class Array def sum(initial=0) inject(initial) { |total, elem| total + elem } end # Compute the pairwise product of two arrays. # That is: result[i] = self[i] * other[i] for i in 0...self.length def pairwise_product(other) result = [] each_index {|i| result << self[i]*other[i] } return result end end class Integer def digits self.to_s.split('').map { |digit| digit.to_i } end end class CreditCard @@types = [["AMEX", /^3[47]\d{13}\$/], ["Discover", /^6011\d{12}\$/], ["MasterCard", /^5[1-5]\d{14}\$/], ["Visa", /^4\d{12}(\d{3})?\$/], ["Unknown", //]] attr_reader :type def initialize(str) num = str.delete(" ") # 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 First  |  Prev  |  Next  |  Last