Misc #16678

Array#values_at has unintuitive behavior when supplied a range starting with negative index

Added by prajjwal (Prajjwal Singh) 8 months ago. Updated 8 months ago.



Consider the following:

# frozen_string_literal: true

a = (1..5).to_a

p a.values_at(3..5) # => [4, 5, nil]
p a.values_at(-1..3)  # => []

When the range begins with a negative (-1, 0, 1, 2, 3), it returns an empty array, which surprised me because I was expecting [1, 2, 3, 4].

The argument for this is that it cold be confusing to allow this because the index -1 could refer to the last argument and it would be unintuitive to return an array [5, 1, 2, 3, 4] with jumbled values.

The argument against it is that it makes perfect sense to account for this case and return [nil, 1, 2, 3, 4].

Opening a dialog to see what others think of this.

Updated by shevegen (Robert A. Heiler) 8 months ago

Actually .values_at() confused me when I tried to use my go-to method for
obtaining a slice from an Array:

a[3..5] # => [4, 5]

There I wondered why it did not return the same. :-)

But anyway; I believe the question is what -1 refers to. It should be the
last entry, right? Ok, so what should the 3 indicate? I think you reason
that it should refer to the fourth entry (I think ... if an Array count
begins at 0, then 3 would refer to the fourth entry). So from that point
of view I actually do not even disagree with you; perhaps I may have
missed some other explanation. (There is probably another explanation;
I think this has come up in the past too. I forgot the explanation,
though, if there was one.)

Personally I will stick with [] an leave .values_at() to others. I am
just so used to [] there. ;-)

Updated by Eregon (Benoit Daloze) 8 months ago

You can easily achieve wrap-around behavior with:

> (1..5).to_a.values_at(*(-1..3))
=> [5, 1, 2, 3, 4]

Using a Range for values_at is like taking a slice with Array#[]/slice, and Array slices never wrap around (a good thing IMHO, that would be expensive to compute and confusing).

Updated by Dan0042 (Daniel DeLorme) 8 months ago

Negative indices have always meant "offset from the end" in ruby. So if you take a negative index and add the size of the array you get the "normal index" and then I think you'll see everything is pretty intuitive.

a = (1..5).to_a

# get all values from a[-4] (a[1]) to a[3]
a.values_at(-4..3) #=> [2, 3, 4]
a.values_at(1..3)  #=> [2, 3, 4]

# get all values from a[-1] (a[4]) to a[3]
a.values_at(-1..3) #=> []
a.values_at(4..3)  #=> []  #range start > range end = empty range, therefore empty array

But I think this is slightly inconsistent:

(4..6).map{ a[_1] } #=> [5, nil, nil]
a.values_at(4..6)   #=> [5, nil, nil]

(-7..-5).map{ a[_1] } #=> [nil, nil, 1]
a.values_at(-7..-5)   #=> RangeError (-7..-5 out of range), should be [nil, nil, 1] imho

Also available in: Atom PDF