In Progress
Unit 1, Lesson 21
In Progress

Range

Video transcript & code

When I think about Ruby's core data structures, I immediately think about arrays and hashes. Sometimes I think about sets as well, although technically Set isn't one of the core types. Conspicuously absent from this list is the Range class. Which is a bit unfair, because Ruby ranges are uniquely useful data structures.

A Range object represents exactly what its name suggests: a range with a beginning and an end. We can explicitly instantiate a Range using Range.new with a begin and end value:

Range.new(1, 10)                # => 1..10

But it's more common and idiomatic to use Range literal syntax, which is simply two values separated by a pair of dots.

1..10                           # => 1..10

This syntax doesn't always play well with larger expressions. For instance, let's ask this range if it includes the number 5.

1..10.include?(5)               # =>
# ~> -:1:in `<main>': undefined method `include?' for 10:Fixnum (NoMethodError)

We get an error saying that the method #include? is undefined for the number 10. What happened here is that the message send syntax bound more tightly than the range literal syntax. So Ruby parsed the expression like this:

1..(10.include?(5))

Because of these parsing issues, we commonly see ranges included in parentheses, even though the parens aren't technically part of the Range syntax. It's a good idea to get into the habit of using parens around ranges, since they are so prone to parsing issues otherwise.

The ranges we've constructed so far are inclusive ranges, meaning that the end value is considered part of the range. Ruby also has exclusive ranges, meaning that the end value is treated as a boundary, but is not considered part of the range. This is more easily demonstrated than explained. Given an inclusive range from 1 to 10, 10 is included in the range. But given an exclusive range from 1 to 10, the ending value 10 is not included.

(1..10).include?(10)            # => true
(1...10).include?(10)           # => false

We've been using the #include? predicate to determine whether a value is contained in a range. We can also use the alias #member? if we prefer.

(1..10).member?(10)             # => true

There's another way to check if a value is included in a range. The threequals operator performs the same operation as #member? and #include? predicates.

(1..10) === 5                   # => true

As we discussed in episode #202, this is also known as the "case equality" operator, and it enables us to use Ranges as patterns in case statements.

case spaceball_one_speed
when 0...299_792_458
  puts "sublight speed"
when 299_792_458
  puts "light speed"
when 299_792_459...1_000_000_000
  puts "rediculous speed"
when 1_000_000_000...10_000_000_000
  puts "ludicrous speed"
end

And as you may remember from episode #215, this also makes ranges useful with the Enumerable#grep method. For instance, we can filter out some outlier readings using #grep and a range of allowable values.

readings = [26, 72, 9000, 8, 17, -3400]
readings.grep(0..100)
# => [26, 72, 8, 17]

So far we've just been using numbers as our beginning and ending values, but ranges are not limited to using numbers. In fact, we can use any comparable value in a range. For instance, we can check to see if a string is a lower-case ASCII character with a range from 'a' to 'z'.

("a".."z").include?("x")        # => true

One particularly useful type to use in a Range is Time. For instance, here's a range that represents this week.

this_week = Time.new(2014, 7, 20)...Time.new(2014, 7, 27)

Let's ask it if it includes today.

this_week = Time.new(2014, 7, 20)...Time.new(2014, 7, 27)
this_week.include?(Time.new(2014, 7, 22))
  # =>
# ~> -:2:in `each': can't iterate from Time (TypeError)
# ~>    from -:2:in `include?'
# ~>    from -:2:in `include?'
# ~>    from -:2:in `<main>'

As we can see, there is a complication. We get an error "can't iterate from Time". This message reveals a little of how Range works. When we ask a Range of something other than basic integers or strings whether it includes some value, it makes the determination by iterating from the start value to the end value and seeing if any of the increments match the given value.

But this means it has to know how to increment the start value. It uses the #succ method for this. As we can see, this method is implemented for integers and strings. Technically it is also implemented for Time, but this is deprecated. The deprecation is presumably because the successor for a given point in time is ambiguous; is it the next second? The next hour? The next day?

As a result of this deprecation, Ruby doesn't allow Times to be iterated in Ranges.

1.succ                          # => 2
"a".succ                        # => "b"
Time.now.succ                   # => 2014-07-22 11:50:03 -0400 # !> Time#succ is obsolete; use time + 1

This might seem like it would make Time ranges useless. But there is another predicate we can use instead of #include?, called #cover?. This predicate doesn't try to interpolate the values in between the beginning and the end of the range. Instead it just checks that the given value is between the beginning and end values, respecting usual rules for inclusive or exclusive ranges.

this_week = Time.new(2014, 7, 20)...Time.new(2014, 7, 27)
this_week.cover?(Time.new(2014, 7, 22))
  # => true

Here's a hash representing a set of calendar entries. We can use our Time range to select only the entries for this week.

marvin_calendar = {
  Time.new(2014, 7, 20) => "Mope",
  Time.new(2014, 7, 22) => "Rust",
  Time.new(2014, 7, 28) => "Write sad poem"
}

this_week = Time.new(2014, 7, 20)...Time.new(2014, 7, 27)

marvin_calendar.select{|date, _| this_week.cover?(date) }.values
# => ["Mope", "Rust"]

For ranges that can be iterated, Ruby provides an #each method that we can use to iterate over every value in the range.

(0..10).each do |n|
  puts n
end
# >> 0
# >> 1
# >> 2
# >> 3
# >> 4
# >> 5
# >> 6
# >> 7
# >> 8
# >> 9
# >> 10

Along with #each comes all the usual Enumerable methods. This means, for instance, that we can easily expand an iterable Range into an array.

("a".."z").to_a
# => ["a",
#     "b",
#     "c",
#     "d",
#     "e",
#     "f",
#     "g",
#     "h",
#     "i",
#     "j",
#     "k",
#     "l",
#     "m",
#     "n",
#     "o",
#     "p",
#     "q",
#     "r",
#     "s",
#     "t",
#     "u",
#     "v",
#     "w",
#     "x",
#     "y",
#     "z"]

What's that you say? You want to iterate over a numeric range by some increment other than 1? Range can accommodate that. Instead of #each, we use #step.

(0..10).step(2) do |n|
  puts n
end
# >> 0
# >> 2
# >> 4
# >> 6
# >> 8
# >> 10

Without a block, this returns an enumerator which also has the full suite of Enumerable methods.

(0..10).step(2).to_a
# => [0, 2, 4, 6, 8, 10]

Alright, this has been a lot to go over, so it's time to bring this episode to a close. Hopefully these examples have given you some ideas about how to make your code more expressive by making greater use of Ranges. Happy hacking!

Responses