Issue #18368 has been updated by zverok (Victor Shepelev).
The PR is here:
https://github.com/ruby/ruby/pull/7444
Clarification of semantics led to a few minor changes of behavior for numeric steps, too:
* Consistent support for negative step:
```ruby
p (1..-10).step(-3).to_a
#=> [1, -2, -5, -8] -- ArithmeticSequence backward iteration, on Ruby 3.2 and master
(1..-10).step(-3) { p _1 }
# Ruby 3.2: step can't be negative (ArgumentError) -- inconsistent with
ArithmeticSequence behavior
# master: prints 1, -2, -5, -8, consistent with ArithmeticSequence
```
* Less greedy float conversion:
```ruby
require 'active_support/all'
p (1.0..).step(2.minutes).take(3)
# 3.2: [1.0, 121.0, 241.0] -- forces any passed value to be float if it has #to_f
# master: [1.0, 2 minutes and 1.0 second, 4 minutes and 1.0 second] -- properly uses
step#coerce to find a suitable type
```
* Drop support for generic `#to_int`. Before, it was considered that integer is (almost)
always the intended step value, so the step tried to be converted to `#to_int` if it
wasn't numeric:
```ruby
o = Object.new
def o.to_int
2
end
p (1..6).step(o).to_a
#=> [1, 3, 5] on Ruby 3.2
# Now, no assumptions on the step are made other than it should be `+`-able to `begin`:
p (1..6).step(o).to_a
# master: `+': Object can't be coerced into Integer
# But:
def o.coerce(other)
[other, 2]
end
p (1..6).step(o).to_a
#=> [1, 3, 5] on master
```
I am open to discussing those changes, but to the best of my understanding, neither of
them should be severely breaking, and they are naturally following the change of the
semantics.
----------------------------------------
Feature #18368: Range#step semantics for non-Numeric ranges
https://bugs.ruby-lang.org/issues/18368#change-102144
* Author: zverok (Victor Shepelev)
* Status: Open
* Priority: Normal
----------------------------------------
I am sorry if the question had already been discussed, can't find the relevant topic.
"Intuitively", this looks (for me) like a meaningful statement:
```ruby
(Time.parse('2021-12-01')..Time.parse('2021-12-24')).step(1.day).to_a
# ^^^^^ or just 24*60*60
```
Unfortunately, it doesn't work with "TypeError (can't iterate from
Time)".
Initially it looked like a bug for me, but after digging a bit into code/docs, I
understood that `Range#step` has an odd semantics of "advance the begin N times with
`#succ`, and yield the result", with N being always integer:
```ruby
('a'..'z').step(3).first(5)
# => ["a", "d", "g", "j", "m"]
```
The fact that semantic is "odd" is confirmed by the fact that for Float it is
redefined to do what I "intuitively" expected:
```ruby
(1.0..7.0).step(0.3).first(5)
# => [1.0, 1.3, 1.6, 1.9, 2.2]
```
(Like with [`Range#===` some time
ago](https://bugs.ruby-lang.org/issues/14575), I believe
that to be a strong proof of the wrong generic semantics, if for numbers the semantics
needed to be redefined completely.)
Another thing to note is that "skip N elements" seem to be rather
"generically Enumerable-related" yet it isn't defined on `Enumerable`
(because nobody needs this semantics, typically!)
Hence, two questions:
* Can we redefine generic `Range#step` to new semantics (of using `begin + step`
iteratively)? It is hard to imagine the amount of actual usage of the old behavior (with
String?.. to what end?) in the wild
* If the answer is "no", can we define a new method with new semantics, like,
IDK, `Range#over(span)`?
**UPD:** More examples of useful behavior (it is NOT only about core `Time` class):
```ruby
require 'active_support/all'
(1.minute..20.minutes).step(2.minutes).to_a
#=> [1 minute, 3 minutes, 5 minutes, 7 minutes, 9 minutes, 11 minutes, 13 minutes, 15
minutes, 17 minutes, 19 minutes]
require 'tod'
(Tod::TimeOfDay.parse("8am")..Tod::TimeOfDay.parse("10am")).step(30.minutes).to_a
#=> [#<Tod::TimeOfDay 08:00:00>, #<Tod::TimeOfDay 08:30:00>,
#<Tod::TimeOfDay 09:00:00>, #<Tod::TimeOfDay 09:30:00>, #<Tod::TimeOfDay
10:00:00>]
require 'matrix'
(Vector[1, 2, 3]..).step(Vector[1, 1, 1]).take(3)
#=> [Vector[1, 2, 3], Vector[2, 3, 4], Vector[3, 4, 5]]
require 'unitwise'
(Unitwise(0, 'km')..Unitwise(1, 'km')).step(Unitwise(100,
'm')).map(&:to_s)
#=> ["0 km", "1/10 km", "1/5 km", "3/10 km",
"2/5 km", "0.5 km", "3/5 km", "7/10 km", "4/5
km", "9/10 km", "1 km"]
```
**UPD:** Responding to discussion points:
**Q:** Matz is concerned that the proposed simple definition will be confusing with the
classes where `+` is redefined as concatenation.
**A:** I believe that simplicity of semantics and ease of explaining ("it just uses
`+` underneath, whatever `+` does, will be performed") will make the confusion
minimal.
**Q:** Why not introduce new API requirement (like "class of range's `begin`
should implement `increment` method, and then it will be used in `step`)
**A:** require *every* gem author to change *every* of their objects' behavior. For
that, they should be aware of the change, consider it important enough to care, clearly
understand the necessary semantics of implementation, have a resource to release a new
version... Then all users of all such gems would be required to upgrade. The feature would
be DOA (dead-on-arrival).
The two alternative ways I am suggesting: change the behavior of `#step` or introduce a
new method with desired behavior:
1. Easy to explain and announce
2. Require no other code changes to immediately become useful
3. With something like [
backports](https://github.com/marcandre/backports) or
[
ruby-next](https://github.com/ruby-next/ruby-next) easy to start using even in older Ruby
version, making the code more expressive even before it would be possible for some
particular app/compny to upgrade to (say) 3.2
All examples of behavior from the code above are real `irb` output with monkey-patched
`Range#step`, demonstrating how little change will be needed to code outside of the
`Range`.
--
https://bugs.ruby-lang.org/