1. Technology
You can opt-out at any time. Please refer to our privacy policy for contact information.

Ranges

By

Ranges

Most languages don't have a concept of a range of numbers, you simply loop over the numbers you need with a for loop or similar. Ruby, on the other hand, provides ranges of numbers as first class objects.

What are the types of things you can do with an object representing a range of numbers? First and foremost you can iterate over them. Think of that like a C-style "for" loop except you explicitly define the range of numbers you want to iterate over. Ranges can also be used to take slices of strings, generate successive strings, etc. They're not the most important of the Ruby objects, but they do have their uses.

Range Literals

Ranges can be constructed with the "double dot" syntax. To make a range object representing all the integers from 0 to 10, you could say 0..10. Note that the smaller number must be on the left. This is the most common form of the Range literal, and it produces and object of the type Range, which is an Enumerable type.

The above syntax creates inclusive ranges. Both the first and last object of the range are considered to be part of the range. The range 0..10 represents all integers from 0 to 10 including 0 and 10. You can also generate exclusive ranges by using the "triple dot" syntax. The range 0...10 represents the integers from 0 to 10 not including 10. This is a lesser used syntax though, it may lead to confusion.

Like everything else in Ruby, the double dot and triple dot syntax is dynamic. There is no "compile time," so any two values can be used on the left or right hand sides. That said, variables are often used on one of both sides. For example, you can determine how many elements need to be iterated over then iterate over the 0..some_variable range.

One caveat when using the Range literal syntax is that the double dot and triple dot syntax (or operators) bind very loosely. Namely, the single dot operator (used for method calls) binds more tightly. For this reason, you'll often see the range literals in parentheses. For example, (0..10).each { … }.

Finally, the less used Range.new method can be used, though it does the same thing and offers no more functionality from the double dot and triple dot syntax. Its arguments are Range.new(min, max, exclusive). Though it may be more clear to some, as the Range literal can slightly subtract from readability in some cases.

Using Ranges

The primary use of ranges is to iterate over as a from of loop, and to determine if an object lies within the range. Since the Range class includes the Enumerable mixin module, this is done exactly as you'd expect. Iterating is done using the each method and testing if an object lies within the range is done with the include? method.


(0..10).each do|i|
  puts i
end

if (0..10).include?(7)
  puts "Yes, 7 is between 0 and 10"
end

In addition to the typical each method for iterating, the Range class also provides the step method, which will iterate over the range skipping every n elements. This can be used to further emulate a typical C-style loop.


# Print all elements
(0..10).each do|i|
  puts i
end

# Step size of 1 is the same as each
(0..10).step(1) do|i|
  puts i
end

# Step size of 2, print only even numbers
# AKA for(i = 0; i <= 10; i += 2)
(0..10).step(2) do|i|
  puts i
end

How Ranges Work

Ranges can be used with any objects, not just integers. For example, you can use floating point numbers in the range, but you'll quickly run into a limitation there: you won't be able to iterate over the range. This has to do with how Ranges are implemented.

In order to use (a..b).include?(c), c must be greater than or equal to a, and less than or equal to b. As long as a, b and c all respond to the correct operators, they can be used. So you can say things like ('a'..'z').include?('k'). Since the string 'k' is greater than and less than the min and max of the range (as defined by the String#<=> "spaceship operator" method), the method call will be true.

Similarly, in order to be able to iterate over the range a..b, a must be less than b (otherwise the range wouldn't make any sense) and a must respond to the succ method. The succ method should return a successor to the object. For integers, this means the next highest integer. For strings, this will increment the string from 'a' to 'b', and from 'aaa' to 'aab'.

And this brings us back to the original problem. You cannot iterate over the range 10.1..20.1. Why? Because 10.1 is a Float object, and the Float class doesn't implement the succ method.

Similarly, you cannot iterate over the 10..0 range, but for a different reason. If the first element of the range isn't less than the second element of the range, the range won't function in any way, at least with typical number and string arguments (how your objects implement the comparison operators may be different). Since no number can be both greater than 10 and less than 0, no number will ever be included in the range. And since 10 is not less than 0, even though the integer objects implement the succ method, you still cannot iterate over them.

Ranges in Case Statements

One very common place you'll see Range objects is in case statements. Since the Range#=== method is defined as Range#include?, if the argument is included in the range it will return true. So, you may see things like this:


print "Enter your age: "
age = gets.to_i

case age
when 0...21
  puts "You're too young to get in"
when 21..100
  puts "OK, you can come in"
end

©2014 About.com. All rights reserved.