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));
}
}