summaryrefslogtreecommitdiff
path: root/range.c
diff options
context:
space:
mode:
authorzverok <[email protected]>2024-08-25 20:58:47 +0300
committerAkinori Musha <[email protected]>2024-09-03 16:21:42 +0900
commit245ed2fc890f7f33526e1c0f42b79c30366493e6 (patch)
tree8879a308984a12dcb53580543fa1a20c638f026f /range.c
parent452eb24b1566f3fd8319e9eeb0ece6758eace08d (diff)
Range#step: restore legacy behavior for String ranges
Notes
Notes: Merged: https://github.com/ruby/ruby/pull/11454
Diffstat (limited to 'range.c')
-rw-r--r--range.c60
1 files changed, 56 insertions, 4 deletions
diff --git a/range.c b/range.c
index 536568ba76..a52418d2cb 100644
--- a/range.c
+++ b/range.c
@@ -309,6 +309,34 @@ range_each_func(VALUE range, int (*func)(VALUE, VALUE), VALUE arg)
}
}
+// NB: Two functions below (step_i_iter and step_i) are used only to maintain the
+// backward-compatible behavior for string ranges with integer steps. If that branch
+// will be removed from range_step, these two can go, too.
+static bool
+step_i_iter(VALUE arg)
+{
+ VALUE *iter = (VALUE *)arg;
+
+ if (FIXNUM_P(iter[0])) {
+ iter[0] -= INT2FIX(1) & ~FIXNUM_FLAG;
+ }
+ else {
+ iter[0] = rb_funcall(iter[0], '-', 1, INT2FIX(1));
+ }
+ if (iter[0] != INT2FIX(0)) return false;
+ iter[0] = iter[1];
+ return true;
+}
+
+static int
+step_i(VALUE i, VALUE arg)
+{
+ if (step_i_iter(arg)) {
+ rb_yield(i);
+ }
+ return 0;
+}
+
static int
discrete_object_p(VALUE obj)
{
@@ -430,9 +458,18 @@ range_step_size(VALUE range, VALUE args, VALUE eobj)
*
* For non-Numeric ranges, step absence is an error:
*
- * ('a'..'z').step { p _1 }
+ * (Time.utc(2022, 3, 1)..Time.utc(2022, 2, 24)).step { p _1 }
* # raises: step is required for non-numeric ranges (ArgumentError)
*
+ * For backward compatibility reasons, String ranges support the iteration both with
+ * string step and with integer step. In the latter case, the iteration is performed
+ * by calculating the next values with String#succ:
+ *
+ * ('a'..'e').step(2) { p _1 }
+ * # Prints: a, c, e
+ * ('a'..'e').step { p _1 }
+ * # Default step 1; prints: a, b, c, d, e
+ *
*/
static VALUE
range_step(int argc, VALUE *argv, VALUE range)
@@ -445,11 +482,15 @@ range_step(int argc, VALUE *argv, VALUE range)
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);
+ // For backward compatibility reasons (conforming to behavior before 3.4), String supports
+ // both old behavior ('a'..).step(1) and new behavior ('a'..).step('a')
+ // Hence the additional conversion/addional checks.
+ const VALUE sb = rb_check_string_type(b);
if (rb_check_arity(argc, 0, 1))
step = argv[0];
else {
- if (b_num_p || (NIL_P(b) && e_num_p))
+ if (b_num_p || !NIL_P(sb) || (NIL_P(b) && e_num_p))
step = INT2FIX(1);
else
rb_raise(rb_eArgError, "step is required for non-numeric ranges");
@@ -520,8 +561,19 @@ 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 {
+ } else if (!NIL_P(sb) && 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
+
+ VALUE iter[2] = {INT2FIX(1), step};
+
+ if (NIL_P(e)) {
+ rb_str_upto_endless_each(sb, step_i, (VALUE)iter);
+ }
+ else {
+ rb_str_upto_each(sb, e, EXCL(range), step_i, (VALUE)iter);
+ }
+ } else {
v = b;
if (!NIL_P(e)) {
if (b_num_p && step_num_p && r_less(step, INT2FIX(0)) < 0) {