1. Computing

Challenge: Sequence of Integers

By

A challenge: Given an integer, write a method that takes an integer and must return an array containing all the integers from 1 to that integer. While this at first sound simple, there are a few things to consider. And those new to Ruby may be surprised at the answers.

The Non-Ruby Way: Building the Array

In most languages like C++ or Java, there aren't really any ways to simply build an array. You must declare an object of some type (in C++, a vector would be a good choice here, but there are many others) and build the array manually. Since people are just so used to doing this, when presented with this challenge they reach for the tools they know best. This is the equivalent of someone learning a foreign language, but using word orderings and idioms from their native language, only with their newly learned foreign language words. They've got the idea right, and people (and the computer) will understand you, but there's an easier way to say it.

So let's take a look at such an implementation. This works, but it is very un-Ruby.


#!/usr/bin/env ruby

def array_of_1_to(x)
  array = []

  n = 1
  while n <= x
    array << n
    n += 1
  end

  return array
end

puts array_of_1_to(10).inspect

This just looks utterly bizarre in Ruby. This is the type of code you see in C. There are loops, variables where none are needed in Ruby, etc. Again, works just fine but very un-Ruby.

The Range

"Ah ha!" you say, "Ruby already have a construct for describing ranges of numbers already, the Range class!" Yes, you can use that, and the code becomes much more reasonable. You can convert any range into an array using the to_a method, and it's done in a single line. While this looks like the ultimate solution to the problem, it's not. However, it is the most idiomatic Ruby, a simple implementation that reads well and presents no surprises.


#!/usr/bin/env ruby

def array_of_1_to(x)
  (1..x).to_a
end

puts array_of_1_to(10).inspect

I suspect many Rubyists would stop at that. The problem is solved, it cannot simply be any more simple and they'll just go on with their project. But read the requirements again carefully: what if the number passed is negative? Or 1 itself? Blindly chucking the number into a range and hoping for the best is not an option. So let's whip up a test to make sure we've got this right.

Adding the Tests

When working on a problem, it's often useful to whip up some tests to tell you if your code is working. If you look at the code above, I was simply inspecting the array, printing it and manually reading it. What if, after working all day and my tenth cup of coffee I simply misread it? Computers don't misread results, write tests even if you don't think you don't need to. So we know the last solution doesn't work, let's add tests to show it doesn't work.


#!/usr/bin/env ruby
require 'test/unit'

def array_of_1_to(x)
  (1..x).to_a
end

class TestArrayOf1To < Test::Unit::TestCase
  def test_greater_than_1
    assert_equal [1,2,3], array_of_1_to(3)
  end

  def test_less_than_1
    assert_equal [-3, -2, -1, 0, 1], array_of_1_to(-3)
  end

  def test_equal_to_1
    assert_equal [1], array_of_1_to(1)
  end
end

Now when you run the program, you'll get output that looks like this.


Run options: 

# Running tests:

[3/3] TestArrayOf1To#test_less_than_1 = 0.00 s           
  1) Failure:
test_less_than_1(TestArrayOf1To) [array2.rb:14]:
<[-3, -2, -1, 0, 1]> expected but was
<[]>.

Finished tests in 0.001975s, 1518.9773 tests/s, 1518.9773 assertions/s.
3 tests, 3 assertions, 1 failures, 0 errors, 0 skips

ruby -v: ruby 2.0.0dev (2012-11-01 trunk 37411) [i686-linux]

3 tests, 3 assertions, 1 failure. Just what we expected (since (1..1).to_a will still work, passed as well). Now all we need to do is fix the method. We can do this will a simple conditional statement.

Fixed

So, here's the fixed code.


#!/usr/bin/env ruby
require 'test/unit'

def array_of_1_to(x)
  if x < 1
    (x..1).to_a
  else
    (1..x).to_a
  end
end

class TestArrayOf1To < Test::Unit::TestCase
  def test_greater_than_1
    assert_equal [1,2,3], array_of_1_to(3)
  end

  def test_less_than_1
    assert_equal [-3, -2, -1, 0, 1], array_of_1_to(-3)
  end

  def test_equal_to_1
    assert_equal [1], array_of_1_to(1)
  end
end

And its tests being run.


Run options: 

# Running tests:

Finished tests in 0.001847s, 1624.6443 tests/s, 1624.6443 assertions/s.
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips

ruby -v: ruby 2.0.0dev (2012-11-01 trunk 37411) [i686-linux]

It works! And you can leave it at that. It passes all the tests, it reads well, it's idiomatic Ruby, it can't get much better than that. But some people (primarily ex-Perl programmers, or just people who like a challenge) think this is not done until it's as short as possible, or at least done in a single line. Making it absolutely as short as possible is referred to as "code golf," making a working implementation in as few characters as possible. This isn't really a good idea though. If you have to stop when you see this code and ask yourself "wait, what does this do again?" and especially for something so simple, you should opt for the more readable approach. But let's do it anyway, because it's fun.

There are two possibilities that come to mind. The ternary operator lets you cram a conditional expression into a single line, implementing it with the ternary operator would look like this.


def array_of_1_to(x)
  ( x < 1 ? (x..1) : (1..x) ).to_a
end

Not bad, but looks pretty Perlish. The signal to noise ratio is much too low, way too many symbols. Let's instead use the Range constructor and just sort our arguments. We'll use the splat operator to make the Range constructor happy, it doesn't like dealing with arrays.


def array_of_1_to(x)
  Range.new( *[x,1].sort ).to_a
end

This one works because if x is greater than 1, the sort method will always put it the right way around. The splat operator turns this from an array to 2 arguments to the Range.new method, and then to_a turns the range back into an array. Neither of these are very good though, you should stick with the if statement.

  1. About.com
  2. Computing
  3. Ruby
  4. Question and Answer
  5. Challenge: Sequence of Integers

©2014 About.com. All rights reserved.