[QUIZ] Checking Credit Cards (#122)

Y

Yossef Mendelssohn

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

Jeremy Hinegardner

--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 (e-mail address removed)


--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 = name
@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 = LIST[cc_type.name]
if by_name then
by_name << cc_type
else
by_name = [cc_type]
end
LIST[cc_type.name] = by_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) = ct.split(":").collect {|x| x.strip}
lengths = length.split(",").collect {|x| x.strip }
prefixes = []

prefix.split(",").each do |p|
p.strip!
if p.index("-") then
range_start, range_end = p.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 = CreditCard::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 = digits.sub(/\s+/,'')
if @digits !~ /\A\d+\Z/ then
raise ArgumentError, "#{digits} must only be digits 0-9"
end
@type = Types.of(@digits)
end

def luhn
num_list = digits.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 = digit_sums.inject(0) { |s,v| s + v }
sum % 10
end

def valid?
luhn == 0
end
end
end

# testing


if $0 == __FILE__
require 'optparse'
parser = OptionParser.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 = line.strip.split(",")
ccn = CreditCard::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 = ARGV.join("")
ccn = CreditCard::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--
 
P

Philip Gatt

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 *= 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
 
C

Chris Shea

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
 
M

Mark Day

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 = self * other for i in 0...self.length
def pairwise_product(other)
result = []
each_index {|i| result << self*other }
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
 
B

Brad Ediger

Let's try this again. List ate my post. Maybe it's the S/MIME sig?

----

OK, this is the first Quiz contribution I have made (publicly). I
think it is a good balance between terse and readable.

JEG2, the luhn_valid? check is what I mean by Functional Programming
in Ruby, or as you say, the Power of Iterators. :)

--be

####
#!/usr/bin/env ruby -wKU

# Some utility functions first
require 'enumerator'
module Enumerable
# Maps n-at-a-time (n = arity of given block) and collects the
results
def mapn(&b)
r = []
each_slice(b.arity) {|*args| r << b.call(*args) }
r
end

def sum; inject(0){|s, i| s + i} end
end

class CreditCardNumber < String
TYPES = {"3[47]\\d{13}" => "Amex",
"6011\\d{12}" => "Discover",
"5[1-5]\\d{14}" => "Mastercard",
"4(\\d{12}|\\d{15})" => "Visa"}

# Returns the type of the given card, or nil if the card does not
match a pattern
def card_type
(t = TYPES.detect{|re, t| /^#{re}$/ === self}) && t.last
end

# Returns true iff Luhn check passes for this number
def luhn_valid?
# a trick: double_and_sum[8] == sum_digits(8*2) == sum_digits
(16) == 1 + 6 == 7
double_and_sum = [0, 2, 4, 6, 8, 1, 3, 5, 7, 9]
split(//).reverse.mapn{|a,b| a.to_i + double_and_sum
[b.to_i]}.sum % 10 == 0
end

# Returns true iff card matches a known type and Luhn check passes.
def valid?
luhn_valid? && !card_type.nil?
end
end

if (arg = ARGV.join.gsub(/[^0-9]/, '')) and !arg.empty?
number = CreditCardNumber.new(arg)

if number.valid?
puts "Valid #{number.card_type}"
else
puts "Invalid card"
end
end
 
R

Raj Sahae

anansi said:
I'll wait with my solution for a further hour ;) but a comment to your
solution:
try out: ruby ccc.rb 5508 0412 3456 7893
it should show: Diners Club US & Canada or Mastercard
but shows just Mastercaards
Fixed, and resubmitting. Thanks for the lookout. I didn't include that
initially because I didn't think there would be overlap. I didn't even
bother to look myself and see if that assumption was true.

# ccc.rb
# Checking Credit Cards

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 = 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
names = Array.new
TYPES.each do |company|
company['key'].each do |key|
if company['length'].include?(@number.length) and
@number.join.begins_with?(key.to_s)
names << company['type']
end
end
end
names.empty? ? ["Unknown"] : names
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.join(' or ')}"
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
 
A

akbarhome

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.

Here is my solution.
#!/usr/bin/ruby

credit_card_number = ARGV.join

case
when (credit_card_number=~/^(34|37)\d{13}$/): print 'AMEX '
when (credit_card_number=~/^6011\d{12}$/): print 'Discover '
when (credit_card_number=~/^5[1-5]\d{14}$/): print 'MasterCard '
when (credit_card_number=~/^4(\d{12}|\d{15})$/): print 'Visa '
else print 'Unknown '
end

i = 0
luhl_number = ''
credit_card_number.reverse.each_byte {|char|
if (i%2==1) then
char = (char.chr.to_i * 2).to_s
else
char = char.chr
end
luhl_number = char + luhl_number
i += 1
}

sum_total = 0

luhl_number.each_byte {|char|
sum_total += char.chr.to_i
}

if (sum_total%10==0) then
print "Valid\n"
else
print "Invalid\n"
end
 
D

Drew Olson

My solution is below.

# file: credit_card.rb
# author: Drew Olson

class CreditCard
def initialize num
@number = num
end

# check specified conditions to determine the type of card
def type
length = @number.size
if length == 15 && @number =~ /^(34|37)/
"AMEX"
elsif length == 16 && @number =~ /^6011/
"Discover"
elsif length == 16 && @number =~ /^5[1-5]/
"MasterCard"
elsif (length == 13 || length == 16) && @number =~ /^4/
"Visa"
else
"Unknown"
end
end

# determine if card is valid based on Luhn algorithm
def valid?
digits = ''
# double every other number starting with the next to last
# and working backwards
@number.split('').reverse.each_with_index do |d,i|
digits += d if i%2 == 0
digits += (d.to_i*2).to_s if i%2 == 1
end

# sum the resulting digits, mod with ten, check against 0
digits.split('').inject(0){|sum,d| sum+d.to_i}%10 == 0
end
end

if __FILE__ == $0
card = CreditCard.new(ARGV.join.chomp)
puts "Card Type: #{card.type}"
if card.valid?
puts "Valid Card"
else
puts "Invalid Card"
end
end
 
B

Bob Lisbonne

My second ever Ruby Quiz.
TIA for any suggestions for making it more Ruby-like.
/Bob

#!/usr/bin/env ruby -w

class CreditCard
attr_reader :number, :type, :validity
def initialize(cardnumber)
@number = cardnumber.gsub(/\s/,'')
@type = case @number
when /^3[47]\d{13}$/ then "AMEX"
when /^6011\d{12}$/ then "Discover"
when /^5[12345]\d{14}$/ then "Mastercard"
when /^4\d{12}$/ then "VISA"
when /^4\d{15}$/ then "VISA"
else "Unknown"
end
sum = 0
digits = @number.to_s.split('').reverse.map {|i| i.to_i}
digits.each_index {|i| i%2==0 ? sum+=add_digits(digits) : sum
+=add_digits(digits*2)}
@validity = sum%10 == 0 ? "Valid" : "Invalid"
end
def add_digits(n)
return n.to_s.split('').inject(0) {|sum, i| sum += i.to_i}
end
end #CreditCard

c = CreditCard.new(ARGV.join)
puts "#{c.number}: #{c.type}\t#{c.validity}"
 
J

James Edward Gray II

OK, this is the first Quiz contribution I have made (publicly).

Wow, a semi-local solving the quiz. I guess that means you need to
get back to another OK.rb meeting Brad. ;)

Welcome to all the new solvers!

James Edward Gray II
 
J

James Edward Gray II

# Maps n-at-a-time (n = arity of given block) and collects the
results
def mapn(&b)
r = []
each_slice(b.arity) {|*args| r << b.call(*args) }
r
end

That's pretty darn clever. You can collapse it to one line with
inject() of course:

def mapn(&b)
enum_slice(b.arity).inject([]) {|r, args| r << b.call(*args) }
end

James Edward Gray II
 
B

Brad Ediger

# Maps n-at-a-time (n = arity of given block) and collects the
results
def mapn(&b)
r = []
each_slice(b.arity) {|*args| r << b.call(*args) }
r
end

That's pretty darn clever. You can collapse it to one line with
inject() of course:

def mapn(&b)
enum_slice(b.arity).inject([]) {|r, args| r << b.call(*args) }
end

Didn't even think about inject here. Awesome. I'm sure that I'm going
to be using mapn more, as it's cool.

I can't take credit for the mapn concept though: eachn is a Facets
function I stumbled across.

--be
 
D

Dennis Frommknecht

Am 02.05.2007 um 17:13 schrieb Christian Neukirchen:
Ruby Quiz said:
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

def cardtype(n)
case n.delete("^0-9")
when /\A3[37]\d{13}\z/: "AMEX"
when /\A6011\d{12}\z/: "Discover"
when /\A5[1-4]\d{14}\z/: "Master Card"
when /\A4\d{12}\d{3}?\z/: "Visa"
else "Unknown"
end
end

def luhn?(n)
f = 2
(n.delete("^0-9").reverse.split(//).map{|d|d.to_i}.
inject(0) { |a,e| f=3-f; a + (e*f > 9 ? e*f-9 : e*f) } %
10).zero?
end

puts cardtype(ARGV.join)
puts luhn?(ARGV.join) ? "valid" : "invalid"

__END__


Wow, very nice solution! Impressive how short it is...

Here is my solution:



#!/usr/bin/env ruby -W

# Assign a regular expression that checks first characters and length
PROVIDERINFO = {
"AMEX" => /^(34|37)\d{13}$/,
"Discover" => /^6011\d{12}$/,
"MasterCard" => /^5[1-5]\d{14}$/,
"Visa" => /^4(\d{12}|\d{15})$/,
}

class CreditCard
attr_reader :provider, :number

def initialize(number)
@number = []
# split credit card number and store in array
number.scan(/\d/){|c| @number.push c.to_i}

# Check Provider Infos
@provider = "Unknown"
PROVIDERINFO.each_pair {|k, v| @provider = k if
@number.join.match(v) }
end

def luhn_passed?
sum = 0
@number.reverse.each_with_index do |num, i|
# double the nummer if necessary and subtract 9 if the result
# consists of 2 numbers (here same as summing up both numbers)
num = num * 2 - ((num > 4) ? 9 : 0) if i % 2 == 1
sum += num
end
sum % 10 == 0
end

def to_s
"Creditcard number #{@number}\n" +
" Provider: #{self.provider}\n" +
" Luhn Algorithm #{'not ' unless self.luhn_passed?}passed"
end
end

puts CreditCard.new(ARGV.join)



Regards,

Dennis Frommknecht
 
P

Pit Capitain

James said:
That's pretty darn clever. You can collapse it to one line with
inject() of course:

def mapn(&b)
enum_slice(b.arity).inject([]) {|r, args| r << b.call(*args) }
end

Simpler:

def mapn(&b)
to_enum:)each_slice, b.arity).map(&b)
end

Regards,
Pit
 
J

James Edward Gray II

Just one minor nit:

Christian Neukirchen said:
def luhn?(n)
f = 2
(n.delete("^0-9").reverse.split(//).map{|d|d.to_i}.
inject(0) { |a,e| f=3-f; a + (e*f > 9 ? e*f-9 : e*f) } %
10).zero?
end

You do know that (e*f > 9 ? e*f-9 : e*f) is equivalent to e*f%9,
right?
(0..9).map { |n| n * 2 % 9 } => [0, 2, 4, 6, 8, 1, 3, 5, 7, 0]
(0..9).map { |n| n * 2 > 9 ? n * 2 - 9 : n * 2 }
=> [0, 2, 4, 6, 8, 1, 3, 5, 7, 9]

James Edward Gray II
 
P

Paul Novak

Thank you James for another fun quiz.

My solution is a more verbose version of the metaprogramming-based
routing used in why's excellent camping. But instead of regexen
matching urls to controller classes, here they are matching card
numbers to the card classes.

What I like about this approach is that you can add more card patterns
just by adding more classes with the appropriate regexen, and you can
override validation or other functionality as needed.

Regards,

Paul.

# cardvalidator.rb
#
require 'metaid'

module Card
@cards=[]

def Card.Base *u
c = @cards
Class.new {
meta_def:)patterns){u}
meta_def:)validity){|x|Card.validity(x)}
meta_def:)inherited){|x|c<<x}
}
end
def Card.validate cardnum
@cards.map { |k|
k.patterns.map { |x|
if cardnum =~ /^#{x}\/?$/
return [k.name.upcase, k.validity(cardnum)].join( " ")
end
}
}
raise "Unexpected Card Number pattern: '#{cardnum}'"
end
def Card.sum_of_digits s
s.split("").inject(0){|sum,x|sum + Integer(x)}
end
def Card.luhnSum s
temp = ""
r = 0..s.size
a = s.split("").reverse
r.each do |i|
if i%2==1
x = (Integer(a)*2).to_s
else
x = a
end
temp << x.to_s
end
sum_of_digits temp
end
def Card.validity cardnum
if (Card.luhnSum(cardnum) % 10)==0
return "Valid"
else
return "Invalid"
end
end
end

# patterns will be tested for match in order defined
class Visa < Card.Base /4[0-9]{12,15}/
end
class Amex < Card.Base /3[4,7][0-9]{13}/
end
class Mastercard < Card.Base /5[1-5][0-9]{13}/
end
class Discover < Card.Base /6011[0-9]{12}/
end
# catch-all
class Unknown < Card.Base /.+/
end

p Card.validate(ARGV[0])
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,780
Messages
2,569,611
Members
45,276
Latest member
Sawatmakal

Latest Threads

Top