5858import org .jruby .runtime .Signature ;
5959import org .jruby .runtime .ThreadContext ;
6060
61+ import static org .jruby .RubyEnumerator .enumeratorize ;
6162import static org .jruby .RubyEnumerator .enumeratorizeWithSize ;
6263import static org .jruby .RubyNumeric .*;
6364import static org .jruby .api .Convert .*;
@@ -755,31 +756,11 @@ public IRubyObject step(final ThreadContext context, final Block block) {
755756
756757 @ JRubyMethod (name = "step" )
757758 public IRubyObject step (final ThreadContext context , IRubyObject step , final Block block ) {
758- String method = "step" ;
759- if (!block .isGiven ()) {
760- return stepEnumeratorize (context , step , method );
761- }
762-
763- step = checkStepDomain (context , step , method );
764-
765759 return stepCommon (context , step , block );
766760 }
767761
768- private IRubyObject checkStepDomain (ThreadContext context , IRubyObject step , String method ) {
769- if (!(step instanceof RubyNumeric )) step = step .convertToInteger ("to_int" );
770- if (((RubyNumeric ) step ).isNegative (context )) throw argumentError (context , method + " can't be negative" );
771- if (((RubyNumeric ) step ).isZero (context )) throw argumentError (context , method + " can't be 0" );
772-
773- return step ;
774- }
775-
776762 private IRubyObject stepEnumeratorize (ThreadContext context , IRubyObject step , String method ) {
777- if (!step .isNil () && !(step instanceof RubyNumeric )) step = step .convertToInteger ("to_int" );
778- if ((step instanceof RubyNumeric num ) && num .isZero (context )) throw argumentError (context , "step can't be 0" );
779-
780- if ((begin instanceof RubyNumeric && (end .isNil () || end instanceof RubyNumeric )) ||
781- (end instanceof RubyNumeric && begin .isNil ())) {
782-
763+ if (step instanceof RubyNumeric && (begin instanceof RubyNumeric && (end .isNil () || end instanceof RubyNumeric )) || (begin .isNil () && end instanceof RubyNumeric )) {
783764 return RubyArithmeticSequence .newArithmeticSequence (
784765 context ,
785766 this ,
@@ -791,9 +772,14 @@ private IRubyObject stepEnumeratorize(ThreadContext context, IRubyObject step, S
791772 isExclusive ? context .tru : context .fals );
792773 }
793774
775+ // ...but generic Enumerator from beginless range is useless and probably an error.
776+ if (begin .isNil ()) {
777+ throw argumentError (context , "#step for non-numeric beginless ranges is meaningless" );
778+ }
779+
794780 return !step .isNil () ?
795- enumeratorizeWithSize (context , this , method , new IRubyObject []{ step }, RubyRange :: stepSize ) :
796- enumeratorizeWithSize (context , this , method , RubyRange :: stepSize );
781+ enumeratorize (context . runtime , this , method , step ) :
782+ enumeratorize (context . runtime , this , method );
797783 }
798784
799785 @ JRubyMethod (name = "%" )
@@ -802,81 +788,148 @@ public IRubyObject op_mod(final ThreadContext context, IRubyObject step) {
802788 }
803789
804790 private IRubyObject stepCommon (ThreadContext context , IRubyObject step , Block block ) {
805- Ruby runtime = context .runtime ;
806- if (begin instanceof RubyFixnum && end .isNil () && step instanceof RubyFixnum ) {
807- long i = begin .convertToInteger ().getLongValue ();
808- long unit = step .convertToInteger ().getLongValue ();
809- while (i < Long .MAX_VALUE ) {
810- block .yield (context , asFixnum (context , i ));
811- i += unit ;
812- }
813- IRubyObject b = asFixnum (context , i );
814- for (;; b = ((RubyInteger ) b ).op_plus (context , step )) {
815- block .yield (context , b );
791+ boolean beginIsNumeric = begin instanceof RubyNumeric ;
792+ boolean endIsNumeric = end instanceof RubyNumeric ;
793+ // For backward compatibility reasons (conforming to behavior before 3.4), String/Symbol
794+ // supports both old behavior ('a'..).step(1) and new behavior ('a'..).step('a')
795+ // Hence the additional conversion/additional checks.
796+ IRubyObject strBegin = begin .checkStringType ();
797+ IRubyObject symBegin = begin instanceof RubySymbol symbol ? symbol .to_s (context ) : context .nil ;
798+
799+ if (step .isNil ()) {
800+ if (beginIsNumeric || !strBegin .isNil () || !symBegin .isNil () || (begin .isNil () && endIsNumeric )) {
801+ step = asFixnum (context , 1 );
802+ } else {
803+ throw argumentError (context , "step is required for non-numeric ranges" );
816804 }
805+ }
806+
807+ boolean stepIsNumeric = step instanceof RubyNumeric ;
808+
809+ if (stepIsNumeric && beginIsNumeric && step .op_eqq (context , asFixnum (context , 0 )).isTrue ()) {
810+ throw argumentError (context , "step can't be 0" );
811+ }
812+
813+ if (!block .isGiven ()) {
814+ return stepEnumeratorize (context , step , "step" );
815+ }
816+
817+ if (begin .isNil ()) {
818+ throw argumentError (context , "#step iteration for beginless ranges is meaningless" );
819+ }
820+
821+ if (begin instanceof RubyFixnum && end .isNil () && step instanceof RubyFixnum ) {
822+ fixnumEndlessStep (context , step , block );
817823 } else if (begin instanceof RubyFixnum && end instanceof RubyFixnum && step instanceof RubyFixnum ) {
818824 fixnumStep (context , ((RubyFixnum ) step ).getLongValue (), block );
819- } else if (begin instanceof RubyFloat || end instanceof RubyFloat || step instanceof RubyFloat ) {
820- RubyNumeric .floatStep (context , runtime , begin , end , step , isExclusive , isEndless , block );
821- } else if (begin instanceof RubySymbol && (end .isNil () || end instanceof RubySymbol )) { /* symbols are special */
822- RubyString b = begin .asString ();
823- SymbolStepBlockCallBack callback = new SymbolStepBlockCallBack (block , RubyFixnum .one (runtime ), step );
824- Block blockCallback = CallBlock .newCallClosure (context , this , Signature .ONE_ARGUMENT , callback );
825- if (end .isNil ()) {
826- b .uptoEndless (context , blockCallback );
827- } else {
828- b .uptoCommon (context , end .asString (), isExclusive , blockCallback );
829- }
830- } else if (begin instanceof RubyNumeric
831- || !checkToInteger (context , begin ).isNil ()
832- || !checkToInteger (context , end ).isNil ()) {
833- numericStep (context , step , block );
825+ } else if (beginIsNumeric && endIsNumeric && floatStep (context , begin , end , step , isExclusive , isEndless , block )) {
826+ /* done */
827+ } else if (!strBegin .isNil () && step instanceof RubyFixnum ) {
828+ // backwards compatibility behavior for String only, when no step/Integer step is passed
829+ // See discussion in https://bugs.ruby-lang.org/issues/18368
830+ stringStep (context , step , block , (RubyString ) strBegin );
831+ } else if (!symBegin .isNil () && step instanceof RubyFixnum ) {
832+ // same as above: backward compatibility for symbols
833+ symbolStep (context , step , block , (RubyString ) symBegin );
834834 } else {
835- IRubyObject tmp = begin .checkStringType ();
836- if (!tmp .isNil ()) {
837- StepBlockCallBack callback = new StepBlockCallBack (block , RubyFixnum .one (runtime ), step );
838- Block blockCallback = CallBlock .newCallClosure (context , this , Signature .ONE_ARGUMENT , callback );
839- if (end .isNil ()) {
840- ((RubyString ) tmp ).uptoEndless (context , blockCallback );
835+ int c , dir ;
836+ IRubyObject v = begin ;
837+ if (!end .isNil ()) {
838+ if (beginIsNumeric && stepIsNumeric && rangeLess (context , step , asFixnum (context , 0 )) < 0 ) {
839+ // iterate backwards, for consistency with ArithmeticSequence
840+ if (isExclusive ) {
841+ for (; rangeLess (context , end , v ) < 0 ; v = v .callMethod (context , "+" , step )) {
842+ block .yield (context , v );
843+ }
844+ } else {
845+ for (; (c = rangeLess (context , end , v )) <= 0 ; v = v .callMethod (context , "+" , step )) {
846+ block .yield (context , v );
847+ if (c == 0 ) break ;
848+ }
849+ }
850+
841851 } else {
842- ((RubyString ) tmp ).uptoCommon (context , end , isExclusive , blockCallback );
852+ // Direction of the comparison. We use it as a comparison operator in cycle:
853+ // if begin < end, the cycle performs while value < end (iterating forward)
854+ // if begin > end, the cycle performs while value > end (iterating backward with
855+ // a negative step)
856+ dir = rangeLess (context , begin , end );
857+ // One preliminary addition to check the step moves iteration in the same direction as
858+ // from begin to end; otherwise, the iteration should be empty.
859+ if (rangeLess (context , begin , begin .callMethod (context , "+" , step )) == dir ) {
860+ if (isExclusive ) {
861+ for (; rangeLess (context , v , end ) == dir ; v = v .callMethod (context , "+" , step )) {
862+ block .yield (context , v );
863+ }
864+ } else {
865+ for (; (c = rangeLess (context , v , end )) == dir || c == 0 ; v = v .callMethod (context , "+" , step )) {
866+ block .yield (context , v );
867+ if (c == 0 ) break ;
868+ }
869+ }
870+ }
843871 }
844872 } else {
845- if (!begin .respondsTo ("succ" )) throw typeError (context , "can't iterate from " , begin , "" );
846-
847- // range_each_func(range, step_i, b, e, args);
848- rangeEach (context , new StepBlockCallBack (block , RubyFixnum .one (runtime ), step ));
873+ for (; ; v = v .callMethod (context , "+" , step )) {
874+ block .yield (context , v );
875+ }
849876 }
850877 }
851878 return this ;
852879 }
853880
881+ private void fixnumEndlessStep (ThreadContext context , IRubyObject step , Block block ) {
882+ long i = begin .convertToInteger ().getLongValue ();
883+ long unit = step .convertToInteger ().getLongValue ();
884+ while (i < Long .MAX_VALUE ) {
885+ block .yield (context , asFixnum (context , i ));
886+ i += unit ;
887+ }
888+ IRubyObject b = asFixnum (context , i );
889+ for (;; b = ((RubyInteger ) b ).op_plus (context , step )) {
890+ block .yield (context , b );
891+ }
892+ }
893+
854894 private void fixnumStep (ThreadContext context , long step , Block block ) {
855- // We must avoid integer overflows.
856- // Any method calling this method must ensure that "step" is greater than 0.
857- long to = ((RubyFixnum ) end ).getLongValue ();
858- if (isExclusive ) {
859- if (to == Long .MIN_VALUE ) return ;
860- to --;
895+ long end = fix2long (this .end );
896+ long i , unit = step ;
897+ if (unit < 0 ) {
898+ if (!isExclusive )
899+ end -= 1 ;
900+ i = fix2long (begin );
901+ while (i > end ) {
902+ block .yield (context , asFixnum (context , i ));
903+ i += unit ;
904+ }
905+ } else {
906+ if (!isExclusive )
907+ end += 1 ;
908+ i = fix2long (begin );
909+ while (i < end ) {
910+ block .yield (context , asFixnum (context , i ));
911+ i += unit ;
912+ }
861913 }
862- long tov = Long .MAX_VALUE - step ;
863- if (to < tov ) tov = to ;
914+ }
864915
865- long i ;
866- for (i = ((RubyFixnum ) begin ).getLongValue (); i <= tov ; i += step ) {
867- block .yield (context , asFixnum (context , i ));
916+ private void stringStep (ThreadContext context , IRubyObject step , Block block , RubyString strBegin ) {
917+ StepBlockCallBack callback = new StepBlockCallBack (block , RubyFixnum .one (context .runtime ), step );
918+ Block blockCallback = CallBlock .newCallClosure (context , this , Signature .ONE_ARGUMENT , callback );
919+ if (end .isNil ()) {
920+ strBegin .uptoEndless (context , blockCallback );
921+ } else {
922+ strBegin .uptoCommon (context , end , isExclusive , blockCallback );
868923 }
869- if (i <= to ) block .yield (context , asFixnum (context , i ));
870924 }
871925
872- private void numericStep (ThreadContext context , IRubyObject step , Block block ) {
873- final String method = isExclusive ? "<" : "<=" ;
874- IRubyObject beg = begin ;
875- long i = 0 ;
876- while (beg .callMethod (context , method , end ).isTrue ()) {
877- block .yield (context , beg );
878- i ++;
879- beg = begin .callMethod (context , "+" , asFixnum (context , i ).callMethod (context , "*" , step ));
926+ private void symbolStep (ThreadContext context , IRubyObject step , Block block , RubyString symBegin ) {
927+ SymbolStepBlockCallBack callback = new SymbolStepBlockCallBack (block , RubyFixnum .one (context .runtime ), step );
928+ Block blockCallback = CallBlock .newCallClosure (context , this , Signature .ONE_ARGUMENT , callback );
929+ if (end .isNil ()) {
930+ symBegin .uptoEndless (context , blockCallback );
931+ } else {
932+ symBegin .uptoCommon (context , end .asString (), isExclusive , blockCallback );
880933 }
881934 }
882935
0 commit comments