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

Reply via email to