You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by er...@apache.org on 2021/06/03 16:03:30 UTC

[commons-numbers] branch master updated: NUMBERS-161: Refactoring of angle normalization.

This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-numbers.git


The following commit(s) were added to refs/heads/master by this push:
     new 8e2334a  NUMBERS-161: Refactoring of angle normalization.
8e2334a is described below

commit 8e2334ac190892d3aede6edc3a44035d161d5659
Author: Gilles Sadowski <gi...@gmail.com>
AuthorDate: Thu Jun 3 18:01:44 2021 +0200

    NUMBERS-161: Refactoring of angle normalization.
---
 .../org/apache/commons/numbers/angle/Angle.java    | 59 +++++++++++--------
 .../apache/commons/numbers/angle/AngleTest.java    | 66 ++++++++++++++++------
 2 files changed, 84 insertions(+), 41 deletions(-)

diff --git a/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/Angle.java b/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/Angle.java
index a582f57..64ea55e 100644
--- a/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/Angle.java
+++ b/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/Angle.java
@@ -87,6 +87,8 @@ public abstract class Angle implements DoubleSupplier {
     public static final class Turn extends Angle {
         /** Zero. */
         public static final Turn ZERO = Turn.of(0d);
+        /** Normalizing operator (result will be within the {@code [0, 1[} interval). */
+        public static final UnaryOperator<Turn> WITHIN_0_AND_1 = normalizer(of(0d));
 
         /**
          * @param angle (in turns).
@@ -140,13 +142,13 @@ public abstract class Angle implements DoubleSupplier {
 
         /**
          * Creates an operator for normalizing/reducing an angle.
-         * The output will be within the {@code [c - 0.5, c + 0.5[} interval.
+         * The output will be within the {@code [lo, lo + 1[} interval.
          *
-         * @param c Center.
+         * @param lo Lower bound of the normalized interval.
          * @return the normalization operator.
          */
-        public static UnaryOperator<Turn> normalizer(Turn c) {
-            final Normalizer n = new Normalizer(c.value, 1d);
+        public static UnaryOperator<Turn> normalizer(Turn lo) {
+            final Normalizer n = new Normalizer(lo.value, 1d);
             return (Turn a) -> Turn.of(n.applyAsDouble(a.value));
         }
     }
@@ -161,6 +163,10 @@ public abstract class Angle implements DoubleSupplier {
         public static final Rad PI = Rad.of(Math.PI);
         /** 2&pi;. */
         public static final Rad TWO_PI = Rad.of(2 * Math.PI);
+        /** Normalizing operator (result will be within the <code>[0, 2&pi;[</code> interval). */
+        public static final UnaryOperator<Rad> WITHIN_0_AND_2PI = normalizer(of(0d));
+        /** Normalizing operator (result will be within the <code>[-&pi;, &pi;[</code> interval). */
+        public static final UnaryOperator<Rad> WITHIN_MINUS_PI_AND_PI = normalizer(of(-Math.PI));
 
         /**
          * @param angle (in radians).
@@ -214,13 +220,13 @@ public abstract class Angle implements DoubleSupplier {
 
         /**
          * Creates an operator for normalizing/reducing an angle.
-         * The output will be within the <code> [c - &pi;, c + &pi;[</code> interval.
+         * The output will be within the <code> [lo, lo + 2&pi;[</code> interval.
          *
-         * @param c Center.
+         * @param lo Lower bound of the normalized interval.
          * @return the normalization operator.
          */
-        public static UnaryOperator<Rad> normalizer(Rad c) {
-            final Normalizer n = new Normalizer(c.value, TURN_TO_RAD);
+        public static UnaryOperator<Rad> normalizer(Rad lo) {
+            final Normalizer n = new Normalizer(lo.value, TURN_TO_RAD);
             return (Rad a) -> Rad.of(n.applyAsDouble(a.value));
         }
     }
@@ -231,6 +237,8 @@ public abstract class Angle implements DoubleSupplier {
     public static final class Deg extends Angle {
         /** Zero. */
         public static final Deg ZERO = Deg.of(0d);
+        /** Normalizing operator (result will be within the {@code [0, 360[} interval). */
+        public static final UnaryOperator<Deg> WITHIN_0_AND_360 = normalizer(of(0d));
 
         /**
          * @param angle (in degrees).
@@ -284,13 +292,13 @@ public abstract class Angle implements DoubleSupplier {
 
         /**
          * Creates an operator for normalizing/reducing an angle.
-         * The output will be within the {@code [c - 180, c + 180[} interval.
+         * The output will be within the {@code [c, c + 360[} interval.
          *
-         * @param c Center.
+         * @param lo Lower bound of the normalized interval.
          * @return the normalization operator.
          */
-        public static UnaryOperator<Deg> normalizer(Deg c) {
-            final Normalizer n = new Normalizer(c.value, TURN_TO_DEG);
+        public static UnaryOperator<Deg> normalizer(Deg lo) {
+            final Normalizer n = new Normalizer(lo.value, TURN_TO_DEG);
             return (Deg a) -> Deg.of(n.applyAsDouble(a.value));
         }
     }
@@ -300,9 +308,9 @@ public abstract class Angle implements DoubleSupplier {
      */
     private static final class Normalizer implements DoubleUnaryOperator {
         /** Lower bound. */
-        private final double lowerBound;
+        private final double lo;
         /** Upper bound. */
-        private final double upperBound;
+        private final double hi;
         /** Period. */
         private final double period;
         /** Normalizer. */
@@ -311,27 +319,32 @@ public abstract class Angle implements DoubleSupplier {
         /**
          * Note: It is assumed that both arguments have the same unit.
          *
-         * @param center Center of the desired interval.
+         * @param lo Lower bound of the desired interval.
          * @param period Circonference of the circle.
          */
-        Normalizer(double center,
+        Normalizer(double lo,
                    double period) {
-            final double halfPeriod = 0.5 * period;
             this.period = period;
-            lowerBound = center - halfPeriod;
-            upperBound = center + halfPeriod;
-            reduce = new Reduce(lowerBound, period);
+            this.lo = lo;
+            this.hi = lo + period;
+            reduce = new Reduce(lo, period);
         }
 
         /**
          * @param a Angle.
          * @return {@code = a - k} where {@code k} is an integer that satisfies
-         * {@code center - 0.5 <= a - k < center + 0.5} (in turns).
+         * {@code lo <= a - k < lo + period}.
          */
         @Override
         public double applyAsDouble(double a) {
-            final double normalized = reduce.applyAsDouble(a) + lowerBound;
-            return normalized < upperBound ?
+            if (lo <= a &&
+                a < hi) {
+                // Already within the main interval.
+                return a;
+            }
+
+            final double normalized = reduce.applyAsDouble(a) + lo;
+            return normalized < hi ?
                 normalized :
                 // If value is too small to be representable compared to the
                 // floor expression above (ie, if value + x = x), then we may
diff --git a/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/AngleTest.java b/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/AngleTest.java
index ae4c9bc..54ac3e3 100644
--- a/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/AngleTest.java
+++ b/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/AngleTest.java
@@ -59,13 +59,14 @@ class AngleTest {
 
     @Test
     void testNormalizeRadians() {
+        final double twopi = 2 * Math.PI;
         for (double a = -15.0; a <= 15.0; a += 0.1) {
             for (double b = -15.0; b <= 15.0; b += 0.2) {
                 final Angle.Rad aA = Angle.Rad.of(a);
                 final Angle.Rad aB = Angle.Rad.of(b);
                 final double c = Angle.Rad.normalizer(aB).apply(aA).getAsDouble();
-                Assertions.assertTrue((b - Math.PI) <= c);
-                Assertions.assertTrue(c <= (b + Math.PI));
+                Assertions.assertTrue(b <= c);
+                Assertions.assertTrue(c <= b + twopi);
                 double twoK = Math.rint((a - c) / Math.PI);
                 Assertions.assertEquals(c, a - twoK * Math.PI, 1e-14);
             }
@@ -73,42 +74,42 @@ class AngleTest {
     }
 
     @Test
-    void testNormalizeAroundZero1() {
+    void testNormalizeAboveZero1() {
         final double value = 1.25;
         final double expected = 0.25;
-        final double actual = Angle.Turn.normalizer(Angle.Turn.ZERO).apply(Angle.Turn.of(value)).getAsDouble();
+        final double actual = Angle.Turn.WITHIN_0_AND_1.apply(Angle.Turn.of(value)).getAsDouble();
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
     @Test
-    void testNormalizeAroundZero2() {
-        final double value = 0.75;
-        final double expected = -0.25;
-        final double actual = Angle.Turn.normalizer(Angle.Turn.ZERO).apply(Angle.Turn.of(value)).getAsDouble();
+    void testNormalizeAboveZero2() {
+        final double value = -0.75;
+        final double expected = 0.25;
+        final double actual = Angle.Turn.WITHIN_0_AND_1.apply(Angle.Turn.of(value)).getAsDouble();
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
     @Test
-    void testNormalizeAroundZero3() {
-        final double value = 0.5 + 1e-10;
-        final double expected = -0.5 + 1e-10;
-        final double actual = Angle.Turn.normalizer(Angle.Turn.ZERO).apply(Angle.Turn.of(value)).getAsDouble();
+    void testNormalizeAboveZero3() {
+        final double value = -0.5 + 1e-10;
+        final double expected = 0.5 + 1e-10;
+        final double actual = Angle.Turn.WITHIN_0_AND_1.apply(Angle.Turn.of(value)).getAsDouble();
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
     @Test
-    void testNormalizeAroundZero4() {
+    void testNormalizeAroundZero() {
         final double value = 5 * Math.PI / 4;
         final double expected = Math.PI * (1d / 4 - 1);
-        final double actual = Angle.Rad.normalizer(Angle.Rad.ZERO).apply(Angle.Rad.of(value)).getAsDouble();
+        final double actual = Angle.Rad.WITHIN_MINUS_PI_AND_PI.apply(Angle.Rad.of(value)).getAsDouble();
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
 
     @Test
     void testNormalizeUpperAndLowerBounds() {
-        final UnaryOperator<Angle.Rad> nZero = Angle.Rad.normalizer(Angle.Rad.ZERO);
-        final UnaryOperator<Angle.Rad> nPi = Angle.Rad.normalizer(Angle.Rad.PI);
+        final UnaryOperator<Angle.Rad> nZero = Angle.Rad.WITHIN_MINUS_PI_AND_PI;
+        final UnaryOperator<Angle.Rad> nPi = Angle.Rad.WITHIN_0_AND_2PI;
 
         // act/assert
         Assertions.assertEquals(-0.5, nZero.apply(Angle.Turn.of(-0.5).toRad()).toTurn().getAsDouble(), 0d);
@@ -126,8 +127,8 @@ class AngleTest {
 
     @Test
     void testNormalizeVeryCloseToBounds() {
-        final UnaryOperator<Angle.Rad> nZero = Angle.Rad.normalizer(Angle.Rad.ZERO);
-        final UnaryOperator<Angle.Rad> nPi = Angle.Rad.normalizer(Angle.Rad.PI);
+        final UnaryOperator<Angle.Rad> nZero = Angle.Rad.WITHIN_MINUS_PI_AND_PI;
+        final UnaryOperator<Angle.Rad> nPi = Angle.Rad.WITHIN_0_AND_2PI;
 
         // arrange
         final double pi = Math.PI;
@@ -177,4 +178,33 @@ class AngleTest {
     void testPi() {
         Assertions.assertEquals(Math.PI, Angle.Rad.PI.getAsDouble());
     }
+
+    @Test
+    void testNormalizeRetainsInputPrecision() {
+        final double aboveZero = Math.nextUp(0);
+        final double belowZero = Math.nextDown(0);
+
+        Assertions.assertEquals(aboveZero,
+                                Angle.Rad.WITHIN_MINUS_PI_AND_PI.apply(Angle.Rad.of(aboveZero)).getAsDouble());
+        Assertions.assertEquals(aboveZero,
+                                Angle.Rad.WITHIN_0_AND_2PI.apply(Angle.Rad.of(aboveZero)).getAsDouble());
+
+        Assertions.assertEquals(belowZero,
+                                Angle.Rad.WITHIN_MINUS_PI_AND_PI.apply(Angle.Rad.of(belowZero)).getAsDouble());
+        Assertions.assertEquals(0,
+                                Angle.Rad.WITHIN_0_AND_2PI.apply(Angle.Rad.of(belowZero)).getAsDouble());
+    }
+
+    @Test
+    void testNormalizePreciseLowerBound() {
+        final double x = Math.PI / 3;
+        final double above = Math.nextUp(x);
+        final double below = Math.nextDown(x);
+
+        final UnaryOperator<Angle.Rad> normalizer = Angle.Rad.normalizer(Angle.Rad.of(x));
+
+        Assertions.assertEquals(x, normalizer.apply(Angle.Rad.of(x)).getAsDouble());
+        Assertions.assertEquals(above, normalizer.apply(Angle.Rad.of(above)).getAsDouble());
+        // Assertions.assertEquals(below + 2 * Math.PI, normalizer.apply(Angle.Rad.of(below)).getAsDouble());
+    }
 }