I was reading Dave Fecak’s monthly newsletter and, as always, there were lots of interesting tidbits from the world of programming. The newsletter also included a code challenge that I couldn’t resist:) The challenge was to create an elegant method that will tell you if a number is an Armstrong number. An Armstrong number is a number that is equal to the sum of each of its digits raised to the length of the original number. Here are some example Armstrong numbers:

0 = 01

4 = 41

153 = 13 + 53 + 33

Tests

The challenge was to create a method that returns true if a number is an Armstrong number and false if not. Before beginning, I wrote some some unit tests so that I would know my methods were correctly identifying Armstrong numbers.

require "./is_armstrong"
require "test/unit"

class TestIsArmstrong < Test::Unit::TestCase
  def test_zero
    assert_equal(true, is_armstrong?(0))
  end
  
  def test_one_digit_armstrong
    assert_equal(true, is_armstrong?(4))
  end

  def test_three_digit_armstrong
    assert_equal(true, is_armstrong?(153))
  end

  def test_another_large_armstrong
    assert_equal(true, is_armstrong?(371))
  end
  
  def test_non_armstrong
    assert_equal(false, is_armstrong?(12)) 
  end
end

Iterative Method

This seems like a pretty reasonable approach to me but it’s a little kludgy. I have to initialize a lot of stuff to preserve state as the loop executes.

def is_armstrong? number
  sum_of_digits = 0
  original_number = number
  while number > 0 do
    digit = number % 10
    sum_of_digits += (digit ** original_number.to_s.length)
    number /=  10
  end
  original_number == sum_of_digits
end

Recursive Method

This is pretty much the same as the iterative method in terms of kludginess; I still have to preserve the length of the number across method calls. I wrote this mostly for fun … recursion is a funky beast.

def is_armstrong? number
  number == sum_of_digits(number, number.to_s.length)
end

def sum_of_digits(number, length_of_number)
  if number < 10
    return number
  else
    digit = number % 10
    result = (digit ** length_of_number)
    result += sum_of_digits(number /=  10, length_of_number)
  end
end

Functional Method

This is my favorite solution. Once you move away from thinking about the passed value as a number, you can do all sorts of things. Here, rather than do some mathy manipulation to identify individual digits, I convert the number to a string and the string to an array of numbers. Once I have an array of numbers, I can use inject to do my bidding instead of a loop. In my opinion, this method is the most elegant of the three solutions and is more aesthetically pleasing.

def is_armstrong? number
  num_array = number.to_s.split(//).map(&:to_i)
  number == num_array.inject(0){|sum_of_digits, digit| sum_of_digits + digit ** num_array.length}
end

Update 20140103

A big thanks to reader Hafiz for identifying a bug with my original version of the functional solution.

Hafiz:

I agreed that the functional solution is the most elegant. But it fails for some armstrong numbers such as 370 and 371. I wonder why that happen. Do you have any idea? I still can’t figure that out.

Hafiz:

def narcissistic?( value )
value == value.to_s.each_char.map { |x| x.to_i ** value.to_s.length }.reduce(:+)
end

If found this alternative done by some rubyist.

SG:

Man, what a good question Hafiz. This was a tricky one for me too. You would think that the inject and map/reduce solutions would be identical! They do the same thing by turning the number into a string, then doing some math on the letters converted to numbers. I figured out why my method was acting funky. Check this out:

> num_array = [3,7,1]

I started with one of the Armstrong numbers you mentioned in your comment.

> num_array.inject{|sum,x| sum + x**0}
=> 5 
> num_array.inject(0){|sum,x| sum + x**0}
=> 3 

Whoa, that’s weird. Inject takes an argument and a block. If I omit the argument, I get a different value for what is essentially a count method on the array. Straight from the Ruby docs, if sum is not initialized, it will be defaulted to the first value in the array and not manipulated by the block. You can read more about that here http://ruby-doc.org/core-2.1.0/Enumerable.html#method-i-inject. When we were seeing if 153 was an Armstrong number, first value in the array was 1 but it didn’t matter that sum was initialized to 1 because 1 == 13. When anything other than 1 is the first digit, the armstrong number detector will be inaccurate. In the case of 371, sum gets initialized with 3. Then 73 and 71 is added to sum. So, where we would expect to get 371, we instead get 347. If you are just adding a list of numbers, as I usually do with inject, you would not need to care about initializing the sum either in that case. This bug was the result of habit-based coding.

I could really see this initialization problem when I put some print statements in the inject.

> num_array.inject{|sum,x| print "sum" + sum.to_s, "item" + x.to_s }
sum3item7sumitem1 => nil 
> num_array.inject(0){|sum,x| print "sum" + sum.to_s, "item" + x.to_s }
sum0item3sumitem7sumitem1 => nil 

As soon as I initialized the value of sum, the bug was fixed.

> num_array.inject{|sum, digit| sum + digit**num_array.length}
=> 347 
> num_array.inject(0){|sum, digit| sum + digit**num_array.length}
=> 371

So, now that an initial value is set for sum, we can correctly detect that 371 is an Armstrong number.

Thanks for writing in. I am so pleased that you left such a thoughtful question :) You’re my first non-bot commenter!