(No) fun with ranges
by Paweł Świątkowski
08 Aug 2014
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