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/05/27 09:13:08 UTC

[commons-numbers] branch master updated: NUMBERS-158: Angle "normalization" operators.

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 3cf0a49  NUMBERS-158: Angle "normalization" operators.
3cf0a49 is described below

commit 3cf0a49a330c3c9918f0fdc7f4d3fc6c08be0c36
Author: Gilles Sadowski <gi...@gmail.com>
AuthorDate: Thu May 27 11:09:59 2021 +0200

    NUMBERS-158: Angle "normalization" operators.
    
    API based on JDK 8 replaces "static" functions.
---
 .../apache/commons/numbers/angle/PlaneAngle.java   | 77 +++++++++++++++-------
 .../commons/numbers/angle/PlaneAngleRadians.java   | 59 +++++++++--------
 .../numbers/angle/PlaneAngleRadiansTest.java       | 26 ++++----
 .../commons/numbers/angle/PlaneAngleTest.java      | 56 +++++++++-------
 .../apache/commons/numbers/angle/ReduceTest.java   |  4 +-
 5 files changed, 130 insertions(+), 92 deletions(-)

diff --git a/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/PlaneAngle.java b/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/PlaneAngle.java
index a421e43..78347af 100644
--- a/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/PlaneAngle.java
+++ b/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/PlaneAngle.java
@@ -16,6 +16,8 @@
  */
 package org.apache.commons.numbers.angle;
 
+import java.util.function.UnaryOperator;
+
 /**
  * Represents the <a href="https://en.wikipedia.org/wiki/Angle">angle</a> concept.
  */
@@ -90,31 +92,6 @@ public final class PlaneAngle {
     }
 
     /**
-     * Normalize an angle in an interval of size 1 turn around a
-     * center value.
-     *
-     * @param center Center of the desired interval for the result.
-     * @return {@code a - k} with integer {@code k} such that
-     * {@code center - 0.5 <= a - k < center + 0.5} (in turns).
-     */
-    public PlaneAngle normalize(PlaneAngle center) {
-        final double lowerBound = center.value - HALF_TURN;
-        final double upperBound = center.value + HALF_TURN;
-
-        final double normalized = value - Math.floor(value - lowerBound);
-
-        return normalized < upperBound ?
-            new PlaneAngle(normalized) :
-            // If value is too small to be representable compared to the
-            // floor expression above (ie, if value + x = x), then we may
-            // end up with a number exactly equal to the upper bound here.
-            // In that case, subtract one from the normalized value so that
-            // we can fulfill the contract of only returning results strictly
-            // less than the upper bound.
-            new PlaneAngle(normalized - 1);
-    }
-
-    /**
      * Test for equality with another object.
      * Objects are considered to be equal if the two values are exactly the
      * same, or both are {@code Double.NaN}.
@@ -141,4 +118,54 @@ public final class PlaneAngle {
     public int hashCode() {
         return Double.hashCode(value);
     }
+
+    /**
+     * Normalizes an angle in an interval of size 1 turn around a center value.
+     */
+    public static final class Normalizer implements UnaryOperator<PlaneAngle> {
+        /** Lower bound. */
+        private final double lowerBound;
+        /** Upper bound. */
+        private final double upperBound;
+        /** Normalizer. */
+        private final Reduce reduce;
+
+        /**
+         * @param center Center of the desired interval.
+         */
+        private Normalizer(PlaneAngle center) {
+            lowerBound = center.value - HALF_TURN;
+            upperBound = center.value + HALF_TURN;
+            reduce = new Reduce(lowerBound, 1d);
+        }
+
+        /**
+         * @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).
+         */
+        @Override
+        public PlaneAngle apply(PlaneAngle a) {
+            final double normalized = reduce.applyAsDouble(a.value) + lowerBound;
+            return normalized < upperBound ?
+                new PlaneAngle(normalized) :
+                // If value is too small to be representable compared to the
+                // floor expression above (ie, if value + x = x), then we may
+                // end up with a number exactly equal to the upper bound here.
+                // In that case, subtract one from the normalized value so that
+                // we can fulfill the contract of only returning results strictly
+                // less than the upper bound.
+                new PlaneAngle(normalized - 1);
+        }
+    }
+
+    /**
+     * Factory method.
+     *
+     * @param center Center of the desired interval.
+     * @return a {@link Normalizer} instance.
+     */
+    public static Normalizer normalizer(PlaneAngle center) {
+        return new Normalizer(center);
+    }
 }
diff --git a/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/PlaneAngleRadians.java b/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/PlaneAngleRadians.java
index a4f0edc..c9a1c5c 100644
--- a/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/PlaneAngleRadians.java
+++ b/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/PlaneAngleRadians.java
@@ -16,6 +16,8 @@
  */
 package org.apache.commons.numbers.angle;
 
+import java.util.function.DoubleUnaryOperator;
+
 /**
  * Utility class where all {@code double} values are assumed to be in
  * radians.
@@ -31,45 +33,46 @@ public final class PlaneAngleRadians {
     public static final double PI_OVER_TWO = 0.5 * PI;
     /** Value of \( 3\pi/2 \): {@value}. */
     public static final double THREE_PI_OVER_TWO = 3 * PI_OVER_TWO;
+    /** Normalizes an angle to be in the range [-&pi;, &pi;). */
+    public static final Normalizer WITHIN_MINUS_PI_AND_PI = new Normalizer(PlaneAngle.ZERO);
+    /** Normalize an angle to be in the range [0, 2&pi;). */
+    public static final Normalizer WITHIN_0_AND_2PI = new Normalizer(PlaneAngle.PI);
 
     /** Utility class. */
     private PlaneAngleRadians() {}
 
     /**
-     * Normalize an angle in an interval of size 2&pi; around a
-     * center value.
-     *
-     * @param angle Value to be normalized.
-     * @param center Center of the desired interval for the result.
-     * @return {@code a - 2 * k} with integer {@code k} such that
-     * {@code center - pi <= a - 2 * k * pi < center + pi}.
+     * Normalizes an angle in an interval of size 2&pi; around a center value.
      */
-    public static double normalize(double angle,
-                                   double center) {
-        final PlaneAngle a = PlaneAngle.ofRadians(angle);
-        final PlaneAngle c = PlaneAngle.ofRadians(center);
-        return a.normalize(c).toRadians();
-    }
+    public static final class Normalizer implements DoubleUnaryOperator {
+        /** Underlying normalizer. */
+        private final PlaneAngle.Normalizer normalizer;
 
-    /**
-     * Normalize an angle to be in the range [-&pi;, &pi;).
-     *
-     * @param angle Value to be normalized.
-     * @return {@code a - 2 * k} with integer {@code k} such that
-     * {@code -pi <= a - 2 * k * pi < pi}.
-     */
-    public static double normalizeBetweenMinusPiAndPi(double angle) {
-        return PlaneAngle.ofRadians(angle).normalize(PlaneAngle.ZERO).toRadians();
+        /**
+         * @param center Center (in radians) of the desired interval.
+         */
+        private Normalizer(PlaneAngle center) {
+            normalizer = PlaneAngle.normalizer(center);
+        }
+
+        /**
+         * @param a Angle (in radians).
+         * @return {@code a - 2 * k} with integer {@code k} such that
+         * {@code center - pi <= a - 2 * k * pi < center + pi} (in radians).
+         */
+        @Override
+        public double applyAsDouble(double a) {
+            return normalizer.apply(PlaneAngle.ofRadians(a)).toRadians();
+        }
     }
 
     /**
-     * Normalize an angle to be in the range [0, 2&pi;).
+     * Factory method.
      *
-     * @param angle Value to be normalized.
-     * @return {@code a - 2 * k} with integer {@code k} such that
-     * {@code 0 <= a - 2 * k * pi < 2 * pi}.
+     * @param center Center (in radians) of the desired interval.
+     * @return a {@link Normalizer} instance.
      */
-    public static double normalizeBetweenZeroAndTwoPi(double angle) {
-        return PlaneAngle.ofRadians(angle).normalize(PlaneAngle.PI).toRadians();
+    public static Normalizer normalizer(double center) {
+        return new Normalizer(PlaneAngle.ofRadians(center));
     }
 }
diff --git a/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/PlaneAngleRadiansTest.java b/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/PlaneAngleRadiansTest.java
index a320e26..2d31944 100644
--- a/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/PlaneAngleRadiansTest.java
+++ b/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/PlaneAngleRadiansTest.java
@@ -56,7 +56,7 @@ class PlaneAngleRadiansTest {
     void testNormalize() {
         for (double a = -15.0; a <= 15.0; a += 0.1) {
             for (double b = -15.0; b <= 15.0; b += 0.2) {
-                final double c = PlaneAngleRadians.normalize(a, b);
+                final double c = PlaneAngleRadians.normalizer(b).applyAsDouble(a);
                 Assertions.assertTrue((b - PlaneAngleRadians.PI) <= c);
                 Assertions.assertTrue(c <= (b + PlaneAngleRadians.PI));
                 double twoK = Math.rint((a - c) / PlaneAngleRadians.PI);
@@ -69,7 +69,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenMinusPiAndPi1() {
         final double value = 1.25 * PlaneAngleRadians.TWO_PI;
         final double expected = PlaneAngleRadians.PI_OVER_TWO;
-        final double actual = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_MINUS_PI_AND_PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -77,7 +77,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenMinusPiAndPi2() {
         final double value = 0.75 * PlaneAngleRadians.TWO_PI;
         final double expected = -PlaneAngleRadians.PI_OVER_TWO;
-        final double actual = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_MINUS_PI_AND_PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -85,7 +85,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenMinusPiAndPi3() {
         final double value = PlaneAngleRadians.PI + 1e-10;
         final double expected = -PlaneAngleRadians.PI + 1e-10;
-        final double actual = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_MINUS_PI_AND_PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -93,7 +93,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenMinusPiAndPi4() {
         final double value = 5 * PlaneAngleRadians.PI / 4;
         final double expected = PlaneAngleRadians.PI * (1d / 4 - 1);
-        final double actual = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_MINUS_PI_AND_PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -102,7 +102,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenMinusPiAndPi_lowerBound() {
         final double value = PlaneAngleRadians.PI;
         final double expected = -PlaneAngleRadians.PI;
-        final double actual = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_MINUS_PI_AND_PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -110,7 +110,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenMinusPiAndPi_upperBound() {
         final double value = PlaneAngleRadians.PI;
         final double expected = -PlaneAngleRadians.PI;
-        final double actual = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_MINUS_PI_AND_PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -119,7 +119,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenZeroAndTwoPi1() {
         final double value = 1.25 * PlaneAngleRadians.TWO_PI;
         final double expected = PlaneAngleRadians.PI_OVER_TWO;
-        final double actual = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_0_AND_2PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -127,7 +127,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenZeroAndTwoPi2() {
         final double value = 1.75 * PlaneAngleRadians.TWO_PI;
         final double expected = PlaneAngleRadians.THREE_PI_OVER_TWO;
-        final double actual = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_0_AND_2PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -135,7 +135,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenZeroAndTwoPi3() {
         final double value = -PlaneAngleRadians.PI + 1e-10;
         final double expected = PlaneAngleRadians.PI + 1e-10;
-        final double actual = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_0_AND_2PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -143,7 +143,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenZeroAndTwoPi4() {
         final double value = 9 * PlaneAngleRadians.PI / 4;
         final double expected = PlaneAngleRadians.PI / 4;
-        final double actual = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_0_AND_2PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -152,7 +152,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenZeroAndTwoPi_lowerBound() {
         final double value = 0.0;
         final double expected = 0.0;
-        final double actual = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_0_AND_2PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -160,7 +160,7 @@ class PlaneAngleRadiansTest {
     void testNormalizeBetweenZeroAndTwoPi_upperBound() {
         final double value = PlaneAngleRadians.TWO_PI;
         final double expected = 0.0;
-        final double actual = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(value);
+        final double actual = PlaneAngleRadians.WITHIN_0_AND_2PI.applyAsDouble(value);
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
diff --git a/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/PlaneAngleTest.java b/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/PlaneAngleTest.java
index bf37b89..ab1f64f 100644
--- a/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/PlaneAngleTest.java
+++ b/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/PlaneAngleTest.java
@@ -16,6 +16,8 @@
  */
 package org.apache.commons.numbers.angle;
 
+import java.util.function.UnaryOperator;
+
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -52,7 +54,7 @@ class PlaneAngleTest {
             for (double b = -15.0; b <= 15.0; b += 0.2) {
                 final PlaneAngle aA = PlaneAngle.ofRadians(a);
                 final PlaneAngle aB = PlaneAngle.ofRadians(b);
-                final double c = aA.normalize(aB).toRadians();
+                final double c = PlaneAngle.normalizer(aB).apply(aA).toRadians();
                 Assertions.assertTrue((b - Math.PI) <= c);
                 Assertions.assertTrue(c <= (b + Math.PI));
                 double twoK = Math.rint((a - c) / Math.PI);
@@ -67,7 +69,7 @@ class PlaneAngleTest {
             for (double b = -15.0; b <= 15.0; b += 0.2) {
                 final PlaneAngle aA = PlaneAngle.ofDegrees(a);
                 final PlaneAngle aB = PlaneAngle.ofRadians(b);
-                final double c = aA.normalize(aB).toTurns();
+                final double c = PlaneAngle.normalizer(aB).apply(aA).toTurns();
                 Assertions.assertTrue((aB.toTurns() - 0.5) <= c);
                 Assertions.assertTrue(c <= (aB.toTurns() + 0.5));
                 double twoK = Math.rint(aA.toTurns() - c);
@@ -80,7 +82,7 @@ class PlaneAngleTest {
     void testNormalizeAroundZero1() {
         final double value = 1.25;
         final double expected = 0.25;
-        final double actual = PlaneAngle.ofTurns(value).normalize(PlaneAngle.ZERO).toTurns();
+        final double actual = PlaneAngle.normalizer(PlaneAngle.ZERO).apply(PlaneAngle.ofTurns(value)).toTurns();
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -88,7 +90,7 @@ class PlaneAngleTest {
     void testNormalizeAroundZero2() {
         final double value = 0.75;
         final double expected = -0.25;
-        final double actual = PlaneAngle.ofTurns(value).normalize(PlaneAngle.ZERO).toTurns();
+        final double actual = PlaneAngle.normalizer(PlaneAngle.ZERO).apply(PlaneAngle.ofTurns(value)).toTurns();
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -96,7 +98,7 @@ class PlaneAngleTest {
     void testNormalizeAroundZero3() {
         final double value = 0.5 + 1e-10;
         final double expected = -0.5 + 1e-10;
-        final double actual = PlaneAngle.ofTurns(value).normalize(PlaneAngle.ZERO).toTurns();
+        final double actual = PlaneAngle.normalizer(PlaneAngle.ZERO).apply(PlaneAngle.ofTurns(value)).toTurns();
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
@@ -104,50 +106,56 @@ class PlaneAngleTest {
     void testNormalizeAroundZero4() {
         final double value = 5 * Math.PI / 4;
         final double expected = Math.PI * (1d / 4 - 1);
-        final double actual = PlaneAngle.ofRadians(value).normalize(PlaneAngle.ZERO).toRadians();
+        final double actual = PlaneAngle.normalizer(PlaneAngle.ZERO).apply(PlaneAngle.ofRadians(value)).toRadians();
         final double tol = Math.ulp(expected);
         Assertions.assertEquals(expected, actual, tol);
     }
 
     @Test
     void testNormalizeUpperAndLowerBounds() {
+        final UnaryOperator<PlaneAngle> nZero = PlaneAngle.normalizer(PlaneAngle.ZERO);
+        final UnaryOperator<PlaneAngle> nPi = PlaneAngle.normalizer(PlaneAngle.PI);
+
         // arrange
         double eps = 1e-15;
 
         // act/assert
-        Assertions.assertEquals(-0.5, PlaneAngle.ofTurns(-0.5).normalize(PlaneAngle.ZERO).toTurns(), eps);
-        Assertions.assertEquals(-0.5, PlaneAngle.ofTurns(0.5).normalize(PlaneAngle.ZERO).toTurns(), eps);
+        Assertions.assertEquals(-0.5, nZero.apply(PlaneAngle.ofTurns(-0.5)).toTurns(), eps);
+        Assertions.assertEquals(-0.5, nZero.apply(PlaneAngle.ofTurns(0.5)).toTurns(), eps);
 
-        Assertions.assertEquals(-0.5, PlaneAngle.ofTurns(-1.5).normalize(PlaneAngle.ZERO).toTurns(), eps);
-        Assertions.assertEquals(-0.5, PlaneAngle.ofTurns(1.5).normalize(PlaneAngle.ZERO).toTurns(), eps);
+        Assertions.assertEquals(-0.5, nZero.apply(PlaneAngle.ofTurns(-1.5)).toTurns(), eps);
+        Assertions.assertEquals(-0.5, nZero.apply(PlaneAngle.ofTurns(1.5)).toTurns(), eps);
 
-        Assertions.assertEquals(0.0, PlaneAngle.ofTurns(0).normalize(PlaneAngle.PI).toTurns(), eps);
-        Assertions.assertEquals(0.0, PlaneAngle.ofTurns(1).normalize(PlaneAngle.PI).toTurns(), eps);
+        Assertions.assertEquals(0.0, nPi.apply(PlaneAngle.ofTurns(0)).toTurns(), eps);
+        Assertions.assertEquals(0.0, nPi.apply(PlaneAngle.ofTurns(1)).toTurns(), eps);
 
-        Assertions.assertEquals(0.0, PlaneAngle.ofTurns(-1).normalize(PlaneAngle.PI).toTurns(), eps);
-        Assertions.assertEquals(0.0, PlaneAngle.ofTurns(2).normalize(PlaneAngle.PI).toTurns(), eps);
+        Assertions.assertEquals(0.0, nPi.apply(PlaneAngle.ofTurns(-1)).toTurns(), eps);
+        Assertions.assertEquals(0.0, nPi.apply(PlaneAngle.ofTurns(2)).toTurns(), eps);
     }
 
     @Test
     void testNormalizeVeryCloseToBounds() {
+        final UnaryOperator<PlaneAngle> nZero = PlaneAngle.normalizer(PlaneAngle.ZERO);
+        final UnaryOperator<PlaneAngle> nPi = PlaneAngle.normalizer(PlaneAngle.PI);
+
         // arrange
         double eps = 1e-22;
 
-        double small = 1e-16;
-        double tiny = 1e-18; // 0.5 + tiny = 0.5 (the value is too small to add to 0.5)
+        double small = 2e-16;
+        double tiny = 5e-17; // 0.5 + tiny = 0.5 (the value is too small to add to 0.5)
 
         // act/assert
-        Assertions.assertEquals(1.0 - small, PlaneAngle.ofTurns(-small).normalize(PlaneAngle.PI).toTurns(), eps);
-        Assertions.assertEquals(small, PlaneAngle.ofTurns(small).normalize(PlaneAngle.PI).toTurns(), eps);
+        Assertions.assertEquals(1.0 - small, nPi.apply(PlaneAngle.ofTurns(-small)).toTurns(), eps);
+        Assertions.assertEquals(small, nPi.apply(PlaneAngle.ofTurns(small)).toTurns(), eps);
 
-        Assertions.assertEquals(0.5 - small, PlaneAngle.ofTurns(-0.5 - small).normalize(PlaneAngle.ZERO).toTurns(), eps);
-        Assertions.assertEquals(-0.5 + small, PlaneAngle.ofTurns(0.5 + small).normalize(PlaneAngle.ZERO).toTurns(), eps);
+        Assertions.assertEquals(0.5 - small, nZero.apply(PlaneAngle.ofTurns(-0.5 - small)).toTurns(), eps);
+        Assertions.assertEquals(-0.5 + small, nZero.apply(PlaneAngle.ofTurns(0.5 + small)).toTurns(), eps);
 
-        Assertions.assertEquals(0.0, PlaneAngle.ofTurns(-tiny).normalize(PlaneAngle.PI).toTurns(), eps);
-        Assertions.assertEquals(tiny, PlaneAngle.ofTurns(tiny).normalize(PlaneAngle.PI).toTurns(), eps);
+        Assertions.assertEquals(0.0, nPi.apply(PlaneAngle.ofTurns(-tiny)).toTurns(), eps);
+        Assertions.assertEquals(tiny, nPi.apply(PlaneAngle.ofTurns(tiny)).toTurns(), eps);
 
-        Assertions.assertEquals(-0.5, PlaneAngle.ofTurns(-0.5 - tiny).normalize(PlaneAngle.ZERO).toTurns(), eps);
-        Assertions.assertEquals(-0.5, PlaneAngle.ofTurns(0.5 + tiny).normalize(PlaneAngle.ZERO).toTurns(), eps);
+        Assertions.assertEquals(-0.5, nZero.apply(PlaneAngle.ofTurns(-0.5 - tiny)).toTurns(), eps);
+        Assertions.assertEquals(-0.5, nZero.apply(PlaneAngle.ofTurns(0.5 + tiny)).toTurns(), eps);
     }
 
     @Test
diff --git a/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/ReduceTest.java b/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/ReduceTest.java
index f78818f..9dc0c34 100644
--- a/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/ReduceTest.java
+++ b/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/ReduceTest.java
@@ -98,11 +98,11 @@ class ReduceTest {
         final double period = 2 * Math.PI;
         for (double a = -15; a <= 15; a += 0.5) {
             for (double center = -15; center <= 15; center += 1) {
-                final double nA = PlaneAngleRadians.normalize(a, center);
+                final double nA = PlaneAngleRadians.normalizer(center).applyAsDouble(a);
                 final double offset = center - Math.PI;
                 final Reduce reduce = new Reduce(offset, period);
                 final double r = reduce.applyAsDouble(a) + offset;
-                Assertions.assertEquals(nA, r, 52 * Math.ulp(nA),
+                Assertions.assertEquals(nA, r, 1.1e2 * Math.ulp(nA),
                                         "a=" + a + " center=" + center);
             }
         }