Project

General

Profile

Feature #15804

Updated by Eregon (Benoit Daloze) 3 months ago

Currently, the implementation of begin/end-less Ranges leaks the representation of infinite ranges (it's a `nil` value for `begin` or `end`).

For instance, this "works" but I think it's potentially confusing:
```ruby
s = "abcdef"
to = some_computation_that_unexpectedly_returns_nil
s[1..to] # => "bcdef"
```

Particularly problematic, this means Array#[], String#[], MatchData#[], ..., all have to handle Ranges and `nil` explicitly as a special value, in addition to already treat e.g., negative integers specially.
That's not very nice, why should `Array#[]` know how an infinite Range is represented? IMHO it should not need to (separation of concerns).

Here are the current ways to index a sequence:
```
seq[index] index can be negative, but nil is returned if too big or small
seq[start, length] start can be negative, but nil is returned if too big or small, length positive or nil is returned
seq[range] range begin and end can both be negative, but nil is returned if too big or small
```

User-defined classes have no (efficient) way to reuse that logic to resolve indices, they have to rewrite it in Ruby and given it's not trivial and tends to change (e.g., endless ranges) it might be buggy or incomplete.
For instance, maybe one day we might we'd want to support indexing with a `Enumerator::ArithmeticSequence` or even more elaborate ways.

I would like to have a standard way to resolve indexing on a 0-indexed sequence with a given size to a span of selected indices.

This is still an early idea, feedback welcome.
For instance, we could have:

```ruby
resolve(size, *args)
# Either:
# => idx, with 0<=idx<size, which means selecting a single element at index `idx`
# => [from, len] with 0<=from<=from+len<=size, which means selecting elements from `from` included to `from+len` excluded.
# => nil if indices are out of bounds
resolve(5, 3) # => 3
resolve(5, 33) # => nil
resolve(5, 1..) # => [1, 4]
resolve(5, 1...) # => [1, 4]
resolve(5, -3..) # => [2, 3]
resolve(5, ..-2) # => [0, 4]
resolve(5, -4..-2) # => [1, 3]
```

I'm not sure what a good name for this method would be, or what a good namespace would be.
Maybe an `Enumerable.resolve(size, *args)`, `(0...size).resolve(*args)` or `Integer.indexing(size, *args)`?

Such functionality can currently be worked around by reusing `Array#[]` with `size.times.to_a(*args)`, but that's of course quite inefficient.

A smaller-scoped version of this would be to just have a way to resolve a Range object with a size like:
```ruby
(-2...).resolve(5) # => [3, 2]
```
That part would at least let Range take care of resolving indices and begin/end-less ranges, instead of duplicating that logic in many places.

Back