[Bug #21030] Fix step for non-numeric range

When the end points of an inclusive range equal, `Range#step` should
yields the element once.
This commit is contained in:
Nobuyoshi Nakada 2025-01-13 02:07:05 +09:00
parent d9e1a7cdf8
commit f56f3eaae5
No known key found for this signature in database
GPG Key ID: 3582D74E1FEE4465
Notes: git 2025-01-12 17:47:22 +00:00
2 changed files with 62 additions and 41 deletions

87
range.c
View File

@ -488,6 +488,7 @@ range_step(int argc, VALUE *argv, VALUE range)
b = RANGE_BEG(range);
e = RANGE_END(range);
v = b;
const VALUE b_num_p = rb_obj_is_kind_of(b, rb_cNumeric);
const VALUE e_num_p = rb_obj_is_kind_of(e, rb_cNumeric);
@ -559,7 +560,8 @@ range_step(int argc, VALUE *argv, VALUE range)
rb_yield(LONG2NUM(i));
i += unit;
}
} else {
}
else {
if (!EXCL(range))
end += 1;
i = FIX2LONG(b);
@ -571,7 +573,8 @@ range_step(int argc, VALUE *argv, VALUE range)
}
else if (b_num_p && step_num_p && ruby_float_step(b, e, step, EXCL(range), TRUE)) {
/* done */
} else if (!NIL_P(str_b) && FIXNUM_P(step)) {
}
else if (!NIL_P(str_b) && FIXNUM_P(step)) {
// backwards compatibility behavior for String only, when no step/Integer step is passed
// See discussion in https://bugs.ruby-lang.org/issues/18368
@ -583,7 +586,8 @@ range_step(int argc, VALUE *argv, VALUE range)
else {
rb_str_upto_each(str_b, e, EXCL(range), step_i, (VALUE)iter);
}
} else if (!NIL_P(sym_b) && FIXNUM_P(step)) {
}
else if (!NIL_P(sym_b) && FIXNUM_P(step)) {
// same as above: backward compatibility for symbols
VALUE iter[2] = {INT2FIX(1), step};
@ -594,47 +598,48 @@ range_step(int argc, VALUE *argv, VALUE range)
else {
rb_str_upto_each(sym_b, rb_sym2str(e), EXCL(range), sym_step_i, (VALUE)iter);
}
} else {
v = b;
if (!NIL_P(e)) {
if (b_num_p && step_num_p && r_less(step, INT2FIX(0)) < 0) {
// iterate backwards, for consistency with ArithmeticSequence
if (EXCL(range)) {
for (; r_less(e, v) < 0; v = rb_funcall(v, id_plus, 1, step))
rb_yield(v);
}
else {
for (; (c = r_less(e, v)) <= 0; v = rb_funcall(v, id_plus, 1, step)) {
rb_yield(v);
if (!c) break;
}
}
} else {
// Direction of the comparison. We use it as a comparison operator in cycle:
// if begin < end, the cycle performs while value < end (iterating forward)
// if begin > end, the cycle performs while value > end (iterating backward with
// a negative step)
dir = r_less(b, e);
// One preliminary addition to check the step moves iteration in the same direction as
// from begin to end; otherwise, the iteration should be empty.
if (r_less(b, rb_funcall(b, id_plus, 1, step)) == dir) {
if (EXCL(range)) {
for (; r_less(v, e) == dir; v = rb_funcall(v, id_plus, 1, step))
rb_yield(v);
}
else {
for (; (c = r_less(v, e)) == dir || c == 0; v = rb_funcall(v, id_plus, 1, step)) {
rb_yield(v);
if (!c) break;
}
}
}
}
else if (NIL_P(e)) {
// endless range
for (;; v = rb_funcall(v, id_plus, 1, step))
rb_yield(v);
}
else if (b_num_p && step_num_p && r_less(step, INT2FIX(0)) < 0) {
// iterate backwards, for consistency with ArithmeticSequence
if (EXCL(range)) {
for (; r_less(e, v) < 0; v = rb_funcall(v, id_plus, 1, step))
rb_yield(v);
}
else {
for (; (c = r_less(e, v)) <= 0; v = rb_funcall(v, id_plus, 1, step)) {
rb_yield(v);
if (!c) break;
}
}
else
for (;; v = rb_funcall(v, id_plus, 1, step))
}
else if ((dir = r_less(b, e)) == 0) {
if (!EXCL(range)) {
rb_yield(v);
}
}
else if (dir == r_less(b, rb_funcall(b, id_plus, 1, step))) {
// Direction of the comparison. We use it as a comparison operator in cycle:
// if begin < end, the cycle performs while value < end (iterating forward)
// if begin > end, the cycle performs while value > end (iterating backward with
// a negative step)
// One preliminary addition to check the step moves iteration in the same direction as
// from begin to end; otherwise, the iteration should be empty.
if (EXCL(range)) {
for (; r_less(v, e) == dir; v = rb_funcall(v, id_plus, 1, step))
rb_yield(v);
}
else {
for (; (c = r_less(v, e)) == dir || c == 0; v = rb_funcall(v, id_plus, 1, step)) {
rb_yield(v);
if (!c) break;
}
}
}
return range;
}

View File

@ -594,6 +594,22 @@ class TestRange < Test::Unit::TestCase
assert_equal(4, (1.0...5.6).step(1.5).to_a.size)
end
def test_step_with_nonnumeric_endpoint
num = Data.define(:value) do
def coerce(o); [o, 100]; end
def <=>(o) value<=>o; end
def +(o) with(value: value + o) end
end
i = num.new(100)
assert_equal([100], (100..100).step(10).to_a)
assert_equal([], (100...100).step(10).to_a)
assert_equal([100], (100..i).step(10).to_a)
assert_equal([i], (i..100).step(10).to_a)
assert_equal([], (100...i).step(10).to_a)
assert_equal([], (i...100).step(10).to_a)
end
def test_each
a = []
(0..10).each {|x| a << x }