A Check Digit Code Challenge

I was recently playing with a code challenge on CodeWars called “Modulus 11 - Check Digit” where I was using Ruby. It got me thinking about all of the Enumerable module methods in Ruby and how nice they can be.

The code challenge requires us to:

  1. Take a string of digits as input,
  2. Convert the string into an array of numerical digits in reverse order,
  3. Apply a scaling factor to each digit, \(2\) through \(7\) cycled across the input digits,
  4. Sum all of the products,
  5. Compute the modulo 11 value of the sum, and
  6. Interpolate a check digit value at the end of a string containing the original number depending on if the result of step 5 is \(0\), \(1\), or something else.

Here’s my solution:

MULTI_FACTOR = (2..7).freeze

def str_to_digits(n) 
    n.chars
     .map(&:to_i)
     .reverse
     .freeze
end

def add_check_digit(number)
  rem = (str_to_digits(number)
            .zip(MULTI_FACTOR.cycle)
            .map { |elem| elem[0]*elem[1] }
            .sum) % 11
  
  return "#{number}0" if rem == 0
  return "#{number}X" if rem == 1
  
  return "#{number}#{11 - rem}"
end

We will need to create a couple of methods as well as a constant for the multiplication factor range, MULTI_FACTOR.

The first piece is getting the input data into the right format and types so we can work with it. Getting it into the correct type is a matter of splitting up the individual characters of the string with #chars, mapping over each one, and typecasting with #to_i, giving us an Array<Integer> type. We have one more thing to do in order to get it into the right format as well: we will need the array in reverse order, so we reverse the mapping output with #reverse. Once we’ve computed this array, there’s no need to change anything, so we’ll freeze the array with #freeze. The final method looks like this:

def str_to_digits(n) 
    n.chars
     .map(&:to_i)
     .reverse
     .freeze
end

If this were some sort of programming competition, there are additional ways to shave some 10e-6 or 10e-9 seconds off like not having a separate method to convert the input arrays like I have done with str_to_digits and maybe just immediately using #reduce and immediately applying the multiplication factor. Since I’m moreso trying to promote good practices for re-use, we’ll modularize things a bit more in these posts.

def add_check_digit(number)
  rem = (str_to_digits(number)
            .zip(MULTI_FACTOR.cycle)
            .map { |elem| elem[0]*elem[1] }
            .sum) % 11
  
  return "#{number}0" if rem == 0
  return "#{number}X" if rem == 1
  
  return "#{number}#{11 - rem}"
end

We’ll finally create our driver method: add_check_digit. We will get our input data into the proper form using str_to_digits, then we’ll use #zip to create an array of tuples with the input values and the scaling factor so we can create products and then sum them up.

Finally we’ve done all of this so we can compute the modulo \(11\) of the sum, so we’ll just immediately do that and store the result in a variable rem. We then group together a few guard conditionals to generate the requested output. In theory, we could have moved the first line to its own method and have the driver method focus on the string output.

The Enumerable module contains all of these methods that we can use on arrays like #zip, #cycle, #map, and #sum. This drives the “sort of”-functional programming paradigm that Ruby satisfies.