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 <gillese...@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π. */ public static final Rad TWO_PI = Rad.of(2 * Math.PI); + /** Normalizing operator (result will be within the <code>[0, 2π[</code> interval). */ + public static final UnaryOperator<Rad> WITHIN_0_AND_2PI = normalizer(of(0d)); + /** Normalizing operator (result will be within the <code>[-π, π[</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 - π, c + π[</code> interval. + * The output will be within the <code> [lo, lo + 2π[</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()); + } }