--X+8siUETKMkW99st
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

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 / hinegardner.org 


--X+8siUETKMkW99st
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="credit-card.rb"

#!/usr/bin/env ruby
#
# Solution to Ruby Quiz #122 http://www.rubyquiz.com/quiz122.html
#
# Copyright 2007 Jeremy Hinegardner 
#
# MIT License http://www.opensource.org/licenses/mit-license.php
#
module CreditCard

    # a uniq paring of prefix and length that determine a credit card
    # type.  
    class Type
        attr_reader :name
        attr_reader :prefixes
        attr_reader :lengths
         
        def initialize(name, prefixes, lengths)
            @name      ame
            @prefixes  prefixes].flatten.collect { |c| c.to_s }.sort
            @lengths   lengths].flatten.collect { |l| l.to_i }
        end

        def to_s
            name
        end

        def matches?(digits)
            if lengths.include?(digits.length) then
                prefixes.each do |p|
                    return true if digits.index(p) 0
                end
            end
            false
        end

    end

    # The master list of known credit card types.
    class Types
        LIST  }
        class << self
            def add(cc_type)
                by_name  IST[cc_type.name]
                if by_name then
                    by_name << cc_type
                else
                    by_name  cc_type]
                end
                LIST[cc_type.name]  y_name
            end

            def each
                LIST.values.flatten.each { |ct| yield ct }
            end

            def of(digits)
                LIST.values.flatten.each do |ct|
                    return ct if ct.matches?(digits)
                end
                return UNKNOWN_TYPE
            end
        end

        DATA_CARD_TYPES  <-DCT
        # http://en.wikipedia.org/wiki/Credit_card_number and
        # http://www.webreference.com/programming/carts/chap7/3/
        # All active cards that use the Luhn algorithm
        # Card name : prefix as #,#,# or start,end : length(s) 
        American Express : 34,37          : 15 
        Diners Club      : 300-305,36,38  : 14
        Discover         : 6011,65        : 16
        JCB              : 35             : 16
        JCB              : 1800,2131      : 15
        Maestro          : 5020,5038,6759 : 16
        MasterCard       : 51-55          : 16
        Visa             : 4              : 13,16
        DCT

        DATA_CARD_TYPES.each do |ct|
            ct.strip!
            next if ct /^#/
            (name,prefix,length)  t.split(":").collect {|x| x.strip}
            lengths   ength.split(",").collect {|x| x.strip }
            prefixes  ]

            prefix.split(",").each do |p|
                p.strip!
                if p.index("-") then
                    range_start, range_end  .split("-")
                    prefixes << (range_start..range_end).to_a
                else
                    prefixes << p
                end
            end
            CreditCard::Types.add(CreditCard::Type.new(name,prefixes,lengths))
        end
        UNKNOWN_TYPE  reditCard::Type.new("Unknown", [], [])
    end
  
    # Representation of a credit card number holding its type and the
    # number of the card.
    class Number
        attr_reader :type
        attr_reader :digits

        def initialize(digits  ")
            @digits  igits.sub(/\s+/,'')
            if @digits !~ /\A\d+\Z/ then
                raise ArgumentError, "#{digits} must only be digits 0-9"
            end
            @type  ypes.of(@digits)
        end

        def luhn
            num_list  igits.split(//)
            digit_sums  ]
            num_list.reverse.each_with_index do |n,i|
                n  n.to_i * 2) if i % 2 1
                digit_sums << n.to_s.split(//).inject(0) { |sum,v| sum + v.to_i }
            end
            sum  igit_sums.inject(0) { |s,v| s + v }
            sum % 10
        end

        def valid?
            luhn 0
        end
    end
end

# testing


if $0 __FILE__
    require 'optparse'
    parser  ptionParser.new do |op|
        op.banner  Usage: #{File.basename(__FILE__)} [options] card-number"
        op.separator ""
        op.separator "Options:"
        op.on("-h", "--help", "Show this help.") do 
            puts op
            exit 1
        end

        op.on("-t", "--test", "Run the unit tests") do 
            require 'test/unit'
            class TestCreditCards < Test::Unit::TestCase
                def test_numbers
                    DATA.each do |line|
                        card_type,number  ine.strip.split(",")
                        ccn  reditCard::Number.new(number)
                        assert_equal(card_type,ccn.type.to_s, "Expect #{number} to be #{card_type} but got #{ccn.type}")
                        assert(ccn.valid?, "#{number} is not valid")
                    end
                end
            end
            require 'test/unit/ui/console/testrunner'
            Test::Unit::UI::Console::TestRunner.run(TestCreditCards)
            exit 0
        end
    end             

    begin
        parser.parse!
        if ARGV.size 0 then
            puts parser
            exit 1
        end

        number  RGV.join("")
        ccn     reditCard::Number.new(number)
        puts "#{number} #{ccn.type} #{ccn.valid? ? "Valid" : "Invalid" }"
        exit 0
    rescue OptionParser::ParseError pe
        puts pe
        exit 1
    rescue ArgumentError ae
        puts "ERROR: #{ae.to_s}"
        exit 1
    end
end

# Fake card numbers that conform to the Luhn algorithm taken from
# https://www.paypal.com/en_US/vhelp/paypalmanager_help/credit_card_numbers.htm
__END__
American Express,378282246310005
American Express,371449635398431
American Express,378734493671000
Diners Club,30569309025904
Diners Club,38520000023237
Discover,6011111111111117
Discover,6011000990139424
JCB,3530111333300000
JCB,3566002020360505
MasterCard,5555555555554444
MasterCard,5105105105105100
Visa,4111111111111111 
Visa,4012888888881881
Visa,4222222222222

--X+8siUETKMkW99st--