[QUIZ] Checking Credit Cards (#122)

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

···

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Before a credit card is submitted to a financial institution, it generally makes
sense to run some simple reality checks on the number. The numbers are a good
length and it's common to make minor transcription errors when the card is not
scanned directly.

The first check people often do is to validate that the card matches a known
pattern from one of the accepted card providers. Some of these patterns are:

  +============+=============+===============+
  > Card Type | Begins With | Number Length |
  +============+=============+===============+
  > AMEX | 34 or 37 | 15 |
  +------------+-------------+---------------+
  > Discover | 6011 | 16 |
  +------------+-------------+---------------+
  > MasterCard | 51-55 | 16 |
  +------------+-------------+---------------+
  > Visa | 4 | 13 or 16 |
  +------------+-------------+---------------+

All of these card types also generate numbers such that they can be validated by
the Luhn algorithm, so that's the second check systems usually try. The steps
are:

  1. Starting with the next to last digit and continuing with every other
     digit going back to the beginning of the card, double the digit
  2. Sum all doubled and untouched digits in the number
  3. If that total is a multiple of 10, the number is valid

For example, given the card number 4408 0412 3456 7893:

  Step 1: 8 4 0 8 0 4 2 2 6 4 10 6 14 8 18 3
  Step 2: 8+4+0+8+0+4+2+2+6+4+1+0+6+1+4+8+1+8+3 = 70
  Step 3: 70 % 10 == 0

Thus that card is valid.

Let's try one more, 4417 1234 5678 9112:

  Step 1: 8 4 2 7 2 2 6 4 10 6 14 8 18 1 2 2
  Step 2: 8+4+2+7+2+2+6+4+1+0+6+1+4+8+1+8+1+2+2 = 69
  Step 3: 69 % 10 != 0

That card is not valid.

This week's Ruby Quiz 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.

For completeness and to make this Quiz exercise valid to more people, can anyone include the information for other major credit cards from major countries? Japan: JCB, et. al., U.K.: Barclay, et. al. , etc...
This would be good to expand the exercise and return more to our friends in various areas!
Discover is itself not found outside of North America, AFIK.

···

On Apr 27, 2007, at 8:59 PM, Ruby Quiz wrote:

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Before a credit card is submitted to a financial institution, it generally makes
sense to run some simple reality checks on the number. The numbers are a good
length and it's common to make minor transcription errors when the card is not
scanned directly.

The first check people often do is to validate that the card matches a known
pattern from one of the accepted card providers. Some of these patterns are:

  +============+=============+===============+
  > Card Type | Begins With | Number Length |
  +============+=============+===============+
  > AMEX | 34 or 37 | 15 |
  +------------+-------------+---------------+
  > Discover | 6011 | 16 |
  +------------+-------------+---------------+
  > MasterCard | 51-55 | 16 |
  +------------+-------------+---------------+
  > Visa | 4 | 13 or 16 |
  +------------+-------------+---------------+

All of these card types also generate numbers such that they can be validated by
the Luhn algorithm, so that's the second check systems usually try. The steps
are:

  1. Starting with the next to last digit and continuing with every other
     digit going back to the beginning of the card, double the digit
  2. Sum all doubled and untouched digits in the number
  3. If that total is a multiple of 10, the number is valid

For example, given the card number 4408 0412 3456 7893:

  Step 1: 8 4 0 8 0 4 2 2 6 4 10 6 14 8 18 3
  Step 2: 8+4+0+8+0+4+2+2+6+4+1+0+6+1+4+8+1+8+3 = 70
  Step 3: 70 % 10 == 0

Thus that card is valid.

Let's try one more, 4417 1234 5678 9112:

  Step 1: 8 4 2 7 2 2 6 4 10 6 14 8 18 1 2 2
  Step 2: 8+4+2+7+2+2+6+4+1+0+6+1+4+8+1+8+1+2+2 = 69
  Step 3: 69 % 10 != 0

That card is not valid.

This week's Ruby Quiz 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 Quiz <james@grayproductions.net> writes:

This week's Ruby Quiz 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.

May I make an additional suggestion? Treat spaces in the card number
as perfectly valid, and simply strip them before processing. One of
the most irritating features found in many online stores is the
requirement that the credit card number be typed in with no spaces.
(The other biggie is using a select box for the state abbreviation)

Now granted, for accepting the card number from the command line this
will mean that on the command line the card number was quoted, but
presumably most people's programs will be a command-line wrapper
around a function/class that does the real work.

···

--
s=%q( Daniel Martin -- martin@snowplow.org
       puts "s=%q(#{s})",s.to_a[1] )
       puts "s=%q(#{s})",s.to_a[1]

        +============+=============+===============+
        > Card Type | Begins With | Number Length |
        +============+=============+===============+
        > AMEX | 34 or 37 | 15 |
        +------------+-------------+---------------+
        > Discover | 6011 | 16 |
        +------------+-------------+---------------+
        > MasterCard | 51-55 | 16 |
        +------------+-------------+---------------+
        > Visa | 4 | 13 or 16 |
        +------------+-------------+---------------+

So is a card number like "4012 3456 7890" a valid Unknown, or an invalid Visa?
Similarly, for Unknown, should we accept any length, or go for the common 16?

Wikipedia has a great chart showing all the prefixes of most known
credit cards, along with lengths and overlap between cards (currently
none).

enjoy,

-jeremy

···

On Fri, Apr 27, 2007 at 08:59:49PM +0900, Ruby Quiz wrote:

The first check people often do is to validate that the card matches a known
pattern from one of the accepted card providers. Some of these patterns are:

  +============+=============+===============+
  > Card Type | Begins With | Number Length |
  +============+=============+===============+
  > AMEX | 34 or 37 | 15 |
  +------------+-------------+---------------+
  > Discover | 6011 | 16 |
  +------------+-------------+---------------+
  > MasterCard | 51-55 | 16 |
  +------------+-------------+---------------+
  > Visa | 4 | 13 or 16 |
  +------------+-------------+---------------+

--

Jeremy Hinegardner jeremy@hinegardner.org

<snippage>

  1. Starting with the next to last digit and continuing with every other
     digit going back to the beginning of the card, double the digit
  2. Sum all doubled and untouched digits in the number
  3. If that total is a multiple of 10, the number is valid

For example, given the card number 4408 0412 3456 7893:

  Step 1: 8 4 0 8 0 4 2 2 6 4 10 6 14 8 18 3
  Step 2: 8+4+0+8+0+4+2+2+6+4+1+0+6+1+4+8+1+8+3 = 70
  Step 3: 70 % 10 == 0

Thus that card is valid.

Uh, this is probably just affecting me but....

In his example, after he doubles the second to last digit (call it d), he uses mod10 on it (9*2 = 18 %10 = 8). That is the way to get his numbers, but a) where does he say that and b) where do the 10 and 14 come from?

Help me, hyperactive ruby posters!
~ Ari
English is like a pseudo-random number generator - there are a bajillion rules to it, but nobody cares.

···

On Apr 27, 2007, at 7:59 AM, Ruby Quiz wrote:

This Ruby Quiz has been translated into Japanese.
Maybe there will be more people participating.

http://d.hatena.ne.jp/nappa_zzz/20070429

Harry

···

On 4/27/07, Ruby Quiz <james@grayproductions.net> wrote:

This week's Ruby Quiz 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.

--

A Look into Japanese Ruby List in English

My second ever rubyquiz submission, so be nice.
Sorry if the submission is a little early (by about an hour and a half), but I won't be around a comp tomorrow.

Raj Sahae

# ccc.rb
# Checking Credit Cards
# By Raj Sahae

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

class CCNumber
  #This data was taken from http://en.wikipedia.org/wiki/Credit_card_number
  TYPES = [
    Hash['type' => 'American Express', 'key' => [34, 37], 'length' => [15]],
    Hash['type' => 'China Union Pay', 'key' => (622126..622925).to_a, 'length' => [16]],
    Hash['type' => 'Diners Club Carte Blanche', 'key' => (300..305).to_a, 'length' => [14]],
    Hash['type' => 'Diners Club International', 'key' => [36], 'length' => [14]],
    Hash['type' => 'Diners Club US & Canada', 'key' => [55], 'length' => [16]],
    Hash['type' => 'Discover', 'key' => [6011, 65], 'length' => [16]],
    Hash['type' => 'JCB', 'key' => [35], 'length' => [16]],
    Hash['type' => 'JCB', 'key' => [1800, 2131], 'length' => [15]],
    Hash['type' => 'Maestro', 'key' => [5020, 5038, 6759], 'length' => [16]],
    Hash['type' => 'MasterCard', 'key' => (51..55).to_a, 'length' => [16]],
    Hash['type' => 'Solo', 'key' => [6334, 6767], 'length' => [16, 18, 19]],
    Hash['type' => 'Switch', 'key' => [4903, 4905, 4911, 4936, 564182, 633110, 6333, 6759],
                                                                                                                        'length' => [16, 18, 19]],
    Hash['type' => 'Visa', 'key' => [4], 'length' => [13, 16]],
    Hash['type' => 'Visa Electron', 'key' => [417500, 4917, 4913], 'length' => [16]]
    ]
     #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
    TYPES.each do |company|
      company['key'].each do |key|
        if company['length'].include?(@number.length) and @number.join.begins_with?(key.to_s)
          return company['type']
        end
      end
    end
    "Unknown"
  end
   def valid?
    temp = @number.reverse.collect_with_index{|num, index| index%2 == 0 ? num*2 : num}
    sum = temp.collect{|num|num > 9 ? [1, num%10] : num}.flatten.inject{|s, n| s+n}
    sum%10 == 0
  end
   def process
    puts "The card type is #{self.type}"
    puts "The card number is #{self.valid? ? 'valid' : 'invalid'}"
  end
end

if $0 == __FILE__
  abort "You must enter a number!" if ARGV.empty?
  CCNumber.new(ARGV.join.strip.split(//)).process
end

Here is my solution to Quiz 122:

<code>
class CardChecker
    def initialize(card_num)
       @card_num = card_num
       @issuer = case
          when visa? then 'VISA'
          when mastercard? then 'MasterCard'
          when amex? then 'AMEX'
          when discover? then 'Discover'
          else 'UNKNOWN'
       end
       @valid = valid?
    end
    def visa?
       (@card_num.size == 13 || @card_num.size == 16) && @card_num =~ /^4/
    end
    def mastercard?
       @card_num.size == 16 && @card_num =~ /^5[1-5]/
    end
    def amex?
       @card_num.size == 15 && @card_num =~ /^3[47]/
    end
    def discover?
       @card_num.size == 16 && @card_num =~ /^6011/
    end
    def valid?
       digits = @card_num.reverse.split('')
       sum = 0
       digits.each_with_index do |e, i|
          d = e.to_i
          if i & 1 == 0
             sum += d
          else
             q, r = (d + d).divmod(10)
             sum += q + r
          end
       end
       sum % 10 == 0
    end
    def to_s
       @issuer + (@valid ? " " : " IN") + "VALID"
    end
end

if $0 == __FILE__
    puts CardChecker.new(ARGV.join)
end
</code>

This was a very easy (I like easy) but fun quiz. I believe this is the first time I actually completed a quiz solution, including testing, in less than an hour.

Regards, Morton

Here is my solution.

···

On 4/27/07, Ruby Quiz <james@grayproductions.net> wrote:

This week's Ruby Quiz 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.

######
# Check Starting numbers and length
def card_ok(str,info)
check = "UNKNOWN"
  info.each do |t|
  pref = t[0]
  leng = t[1]
  name = t[2]
    pref.each do |x|
      if x == str.slice(0...x.length)
        leng.each do |y|
          if y.to_i == str.length
          check = name.dup
          end
        end
      end
    end
  end
return check
end

# Check Luhn algorithm
def luhn(str)
luhn_hash = Hash.new("INVALID")
  if str.length != 0
  luhn_hash[0] = "VALID"
  arr = str.split(//).reverse
  arr2 =
  arr3 =
    (0...arr.length).each do |u|
    arr2 << arr[u] if u %2 == 0
    arr2 << (arr[u].to_i * 2).to_s if u %2 != 0
    end

    arr2.each do |r|
    arr3 << r.split(//).inject(0) {|sum,i| sum + i.to_i}
    end

  val = arr3.inject(0) {|sum,i| sum + i} % 10
  end
return luhn_hash[val]
end

# Card information
test =
test << [["31","34"],["15"],"AMEX"]
test << [["6011"],["16"],"DISCOVER"]
test << [("51".."55").to_a,["16"],"MASTERCARD"]
test << [["4"],["13","16"],"VISA"]
#test << [("3528".."3589").to_a,["16"],"JCB"]
#test << [[("3000".."3029").to_a + ("3040".."3059").to_a +
#("3815".."3889").to_a + ["36","389"]].flatten!,["14"],"DINERS"]

# Main
str = ARGV.join
arr =
arr << card_ok(str,test)
arr << luhn(str)
puts arr
######

Harry

--

A Look into Japanese Ruby List in English

48 hours are past so here is my first solution. I'm just coding ruby for a few days so any feedback is welcome :slight_smile:

My script recognizes 12 different creditcard companys and of course says if a card could belong to two different companys at the same time or if it is unknown.

I think the companycheck part could be done better by calling check.call from a loop but I couldn't figure out how to pass such an hash:

hash = Hash.new { |hash, key| hash[key] = [] }
    raw_data = [ [1,"American Express"],[1,/^34|^37/], [1, "15"],
                 [2,"Diners CLub Blanche"],[2,/^30[0-5]/], [2, "14"],
                 [3,"Solo"],[3,/^6334|^6767/],[3,"16"],[3,"18"],[3,"19"]]
    raw_data.each { |x,y| hash[x] << y }

to

check = lambda{|reg,c,*d| if number =~ reg: @company += " or " + c if d.include?(number.length) end }

in a loop. Any idea how to do this?

here the solution:

#!/usr/bin/ruby -w
# validate.rb

···

######################################################
# validator for creditcard numbers #
# by anansi #
# 29/04/07 on comp.lang.rub #
# #
# [QUIZ] Checking Credit Cards (#122) #
######################################################
# recognizes: #
# American Express, #
# Diners CLub Blanche, #
# Diners CLub International, #
# Diners Club US & Canada, #
# Discover, #
# JCB, #
# Maestro (debit card), #
# Mastercard, #
# Solo, #
# Switch, #
# Visa, #
# Visa Electron #
######################################################

  class CreditCard

   def initialize(number)
    number.scan(/\D/) { |x| # scans number for every not-digit symbol
                         puts x + " is no valid credit card symbol.\nJust digits allowed!!"
                         exit
                         }
   end

   def companycheck(number)
    @company= ""
    # block check compares the length and sets the company value
    check = lambda{|reg,c,*d| if number =~ reg: @company += " or " + c if d.include?(number.length) end }
    # adding a new bank is quite easy, just put a new check.call in with:
    # check.call(regular expressions for the starting bytes,"Company-name",length1,length2,...)
    # I'm sure this can be done somehow better invoking check.call by a loop
    # but I couldn't figure out how to pass a array with dynamic variable count into check.call
    check.call( /^34|^37/ , "American Express" , 15 )
    check.call( /^30[0-5]/ ,"Diners CLub Blanche",14)
    check.call( /^36/ , "Diners CLub International",14)
    check.call( /^55/ , "Diners Club US & Canada",16)
    check.call( /^6011|^65/ , "Discover",16)
    check.call( /^35/ , "JCB" , 16)
    check.call( /^1800|^2131/ , "JCB" , 15)
    check.call( /^5020|^5038|^6759/ , "Maestro (debit card)" , 16)
    check.call( /^5[0-5]/ , "Mastercard" , 16)
    check.call( /^6334|^6767/ , "Solo" , 16 , 18 , 19)
    check.call( /^4903|^4905|^4911|^4936|^564182|^633110|^6333|^6759/ , "Switch" , 16 , 18 , 19)
    check.call( /^4/ , "Visa" , 13 , 16)
    check.call( /^417500|^4917|^4913/ , "Visa Electron" , 16)
    if @company == ""
      puts "Company : Unknown"
     else
      puts "Company : #{@company.slice(4..@company.length)}"
    end
   end

   def crossfoot(digit)
    digit = "%2d" % digit # converts integer to string
    if digit[0] == 32
     digit = (digit[1].to_i) -48
    else # if the doubled digit has more than 2 digits
    digit= (digit[0].to_i) + (digit[1].to_i) -96 # adds the single digits and converts back to integer
    end
   end

   def validation(number)
    math = lambda { |dig| number[@count-dig]-48 } # block math converts str to int of the current digit
    @duplex = false
    @count = number.length
    @result = 0
    for i in (1..@count)
     if @duplex == false # for every first digit from the back
      @result += math.call i # add to result
      @duplex = true
     else # for every second digit from the back
      @result += crossfoot((math.call i)*2) # mutl. digit with 2, do the crossfoot and add to result
      @duplex = false
     end
    end
    @result.modulo(10)
   end

  end

#### begin

  if ARGV.length == 0 # checks if argument is passed
   puts "no input\nusage, e.g.: ruby validate.rb 4408 0412 3456 7893"
   exit
  end

  number = ARGV.join('').gsub(" ","") # reads args and kills all spaces and newlinefeed

  my_creditcard = CreditCard.new(number) # checks if just digits are inputed otherwise: abort.

  my_creditcard.companycheck(number) # checks for known or unknown company

  if my_creditcard.validation(number) == 0 # checks validation with luhn-algo
   puts "Validation: successful"
  else
   puts "Validation: failure"
  end

### eof


greets
                        (
                      )
                     (
               /\ .-"""-. /\
              //\\/ , \//\\
              >/\| ,;;;;;, |/\|
              //\\\;-"""-;///\\
             // \/ . \/ \\
            (| ,-_| \ | / |_-, |)
              //`__\.-.-./__`\\
             // /.-(() ())-.\ \\
            (\ |) '—' (| /)
             ` (| |) `
       jgs \) (/

  one must still have chaos in oneself to be able to give birth to a dancing star

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/

My solution (the second I've submitted, so please be nice :). Suggestions welcome.

···

###
BCTYPES = {
    [[34,37],[15]] => "AMEX",
    [[6011],[16]] => "Discoverer",
    [(51..57).to_a,16] => "MasterCard",
    [[4],[13,16]] => "Visa"}

def ctype(num)
    BCTYPES.each { |n,t| n[0].each { |s|
        return t if num.grep(/^#{s}/).any? && n[1].include?(num.length)
    } }
    "Unknown"
end

def luhncheck(num)
    e = false
    num.split(//).reverse.collect { |a| e=!e
        a.to_i*(e ? 1:2)
    }.join.split(//).inject(0) {|a,b| a+b.to_i} % 10 == 0 ? "Valid" : "Invalid"
end

card = ARGV.join.gsub(/ /, '')
if card == ""
    puts "Usage: #{$0} <card number>"
else
    puts ctype(card)
    puts luhncheck(card)
end
###

Joe

Ruby Quiz wrote:

This week's Ruby Quiz 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.

I'm rather new to ruby and this is the first time I participated in this quiz.
If found this one to be rather easy and fun to work with.
Here's my solution:

#!/usr/bin/env ruby

class CreditCard
  attr_reader :number
  CardTypes = [
    { :name => "AMEX", :regex => /(34|37)\d{13}/, :luhn => true},
    { :name => "Bankcard", :regex => /5610\d{12}/, :luhn => true},
    { :name => "Bankcard", :regex => /56022[1-5]\d{10}/, :luhn => true},
    { :name => "China Union Pay", :regex => /622\d{13}/, :luhn => false},
    { :name => "DC-CB", :regex => /30[0-5]\d{11}/, :luhn => true},
    { :name => "DC-eR", :regex => /2(014|149)\d{11}/, :luhn => false},
    { :name => "DC-Int", :regex => /36\d{12}/, :luhn => true},
    { :name => "DC-UC or MasterCard", :regex => /55\d{14}/, :luhn => true},
    { :name => "Discover", :regex => /6011\d{12}/, :luhn => true},
    { :name => "MasterCard", :regex => /5[1-4]\d{14}/, :luhn => true},
    { :name =>"Maestro", :regex => /(5020|5038|6759)\d{12}/, :luhn => true},
    { :name => "Visa", :regex => /4(\d{13}|\d{16})/, :luhn => true},
    { :name => "Unknown", :regex => //, :luhn => true} ]
    # If the credit card is of unknown type, we'll just assume
    # that it can be verified using the Luhn algorithm.

  def initialize(num)
    self.number=num
  end

  def number=(num)
    raise ArgumentError, "Supplied argument is not a number" unless
                                                     num.to_s =~ /^[-_\s\d]+$/
    @number=num.to_s.gsub(/(\s|_|-)/,'')
    @type=nil
    @validity=nil
  end

  def card_type
    @type||=CardTypes.detect {|i| i[:regex].match @number}
  end

  def to_s
    "Number: #{@number}, Type: #{card_type[:name]}, Valid: #{valid?}"
  end

  def valid?
    return @validity unless @validity.nil?
    return @validity="unknown" unless card_type[:luhn]
    arr=@number.split(//).reverse.map {|x| x.to_i}
    arr.each_with_index{|v,i| arr[i]=v*2 if i%2==1}
    sum=arr.join.split(//).map do |x| x.to_i end.inject {|s,i| i+s}
    @validity = sum%10==0
  end
end

if __FILE__==$0
  card=CreditCard.new(if ARGV.empty?
                                  puts "Please enter your credit card number:"
                                  gets.chomp
                                else
                                  ARGV.join
                                end)
  puts card
end

My solution:

cardcheck.rb (1.67 KB)

···

####

#!/usr/bin/env ruby -w

class Numeric
   def to_a
     [self]
   end
end

class CardType

   @@cards = []

   def initialize(name, prefix, length)
     @name = name
     @prefix = prefix.to_a
     @length = length.to_a
   end

   def match(string)
     @length.member?(string.length) && @prefix.find { |v| string =~ /^#{v.to_s}/ }
   end

   def to_s
     @name
   end

   def CardType.register(name, prefix, length)
     @@cards << CardType.new(name, prefix, length)
   end

   def CardType.luhn_check(value)
     value.reverse.scan(/..{0,1}/).collect do |s|
       s[0..0] + (s[1..1].to_i * 2).to_s
     end.join.scan(/./).inject(0) { |sum, s| sum + s.to_i } % 10 == 0
   end

   def CardType.find_card(string)
     value = string.gsub(/[ -]/,'')
     return "Illegal Code" if value =~ /\W/
     "#{@@cards.find { |c| c.match(value) } || "Unknown"} [#{luhn_check(value) ? "Valid" : "Invalid" }]"
   end

end

CardType.register "Maestro", [5020, 5038, 6759], 16
CardType.register "VISA", 4, [13, 16]
CardType.register "MasterCard", 51..55, 16
CardType.register "Discover", [6011, 65], 16
CardType.register "American Express", [34, 37], 15
CardType.register "Diners Club International", 36, 14
CardType.register "Diners Club Carte Blanche", 300..305, 14
CardType.register "JCB", 3528..3589, 16

number = ARGV.join " "
puts number + " => " + CardType.find_card(number)

####

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}"

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

credit-card.rb (5.84 KB)

···

--

Jeremy Hinegardner jeremy@hinegardner.org

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

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

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

validate_card(ARGV.join("")) if __FILE__ == $0