Forum: Ruby [QUIZ] Checking Credit Cards (#122)

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Cf6d0868b2b4c69bac3e6f265a32b6a7?d=identicon&s=25 Daniel Martin (Guest)
on 2007-04-30 01:23
(Received via mailing list)
I was trying to go for "most compact and obfuscated version of the
Luhn algorithm", but unfortunately the complications of the algorithm
meant that I had to debug my implementation, which left it slightly
more readable.  Oh well.  It's still pretty dense and while my type
method might be readable it's as dense as I care to make it.

#!ruby -x
def type(s)
    case s.gsub(/\D/,'')
    when /^(?=34).{15}$/; "AMEX"
    when /^(?=37).{15}$/; "AMEX"
    when /^(?=6011).{16}$/; "Discover"
    when /^(?=5[1-5]).{16}$/; "MasterCard"
    when /^(?=4).{13}(...)?$/; "Visa"
    else ; "Unknown"
    end
end

def luhn(s)
  s.scan(/\d/).inject([0,0]){|(a,b),c|[b+c.to_i,
  a+c.to_i*2%9+(c=='9' ? 9 : 0)]}[0]%10 == 0
end

s = ARGV.join
puts "#{type(s)} #{luhn(s)?'V':'Inv'}alid"

__END__
E1f43bafda26307a050d11902752b2a6?d=identicon&s=25 Ball, Donald A Jr (Library) (Guest)
on 2007-04-30 01:30
(Received via mailing list)
My submission is pastied here:

http://pastie.caboo.se/57536

and pasted below. I tried to come up with a solution that would allow
easy manipulation of the list of credit card providers.

- donald

# Ruby Quiz 123
# Donald Ball
# version 1.0

require 'enumerator'

class Integer
  def digits
    self.to_s.scan(/\d/).map{|n| n.to_i}
  end

  def sum_digits
    digits.inject{|sum, n| sum + n}
  end

  def luhn
    digits.reverse.enum_slice(2).inject(0) do |sum, sl|
      sum + sl[0].sum_digits + (sl.length == 2 ? (2*sl[1]).sum_digits :
0)
    end
  end

  def luhn?
    luhn % 10 == 0
  end
end

module Credit
  class Provider
    attr_reader :name

    def initialize(name, pattern)
      @name = name
      @pattern = pattern
    end

    def valid?(number)
      @pattern.match(number)
    end

    def to_s
      @name
    end
  end

  Providers = []
  Providers << Provider.new('AMEX', /^(34|37)\d{13}$/)
  Providers << Provider.new('Discover', /^6011\d{12}$/)
  Providers << Provider.new('MasterCard', /^5(1|2|3|4|5)\d{14}$/)
  Providers << Provider.new('Visa', /^4(\d{12}|\d{15})$/)

  class Card
    attr_reader :number

    def initialize(number)
      if number.is_a? Integer
        @number = number
      elsif number.is_a? String
        @number = number.gsub(/\s/, '').to_i
      else
        raise InvalidArgument, number
      end
    end
    def provider
      Providers.each do |provider|
        return provider if provider.valid?(@number.to_s)
      end
      return 'Unknown'
    end
    def valid?
      @number.luhn?
    end
    def to_s
      @number.to_s
    end
  end
end

card = Credit::Card.new(ARGV[0])
puts card.provider.to_s << ' ' << (card.valid? ? 'valid' : 'invalid')
Cf0d3965dc2e3ae8c5b83e118f084c45?d=identicon&s=25 Benjamin Paul Kay (Guest)
on 2007-04-30 05:02
(Received via mailing list)
On Thursday 30 May 2002, Jeff Gray wrote:
> to the 'method=' style of method naming.  For example, you could define a
> (Simply removing the '=' from the method name allows the second case to
> work.)  I couldn't find anything in the pickaxe book or in ruby-talk
> archives about this restriction.  I'm on Ruby version 1.6.5 -- has this
> behavior changed in later versions, or is it a permanent trait of Ruby
> syntax?
>
> Thanks,
>
>   - jeff

Also consider the test case below, where a method with = in its name
always returns the value passed to it. This is really frustrating, as I
want to use = in method names as syntactic sugar but can't because the
methods don't behave like they're supposed to.
Is this a bug or a "feature"?

This script:

class TestClass
  def test=(number)
    # 42 is the answer to life the universe and everything.
    42
  end
  def test(number)
    # 42 is the answer to life the universe and everything.
    42
  end
end

test = TestClass.new

puts 'With the = in the method name:'
puts test.test=7
puts test.test=("Doesn't work with parentheses either.")
puts test.test=[1,2,3,"Arrays are cool!"]

puts 'And without:'
puts test.test(7)
puts test.test("Doesn't work with parentheses either.")
puts test.test([1,2,3,"Arrays are cool!"])

Produces output:

With the = in the method name:
7
Doesn't work with parentheses either.
1
2
3
Arrays are cool!
And without:
42
42
42
Cf6d0868b2b4c69bac3e6f265a32b6a7?d=identicon&s=25 Daniel Martin (Guest)
on 2007-04-30 13:31
(Received via mailing list)
Daniel Martin <martin@snowplow.org> writes:

> I was trying to go for "most compact and obfuscated version of the
> Luhn algorithm", but unfortunately the complications of the algorithm
> meant that I had to debug my implementation, which left it slightly
> more readable.  Oh well.  It's still pretty dense and while my type
> method might be readable it's as dense as I care to make it.

You know, it's longer, but I think I like this definition of the luhn
algorithm better, in terms of its conceptual conciseness:

def luhn(s)
  s.scan(/\d/).map{|x|x.to_i}.inject([0,0]){
  |(a,b),c|[b+c%9,a+2*c%9]}[0]%10 == s.scan(/9/).size%10
end

(Now - the challenge is to figure out why that works)

An interesting observation based on this representation is that the
Luhn algorithm will fail to catch the transposition of an adjacent "0"
and "9":

both '446-667-6097' and '446-667-6907' pass.
915aac3dfde2fb502ce415d77643a72d?d=identicon&s=25 Wil (Guest)
on 2007-04-30 14:45
(Received via mailing list)
This was my first rubyquiz.  I took a meta programming approach, since
it seemed like an easy enough example for it, and I haven't done
enough meta programming yet.  The only one of the solutions above that
I saw using some type of dynamic programming was the one using
method_missing above.  Critiques welcome.  For those interested, I did
manage to write a more details description on my blog:
http://webjazz.blogspot.com/2007/04/ruby-quiz-122-...

Metaprogramming is probably overkill for this, since you don't get a
new type of credit card company all the time, but it's probably a nice
technique for when you have an object that needs lots of different
combinations of things each time.

Here is my solution:

require 'enumerator'

class CreditCardChecker

  def self.metaclass; class << self; self; end; end

  class << self
    attr_reader :cards

    # writes a method with the card_name? as a method name.  The
method created would
    # check what type of credit card a number is, based on the rules
given in the block.
    # Use this function in the subclass
    #
    #   class MyChecker < CreditCardChecker
    #     credit_card(:amex) { |cc| (cc =~ /^34.*/ or cc =~ /^37.*/)
and (cc.length == 15) }
    #   end
    def credit_card(card_name, &rules)
      @cards ||= []
      @cards << card_name

      metaclass.instance_eval do
        define_method("#{card_name}?") do |cc_num|
          return rules.call(cc_num) ? true : false
        end
      end
    end

  end

  def cctype(cc_num)
    self.class.cards.each do |card_name|
      return card_name if self.class.send("#{card_name}?",
normalize(cc_num))
    end
    return :unknown
  end

  def valid?(cc_num)
    rev_num = []
    normalize(cc_num).split('').reverse.each_slice(2) do |pair|
      rev_num << pair.first.to_i << pair.last.to_i * 2
    end
    rev_num = rev_num.to_s.split('')
    sum = rev_num.inject(0) { |t, digit| t += digit.to_i }
    (sum % 10) == 0 ? true : false
  end

  private
  def normalize(cc_num)
    cc_num.gsub(/\s+/, '')
  end
end

class CreditCard < CreditCardChecker
  credit_card(:amex) { |cc| (cc =~ /^34.*/ or cc =~ /^37.*/) and
(cc.length == 15) }
  credit_card(:discover) { |cc| (cc =~ /^6011.*/) and (cc.length ==
16) }
  credit_card(:mastercard) { |cc| cc =~ /^5[1-5].*/ and (cc.length ==
16) }
  credit_card(:visa) { |cc| (cc =~ /^4.*/) and (cc.length == 13 or
cc.length == 16) }
end

CCnum = ARGV[0]

cccheck = CreditCard.new
puts cccheck.cctype(CCnum)
puts cccheck.valid?(CCnum)
8100138c6c0cd6f3bc27b4fe55c550e5?d=identicon&s=25 doug meyer (Guest)
on 2007-05-01 15:39
(Received via mailing list)
The only reason I'm submitting this is because I added a
inject_with_index method to Array. I liked Daniel Martin's use of the
inject([0,0]), I didn't even think of doing that.

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

class CreditCard
  @@types = {
      'AMEX' => { :start => [34, 37], :length => [15] },
      'Discover' => { :start => [6011], :length => [16] },
      'MasterCard' => { :start => (51..55).to_a, :length => [16] },
      'Vista' => { :start => [4], :length => [13, 16] }
    }
  attr :type, true
  attr :valid, true
  def initialize(number)
    @type = (@@types.find do |card_type, card_st|
      card_st[:start].any?{|st| /^#{st}/ =~ number } and
        card_st[:length].any?{|le| number.length == le }
    end || ['Unknown']).first
    @valid = number.reverse.split('').inject_with_index(0) do
|acc,num,ind|
        acc + ( ind%2 == 0 ? num.to_i :
            (num.to_i*2).to_s.split('').inject(0){|a,e|a.to_i+e.to_i} )
      end % 10 == 0
    puts " Type: #{@type}"
    puts "Valid: #{@valid}"
  end
end
This topic is locked and can not be replied to.