crystal
crystal copied to clipboard
Strided subranges for `Array#[]` and `Range`-accepting methods
In Python, indexing a collection with a stepped range creates a strided subrange (or rather, ranges have a step of 1 by default):
# all end values are exclusive
x = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
x[2:8:2] # => [4, 16, 36]
x[2::2] # => [4, 16, 36, 64]
x[0:10:3] # => [4, 16, 36, 64]
x[8:2:-1] # => [64, 49, 36, 25, 16, 9]
x[8:2:-2] # => [64, 36, 16]
x[4::-1] # => [16, 9, 4, 1, 0]
x[:2:-1] # => [81, 64, 49, 36, 25, 16, 9]
x[:2:-2] # => [81, 49, 25, 9]
# unbounded ranges are allowed
x[::3] # => [0, 9, 36, 81]
x[::-1] # => [81, 64, 49, 36, 25, 16, 9, 4, 1, 0]
# anything that responds to `__getitem__` is supported
'abc'[2:0:-1] # => 'cb'
In Ruby, support for Array#[](Enumerator::ArithmeticSequence)
has apparently been added since 3.0.0, whereas String
doesn't work. Note that the two languages handle the end points differently:
# end values may be inclusive
x = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
x[(2..8).step(2)] # => [4, 16, 36, 64]
x[(2..).step(2)] # => [4, 16, 36, 64]
x[(0..10).step(3)] # raises RangeError
x[(8..2).step(-1)] # => [64, 49, 36, 25, 16, 9, 4]
x[(8..2).step(-2)] # => [64, 36, 16, 4]
x[(4..).step(-1)] # => [16, 9, 4, 1, 0]
x[(..2).step(-1)] # => [81, 64, 49, 36, 25, 16, 9, 4]
x[(..2).step(-2)] # => [81, 49, 25, 9]
# end values may be exclusive
x[(2...8).step(2)] # => [4, 16, 36]
x[(2...).step(2)] # => [4, 16, 36, 64]
x[(0...10).step(3)] # => [0, 9, 36, 81]
# if step is negative, _begin_ values, not end values, may be exclusive
x[(8...2).step(-1)] # => [49, 36, 25, 16, 9, 4]
x[(8...2).step(-2)] # => [49, 25, 9]
x[(4...).step(-1)] # => [9, 4, 1, 0]
x[(...2).step(-1)] # => [81, 64, 49, 36, 25, 16, 9, 4]
x[(...2).step(-2)] # => [81, 49, 25, 9]
x[Range.new(nil, nil).step(-1)] # raises TypeError
'abc'[(2..0).step(-1)] # raises TypeError
# the above results are inconsistent with `Range#each` for finite ranges;
# when indexed this way the exclusive ranges behave like in python
x.values_at(*(8..2).step(-1)) # => [64, 49, 36, 25, 16, 9, 4]
x.values_at(*(8...2).step(-1)) # => [64, 49, 36, 25, 16, 9]
Elixir is also getting it soon. I do not know which convention it follows.
I wonder how useful this feature is and whether we need it. It seems to be relatively popular among numerical processing libraries. At a minimum Array#[]
would support this, but this easily extends to similar methods like String#byte_slice
.
Range(B, E)#step(T)
produces a Steppable::StepIterator(B, E, T)
in Crystal. For most ranges B
and T
are Int32
, whereas E
is Int32
or Nil
depending on whether the range is endless. Beginless stepped ranges cannot be formed in Crystal, so extra work would have be done on Range
apart from adding new overloads.
For some reason, these ranges raise RangeError
in Ruby even though their unstepped counterparts merely return nil
. For a positive step larger than 1, a range raises if begin < -size || begin > size || (exclusive? ? end - begin > size : end - begin >= size)
. For a negative step begin
and end
are swapped. (This is most likely what happens not only for range checking but also for the actual indexing.)