(No) fun with ranges

As @nerdblogpl noticed and then published, there seems to be something wrong with how ranges are handled in ruby. I decided to go on a “range trip” myself.

As we know, there are two types of ranges - inclusive and exclusive. They are supposed to provide convenience for programmer to choose which type suits better. But do they really?

> (1..5).to_a
=> [1, 2, 3, 4, 5]
> (1...6).to_a
=> [1, 2, 3, 4, 5]
> (1..5) == (1...6)
=> false
> (1..5).to_a == (1...6).to_a
=> true

OK, not so cool, but I expected that. They are quite primitive after all. Let’s try the other way.

[10] pry(main)> (1..5).end
=> 5
[11] pry(main)> (1...6).end
=> 6

At first it’s a big WHAAAAT?, but then I read the docs:

end → obj
Returns the object that defines the end of the range.

All right, in fact 6 really defines the end of exclusive 1...6 range, so this is cool. So I tried with last, which, as the docs state, returns the last object in the range, or an array of the last n elements.

> (1..5).last
=> 5
> (1...6).last
=> 6
> (1..5).last(1)
=> [5]
> (1...6).last(1)
=> [5]

And that’s the real WTF. It works differently with and without arguments. And only when returning the array, it actually does what docs say. This becomes obvious when I looked at the source:

               static VALUE
range_last(int argc, VALUE *argv, VALUE range)
    if (argc == 0) return RANGE_END(range);
    return rb_ary_last(argc, argv, rb_Array(range));

As we see, if we supply and argument to last it converts range to array and returns last n elements!

As Michał also noticed, there is no built-in way to convert inclusive range into exclusive or vice-versa, so if you want to compare ranges and you are not sure what type are they, you seem to have four options:

  • convert them to arrays and compare
  • use x.begin == y.begin && x.last(1).first == y.last(1).first, which converts them to arrays under the hood
  • tamper with exclude_end? in your condition
  • wrap them in custom class which makes sure that we use a certain type of range

Related posts