Velocity Reviews > Ruby > [SUMMARY] Checking Credit Cards (#122)

# [SUMMARY] Checking Credit Cards (#122)

Ruby Quiz
Guest
Posts: n/a

 05-03-2007
This quiz is super easy, of course. The reason I ran it though is that I wanted
to see how people approached the Luhn algorithm implementation. It's an easy
enough process, but I found myself using an odd combination of Regexp and eval()
when I was fiddling with it:

puts eval( ARGV.join.gsub(/(\d)?(\d)(?=(?:\d\d)*\d\$)/) do
"#{\$1 + '+' if \$1}#{(\$2.to_i * 2).to_s.split('').join('+')}+"
end ) % 10 == 0 ? "Valid" : "Invalid"

I knew that was ugly and wanted to see how you guys would pretty it up.

You have shown me the light and it tells me... Daniel Martin is crazy. I'll
leave it to him to explain his own solution, as punishment for the time it took
me to puzzle it out. I had to print that Array inside of the inject() call
during each iteration to see how it built up the answer.

I do want to show you a slew of interesting tidbits though. First, let's get
the formality of a full solution out of the way. Here's some code from 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

As Drew shows, checking the type() is just a matter of verifying length and
prefix against a known list. Nothing tricky here, as long as you don't get into
the more complicated cards discussed in the quiz thread.

The valid?() method is the Luhn algorithm I wanted to see. Drew bypasses the
need for my crazy Regexp using a trick I'm always harping on: reverse the data.
It won't make any difference mathematically if the number is backwards and then
you just need to double each second digit. Drew figures out when that is by
combining each_with_index() with a little modulo test. From there, it's a
simple sum of the digits and the final modulo test to determine validity.

The application code just runs those two methods and prints results.

Looking at Drew's Luhn algorithm again, see how he declares the digits variable
and then fills it up? That's the classic inject() pattern, but inject() wasn't
an option there since the code needed each_with_index() over just plain each().
This situation seems to call for an inject_with_index(), and look what doug
meyer wrote:

class Array
def inject_with_index(injected)
each_with_index{|obj, index| injected = yield(injected, obj, index) }
injected
end
end

I guess he felt the same way, though I would have added this method to
Enumerable instead of Array. Others used a hand rolled map_with_index() in
similar ways.

Now, if you want to get away from all this index checking, you need to take a
more functional approach and some solutions definitely did that. Here's the
same algorithm we've been examining from Ryan Leavengood's code:

require 'enumerator'

# ...

def self.luhn_check(cc)
# I like functional-style code (though this may be a bit over the top)
(cc.split('').reverse.enum_for(:each_slice, 2).inject('') do |s, (a, b)|
s << a + (b.to_i * 2).to_s
end.split('').inject(0) {|sum, n| sum + n.to_i}) % 10 == 0
end

This process is interesting, I think, so let's walk through it. The card number
is split() into digits and reverse()d as we have been seeing.

Then an Enumerator for each_slice() is combined with inject() to build up the
new digits. This passes the digits into inject() two at a time, so the block
can just double the second one. This will give you an extra nil at the end of
an odd card number, but to_i() turn that into a harmless zero.

The digits are again divided and this time summed to get a grand total. Check
that for divisibility by ten and we have our answer.

Now Ryan chose to build up the digits and then sum, but you could do both in the
same iterator. The first number is only ever one digit, so it's only the second
number that needs special handling:

require "enumerator"
puts ARGV.join.split("").reverse.enum_slice(2).inject(0 ) { |sum, (l, r)|
sum + l.to_i +
(r.to_i * 2).to_s.split("").inject(0) { |s, d| s + d.to_i }
} % 10 == 0 ? "Valid" : "Invalid"

Ryan's version is probably still a little cleaner though.

Going one step further is to think about that second digit some more. It's the
source of the need for tricky code, because it might be a two digit number.
However, it doesn't have to be. Nine doubled is 18, but the sum of the digits
of 18 is 9. In fact, if you subtract nine from any double over ten you will get
the same sum. One way to put this to use is with a trivial lookup table, as
Brad Ediger did:

# Returns true if 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

Skipping over the sum() and mysterious mapn() methods for now, just take in the
usage of double_and_sum. Those are the sums of all possible doubles of a single
digit. Given that, Brad's solution has to do a lot less busy work than many of
the others we saw. I thought this was a clever reduction of the problem.

I'm sure you can guess what that sum() method does, but let's do take a quick
peek at mapn(), which is another bit of clever code:

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

You can see that mapn() is essentially, enum_slice(N).map(). The interesting
part is that N is chosen from the arity of your provided block. If we ask for
two at a time, we get two; ask for three and we get three:

>> (1..10).mapn { |a, b| [a, b] }

=> [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
>> (1..10).mapn { |a, b, c| [a, b, c] }

=> [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, nil, nil]]

That's a pretty smart iterator, I say.

This completes our tour of some of the wild and wacky ideas for applying the
Luhn algorithm to card numbers. My thanks to all who shared them.

Ruby Quiz will now take a one week break. Work has been rough this week and I
need some down time. I'll be back next week, rested, and with new quizzes...

Rick DeNatale
Guest
Posts: n/a

 05-03-2007
I came to this quiz late, but came up with the following, before
reading the solution, which uses String.tr to do the double and sum
instead of an array.

require 'enumerator'
def luhn(string)
string.scan(/./).reverse.enum_for(:each_with_index).inject(0) do
|sum, (digit, index)|
digit = digit.tr('0123456789', '0246813579') if index % 2 == 1
sum += digit.to_i
end # % 10 == 0
end

After seeing the neat use of each_slice instead of each_with_index

def luhn(string)
string.scan(/./).reverse.enum_for(:each_slice, 2).inject(0) do |sum, (d1, d2)|
sum += d1.to_i + (d2||'0').tr('0123456789', '0246813579').to_i
end % 10 == 0
end

No need to define any extra Enumerator methods.

--
Rick DeNatale

My blog on Ruby
http://talklikeaduck.denhaven2.com/

anansi
Guest
Posts: n/a

 05-03-2007
> Ruby Quiz will now take a one week break. Work has been rough this week and I
> need some down time. I'll be back next week, rested, and with new quizzes...

Means that: no quiz tomorrow ?!?

--
greets

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

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

Ryan Leavengood
Guest
Posts: n/a

 05-03-2007
On 5/3/07, Rick DeNatale <(E-Mail Removed)> wrote:
>
> After seeing the neat use of each_slice instead of each_with_index
>
> def luhn(string)
> string.scan(/./).reverse.enum_for(:each_slice, 2).inject(0) do |sum, (d1, d2)|
> sum += d1.to_i + (d2||'0').tr('0123456789', '0246813579').to_i
> end % 10 == 0
> end
>
> No need to define any extra Enumerator methods.

Nice, I like it. When I first thought about using
enum_for(:each_slice, 2) I was surprised that it even worked.
Enumerator is a powerful library.

It seems like I learn something new every time I do one of these
quizzes, either from making my own solution or reading someone else's.

Ryan

James Edward Gray II
Guest
Posts: n/a

 05-03-2007
On May 3, 2007, at 9:15 AM, anansi wrote:

>> Ruby Quiz will now take a one week break. Work has been rough
>> this week and I
>> need some down time. I'll be back next week, rested, and with new
>> quizzes...

>
>
> Means that: no quiz tomorrow ?!?

Yes. I need a short break. We will have one the following Friday
though.

James Edward Gray II

Rick DeNatale
Guest
Posts: n/a

 05-03-2007
On 5/3/07, Ryan Leavengood <(E-Mail Removed)> wrote:

> Enumerator is a powerful library.

Yep. And personally, I like the fact that Ruby 1.9 is on the path to:

1) Move enumerator to the core classes.
2) Have enumerator methods return an enumerator if they are called
without a block.

The second one seems to be slightly controversial, but I love it.

--
Rick DeNatale

My blog on Ruby
http://talklikeaduck.denhaven2.com/

anansi
Guest
Posts: n/a

 05-03-2007
James Edward Gray II wrote:
> On May 3, 2007, at 9:15 AM, anansi wrote:
>
>>> Ruby Quiz will now take a one week break. Work has been rough this
>>> week and I
>>> need some down time. I'll be back next week, rested, and with new
>>> quizzes...

>>
>>
>> Means that: no quiz tomorrow ?!?

>
> Yes. I need a short break. We will have one the following Friday though.
>
> James Edward Gray II
>

Maybe someone is interested or has an idea for an unoffical Ruby Quiz
for this week. I'm bored , so If someone has any idea, feel free to drop
it

--
greets

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

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

Puria Nafisi Azizi
Guest
Posts: n/a

 05-03-2007
anansi wrote:
> Maybe someone is interested or has an idea for an unoffical Ruby Quiz
> for this week. I'm bored , so If someone has any idea, feel free to drop

conway's game of life?

anansi
Guest
Posts: n/a

 05-03-2007
Puria Nafisi Azizi wrote:
> anansi wrote:
>> Maybe someone is interested or has an idea for an unoffical Ruby Quiz
>> for this week. I'm bored , so If someone has any idea, feel free to drop

> conway's game of life?
>

Never heard about it but read now wiki and sounds interesting, so I
would try it. Would you suggest a fixed count of columns and rows for
that, if yes which?

--
greets

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

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

Puria Nafisi Azizi
Guest
Posts: n/a

 05-03-2007
anansi wrote:
> Never heard about it but read now wiki and sounds interesting, so I
> would try it. Would you suggest a fixed count of columns and rows for

pass it as an argument

 Thread Tools

 Posting Rules You may not post new threads You may not post replies You may not post attachments You may not edit your posts BB code is On Smilies are On [IMG] code is On HTML code is OffTrackbacks are On Pingbacks are On Refbacks are Off Forum Rules

 Similar Threads Thread Thread Starter Forum Replies Last Post Ruby Quiz Ruby 57 05-03-2007 01:47 AM Brian Krahmer Ruby 0 05-01-2007 10:20 PM Daniel Martin Ruby 4 04-30-2007 12:43 PM Todd Benson Ruby 1 04-30-2007 03:52 AM

Advertisments