From: Jesse Merriman Date: 2007-04-29T21:16:05+09:00 Subject: [QUIZ][SOLUTION] Checking Credit Cards (#122) 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/