7

I've been trying to implement the Luhn algorithm in Ruby. I've been following these steps:

  • The formula verifies a number against its included check digit, which is usually appended to a partial account number to generate the full account number. This account number must pass the following test:
    • Counting from the check digit, which is the rightmost, and moving left, double the value of every second digit.
    • Sum the digits of the products (e.g., 10 = 1 + 0 = 1, 14 = 1 + 4 = 5) together with the undoubled digits from the original number.
    • If the total modulo 10 is equal to 0 (if the total ends in zero) then the number is valid according to the Luhn formula; else it is not valid.

http://en.wikipedia.org/wiki/Luhn_algorithm

This is what I've came up with:

 def validCreditCard(cardNumber)
    sum = 0
    nums = cardNumber.to_s.split("")
    nums.insert(nums.size, "x")
    nums.reverse!
    nums.each_with_index do |n, i|
        if !n.eql?("x")
            sum += (i % 2 == 0) ? n.to_i : n.to_i * 2
        end
    end
    if (sum % 10) == 0
        return true
    else
        return false
    end
end

However, this returns false every time I test it. I am not sure what I am doing wrong.

3
  • Note that your if statement at the end of your method is extraordinarily and unnecessarily verbose. If the logic is correct, you can simply replace that all with just sum%10 == 0; this evaluates to either true or false, and the value of the final expression in a method is the return value of the method. Commented Feb 8, 2012 at 15:16
  • There is now a gem for this: rubygems.org/gems/luhn Commented Mar 28, 2014 at 19:02
  • Solution in gem is not very elegant Commented Feb 15, 2019 at 11:17

11 Answers 11

24

Here's a quick one that works:

def credit_card_valid?(account_number)
  digits = account_number.chars.map(&:to_i)
  check = digits.pop

  sum = digits.reverse.each_slice(2).flat_map do |x, y|
    [(x * 2).divmod(10), y || 0]
  end.flatten.inject(:+)

  check.zero? ? sum % 10 == 0 : (10 - sum % 10) == check
end

credit_card_valid? "79927398713" #=> true
credit_card_valid? "79927398714" #=> false
Sign up to request clarification or add additional context in comments.

8 Comments

+1 for a very idiomatic implementation. Note that in place of scan(/./) you can use chars in Ruby 1.9+. See also flat_map for 1.9+
@Phrogz: I know both of them, but old habits sometimes die hard. :-)
The problem with using each_slice(2), is that if the account_number is an odd number, you will run into issues when you try to run the inject(:+) method.
it's just me or this code doesn't work? (I having a 'no implicit conversion of Fixnum into Array' in the inject(:+) part. To work, I had to change it to end.flatten.inject(:+)
@bbozo Fixed it for you.
|
10

I had problems running a lot of the above code, so I went ahead and developed a solution. Below is an my solution, along with test code and can be run with Ruby-cores AWESOME minitest library.

require "minitest"
require "minitest/autorun"

# Public: Validates number against Luhn 10 scheme
#
# Luhn Algo ripped from: http://en.wikipedia.org/wiki/Luhn_algorithm
# 1. From the rightmost digit, which is the check digit, moving left, double the value of every second digit; if product of this doubling operation is greater than 9 (e.g., 7 * 2 = 14).
# 2. Sum the digits of the products (e.g., 10: 1 + 0 = 1, 14: 1 + 4 = 5) together with the undoubled digits from the original number.
# 3. If the total modulo 10 is equal to 0 (if the total ends in zero) then the number is valid according to the Luhn formula; else it is not valid.
#
# Returns true or false
def luhn_valid?(cc_number)
  number = cc_number.
    gsub(/\D/, ''). # remove non-digits
    reverse  # read from right to left

  sum, i = 0, 0

  number.each_char do |ch|
    n = ch.to_i

    # Step 1
    n *= 2 if i.odd?

    # Step 2
    n = 1 + (n - 10) if n >= 10

    sum += n
    i   += 1
  end

  # Step 3
  (sum % 10).zero?
end

describe "Luhn Algorithm" do
  # Cards ripped from paypal: http://www.paypalobjects.com/en_US/vhelp/paypalmanager_help/credit_card_numbers.htm
  CC_NUMBERS = {
    "American Express" => "378282246310005",
    "American Express 2" => "371449635398431",
    "American Express Corporate" => "378734493671000",
    "Australian BankCard" => "5610591081018250",
    "Diners Club" => "30569309025904",
    "Diners Club 2" => "38520000023237",
    "Discover" => "6011111111111117",
    "Discover 2" => "6011000990139424",
    "JCB" => "3530111333300000",
    "JCB 2" => "3566002020360505",
    "MasterCard" => "5555555555554444",
    "MasterCard 2" => "5105105105105100",
    "Visa" => "4111111111111111",
    "Visa 2" => "4012888888881881",
    "Visa 3" => "4222222222222",
  }

  it "returns true for valid numbers" do
    assert CC_NUMBERS.values.all? { |number| luhn_valid?(number) }
  end

  it "returns false for invalid numbers" do
    CC_NUMBERS.values.each do |number|
      me_turn_bad = (number.to_i + 1).to_s
      refute luhn_valid?(me_turn_bad)
    end
  end
end

Comments

1

Some cool answers above. I wanted to write my own solution; for posterity I'm adding it here.

def number_is_luhn_valid?(credit_card_number)
  cc_digits = credit_card_number.to_s.reverse.chars.map(&:to_i)
  check_sum = 0

  cc_digits.each_slice(2) do |odd, even|
    check_sum += odd
    next unless even
    even *= 2
    even = even.divmod(10).inject(:+) if even > 9
    check_sum += even
  end

  return check_sum.modulo(10) == 0
end

Always love to hear feedback if you have it!

Comments

0

I would guess the problem is that you have not considered the index shift you got by inserting the "x" at the end of your array, so you double the wrong numbers.

Comments

0

Here is your solution:

def validate(cardNumber)
  nums = cardNumber.to_s.split("")
  checkdigit = nums[nums.length - 1]
  nums[nums.length - 1] = 0
  nums.reverse!
  sum=0
  for i in 1..nums.length
    if i%2==0 
      sum = sum + nums[i].to_i  
      next
    end
    nums[i] = (nums[i].to_i*2 < 10 ) ? (nums[i].to_i*2) : (nums[i].to_i*2 - 9)
    sum = sum + nums[i].to_i
  end
  puts (10 - sum%10).to_i == checkdigit.to_i
end

validate(79927398713)

It is not optimized, but you can do those minor changes yourself :)

Comments

0

I've changed your implementation a bit. Now it works

def validCreditCard(cardNumber)
  sum = 0
  nums = cardNumber.to_s.split("")
  nums.each_with_index do |n, i|
    sum += if (i % 2 == 0)
             n.to_i * 2 >9 ? n.to_i*2-9 : n.to_i*2
           else
             n.to_i
           end
  end
  if (sum % 10) == 0
    return true
  else
    return false
  end
end

Comments

0

I thank Michael for helping me implement my solution.

It might not be as efficient, but it works and I understand Luhn and the implementation on my part.

I'm still learning, but here it is:

 def validCreditCard?(cardNumber)
    sum = 0 
    digits = []
    nums = cardNumber.to_s.split("")
    nums.reverse.each_slice(2) do |n|
        digits << (n.last.to_i * 2)
        digits << n.first.to_i
    end
    digits.each do |n|
        stringNum = n.to_s
        if stringNum.length == 2
            tempNums = stringNum.split("")
            sum += tempNums.first.to_i + tempNums.last.to_i
        else
            sum += stringNum.to_i
        end
    end
    sum%10 == 0
end

2 Comments

Glad to help :-) To follow SO etiquette, if my answer was the one that helped you, you should accept it by using the little tick mark below the voting arrows so others know the question has been answered.
No problem. Thanks again. Also, Phrogz you help make my code cleaner.
0

I stumbled upon this Ruby gem for the Luhn Algorithm: https://github.com/joeljunstrom/ruby_luhn

Comments

0
    #!/usr/bin/env ruby
number = ARGV[0]
## making array from input number having each digit at separate index
digits = number.chars.map(&:to_i)
check = digits.pop

sum = digits.reverse.each_slice(2).flat_map do |x, y|
  [(x * 2).divmod(10), y || 0]
end.flatten.inject(:+)
## validating if sum is nil then assiging it zero, when we pass single digit like 4 or 5 or 9 then sum will be nil
sum = sum ? sum : 0
puts (check.zero? ? sum % 10 == 0 : (10 - sum % 10) == check) ? "#{number} is valid" : "#{number} is invalid"

Solution given by Michael is breaking if you give single digit number as input this code will handle all cases.

Comments

0

We use the Luhn algorithm for checking validity of bank cards.

The length of the bank card number (PAN) in accordance with ISO/IEC 7812-1:2017 is from 10 to 19.

Therefore, my solution is:

def card_valid?(card_number)
  return false unless card_number.match /\A\d{10,19}\z/
  sum = 0
  card_number.reverse.chars.map(&:to_i).each.with_index(1) do |d, i|
    d *= 2 if i.even?
    d -= 9 if d > 9
    sum += d
  end
  (sum % 10).zero?
end

card_valid? '1115' #=> false
card_valid? '4417123456789112' #=> false
card_valid? '4408041234567893' #=> true

Comments

0

There is my hacker implementation in one line (statement).

Input: string

Output: ‘valid’ for correct credit card numbers.

puts (n=gets).size == 16 && n.reverse.chars.map(&:to_i).map.with_index{|x,i| i.odd?? x>4 ? x*2-9: x*2: x}.sum%10==0 ? 'valid': 'not valid'

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.