Hi folks, The NumberRange abstraction tries very hard to allow any Number numeric type to be used but since the Number interface doesn't convey much behavior, there are a few places where it defaults to using Groovy's NumberMath plumbing which, to cut a long story short, falls back to using BigDecimal for any numeric calculations which aren't using the common known simpler types.
A consequence of this is that currently if you created a range using e.g. the Apache Commons Fraction class (which does extend Number), and used a Fraction stepSize, the values in the range would be one Fraction (for the first element) and then subsequent elements would be BigDecimals. @Grab('org.apache.commons:commons-lang3:3.14.0') import org.apache.commons.lang3.math.Fraction def r = (Fraction.ONE..2).by(Fraction.ONE_QUARTER) println r.toList() // => [1/1, 1.25, 1.50, 1.75, 2.00] This isn't incorrect in one sense but is somewhat surprising. Given that the Number interface doesn't have operators, providing a smarter detection of the number system to use becomes somewhat tricky. One thing we could do is provide some interface that providers could use and we could have a "Fraction" math implementation that satisfied that interface. Alternatively, we could supply some [Bi]Functions that offered the supplied behavior that the StepIterator needs when calculating subsequent elements in the range. With this second approach, we could do something like: @Grab('org.apache.commons:commons-lang3:3.14.0') import org.apache.commons.lang3.math.Fraction (Fraction.ONE..2).by(Fraction.ONE_QUARTER, Fraction::add, Fraction::subtract, Fraction::negate).toList() Which gives a list of all Fraction instances: [1/1, 5/4, 3/2, 7/4, 2/1] Is this something we should support? Does anyone have ideas on the best implementation? Patch of a prototype is provided below. I can turn into a PR with tests but I am trying to gauge what folks think first. Thoughts? Paul. =========== >8 ============= Subject: [PATCH] NumberRangeTweaks --- Index: src/main/java/groovy/lang/NumberRange.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/src/main/java/groovy/lang/NumberRange.java b/src/main/java/groovy/lang/NumberRange.java --- a/src/main/java/groovy/lang/NumberRange.java (revision 3cd76364f772250324f5729ef93ffd76fbdd2b79) +++ b/src/main/java/groovy/lang/NumberRange.java (date 1704856055407) @@ -31,6 +31,8 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; +import java.util.function.BiFunction; +import java.util.function.Function; import static org.codehaus.groovy.runtime.ScriptBytecodeAdapter.compareEqual; import static org.codehaus.groovy.runtime.ScriptBytecodeAdapter.compareGreaterThan; @@ -94,6 +96,9 @@ * <code>true</code> if the range includes the upper bound. */ private final boolean inclusiveRight; + private BiFunction<Number, Number, Number> increment = null; + private BiFunction<Number, Number, Number> decrement = null; + private Function<Number, Number> negate = null; /** * Creates an inclusive {@link NumberRange} with step size 1. @@ -246,6 +251,17 @@ return new NumberRange(comparableNumber(from), comparableNumber(to), stepSize, inclusiveLeft, inclusiveRight); } + public <T extends Number & Comparable> NumberRange by(T stepSize, BiFunction<Number, Number, Number> increment, BiFunction<Number, Number, Number> decrement, Function<Number, Number> negate) { + if (!Integer.valueOf(1).equals(this.stepSize)) { + throw new IllegalStateException("by only allowed on ranges with original stepSize = 1 but found " + this.stepSize); + } + NumberRange result = new NumberRange(comparableNumber(from), comparableNumber(to), stepSize, inclusiveLeft, inclusiveRight); + result.increment = increment; + result.decrement = decrement; + result.negate = negate; + return result; + } + @SuppressWarnings("unchecked") /* package private */ static <T extends Number & Comparable> T comparableNumber(Comparable c) { return (T) c; @@ -617,7 +633,7 @@ this.range = range; if (compareLessThan(step, 0)) { - this.step = multiply(step, -1); + this.step = negate != null ? negate.apply(step) : multiply(step, -1); isAscending = range.isReverse(); } else { this.step = step; @@ -691,7 +707,7 @@ */ @SuppressWarnings("unchecked") private Comparable increment(Object value, Number step) { - return (Comparable) plus((Number) value, step); + return (Comparable) (increment != null ? increment.apply((Number) value, step) : plus((Number) value, step)); } /** @@ -703,6 +719,6 @@ */ @SuppressWarnings("unchecked") private Comparable decrement(Object value, Number step) { - return (Comparable) minus((Number) value, step); + return (Comparable) (decrement != null ? decrement.apply((Number) value, step) : minus((Number) value, step)); } }