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 f749d60 NUMBERS-161: Class "Angle" (holding the user-supplied value
without round-off).
f749d60 is described below
commit f749d6096f257fa56c2a3506a381327591f34fb5
Author: Gilles Sadowski <[email protected]>
AuthorDate: Thu Jun 3 00:28:17 2021 +0200
NUMBERS-161: Class "Angle" (holding the user-supplied value without
round-off).
---
.../org/apache/commons/numbers/angle/Angle.java | 345 +++++++++++++++++++++
.../apache/commons/numbers/angle/AngleTest.java | 180 +++++++++++
src/main/resources/checkstyle/checkstyle.xml | 2 +-
3 files changed, 526 insertions(+), 1 deletion(-)
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
new file mode 100644
index 0000000..a582f57
--- /dev/null
+++
b/commons-numbers-angle/src/main/java/org/apache/commons/numbers/angle/Angle.java
@@ -0,0 +1,345 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.numbers.angle;
+
+import java.util.function.UnaryOperator;
+import java.util.function.DoubleUnaryOperator;
+import java.util.function.DoubleSupplier;
+
+/**
+ * Represents the <a href="https://en.wikipedia.org/wiki/Angle">angle</a>
concept.
+ */
+public abstract class Angle implements DoubleSupplier {
+ /** Conversion factor. */
+ private static final double TURN_TO_RAD = 2 * Math.PI;
+ /** Conversion factor. */
+ private static final double TURN_TO_DEG = 360d;
+
+ /** Value (unit depends on concrete instance). */
+ protected final double value;
+
+ /**
+ * @param value Value in turns.
+ */
+ private Angle(double value) {
+ this.value = value;
+ }
+
+ /** @return the value. */
+ @Override
+ public double getAsDouble() {
+ return value;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Double.hashCode(value);
+ }
+
+ /**
+ * @return the angle in <a
href="https://en.wikipedia.org/wiki/Turn_%28geometry%29">turns</a>.
+ */
+ public abstract Turn toTurn();
+
+ /**
+ * @return the angle in <a
href="https://en.wikipedia.org/wiki/Radian">radians</a>.
+ */
+ public abstract Rad toRad();
+
+ /**
+ * @return the angle in <a
href="https://en.wikipedia.org/wiki/Degree_%28angle%29">degrees</a>.
+ */
+ public abstract Deg toDeg();
+
+ /**
+ * Objects are considered to be equal if their values are exactly
+ * the same, or both are {@code Double.NaN}.
+ * Caveat: Method should be called only on instances of the same
+ * concrete type in order to avoid that angles with the same value
+ * but different units are be considered equal.
+ *
+ * @param other Angle.
+ * @return {@code true} if the two instances have the same {@link #value}.
+ */
+ protected boolean isSame(Angle other) {
+ return this == other ||
+ Double.doubleToLongBits(value) ==
Double.doubleToLongBits(other.value);
+ }
+
+ /**
+ * Unit: <a
href="https://en.wikipedia.org/wiki/Turn_%28geometry%29">turns</a>.
+ */
+ public static final class Turn extends Angle {
+ /** Zero. */
+ public static final Turn ZERO = Turn.of(0d);
+
+ /**
+ * @param angle (in turns).
+ */
+ private Turn(double angle) {
+ super(angle);
+ }
+
+ /**
+ * @param angle (in turns).
+ * @return a new intance.
+ */
+ public static Turn of(double angle) {
+ return new Turn(angle);
+ }
+
+ /**
+ * Test for equality with another object.
+ * Objects are considered to be equal if their values are exactly
+ * the same, or both are {@code Double.NaN}.
+ *
+ * @param other Object to test for equality with this instance.
+ * @return {@code true} if the objects are equal, {@code false} if
+ * {@code other} is {@code null}, not an instance of {@code Turn},
+ * or not equal to this instance.
+ */
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof Turn ?
+ isSame((Turn) other) :
+ false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Turn toTurn() {
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Rad toRad() {
+ return Rad.of(value * TURN_TO_RAD);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Deg toDeg() {
+ return Deg.of(value * TURN_TO_DEG);
+ }
+
+ /**
+ * Creates an operator for normalizing/reducing an angle.
+ * The output will be within the {@code [c - 0.5, c + 0.5[} interval.
+ *
+ * @param c Center.
+ * @return the normalization operator.
+ */
+ public static UnaryOperator<Turn> normalizer(Turn c) {
+ final Normalizer n = new Normalizer(c.value, 1d);
+ return (Turn a) -> Turn.of(n.applyAsDouble(a.value));
+ }
+ }
+
+ /**
+ * Unit: <a href="https://en.wikipedia.org/wiki/Radian">radians</a>.
+ */
+ public static final class Rad extends Angle {
+ /** Zero. */
+ public static final Rad ZERO = Rad.of(0d);
+ /** π. */
+ public static final Rad PI = Rad.of(Math.PI);
+ /** 2π. */
+ public static final Rad TWO_PI = Rad.of(2 * Math.PI);
+
+ /**
+ * @param angle (in radians).
+ */
+ private Rad(double angle) {
+ super(angle);
+ }
+
+ /**
+ * @param angle (in radians).
+ * @return a new intance.
+ */
+ public static Rad of(double angle) {
+ return new Rad(angle);
+ }
+
+ /**
+ * Test for equality with another object.
+ * Objects are considered to be equal if their values are exactly
+ * the same, or both are {@code Double.NaN}.
+ *
+ * @param other Object to test for equality with this instance.
+ * @return {@code true} if the objects are equal, {@code false} if
+ * {@code other} is {@code null}, not an instance of {@code Rad},
+ * or not equal to this instance.
+ */
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof Rad ?
+ isSame((Rad) other) :
+ false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Turn toTurn() {
+ return Turn.of(value / TURN_TO_RAD);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Rad toRad() {
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Deg toDeg() {
+ return Deg.of(toTurn().getAsDouble() * TURN_TO_DEG);
+ }
+
+ /**
+ * Creates an operator for normalizing/reducing an angle.
+ * The output will be within the <code> [c - π, c + π[</code>
interval.
+ *
+ * @param c Center.
+ * @return the normalization operator.
+ */
+ public static UnaryOperator<Rad> normalizer(Rad c) {
+ final Normalizer n = new Normalizer(c.value, TURN_TO_RAD);
+ return (Rad a) -> Rad.of(n.applyAsDouble(a.value));
+ }
+ }
+
+ /**
+ * Unit: <a
href="https://en.wikipedia.org/wiki/Degree_%28angle%29">degrees</a>.
+ */
+ public static final class Deg extends Angle {
+ /** Zero. */
+ public static final Deg ZERO = Deg.of(0d);
+
+ /**
+ * @param angle (in degrees).
+ */
+ private Deg(double angle) {
+ super(angle);
+ }
+
+ /**
+ * @param angle (in degrees).
+ * @return a new intance.
+ */
+ public static Deg of(double angle) {
+ return new Deg(angle);
+ }
+
+ /**
+ * Test for equality with another object.
+ * Objects are considered to be equal if their values are exactly
+ * the same, or both are {@code Double.NaN}.
+ *
+ * @param other Object to test for equality with this instance.
+ * @return {@code true} if the objects are equal, {@code false} if
+ * {@code other} is {@code null}, not an instance of {@code Deg},
+ * or not equal to this instance.
+ */
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof Deg ?
+ isSame((Deg) other) :
+ false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Turn toTurn() {
+ return Turn.of(value / TURN_TO_DEG);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Rad toRad() {
+ return Rad.of(toTurn().getAsDouble() * TURN_TO_RAD);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Deg toDeg() {
+ return this;
+ }
+
+ /**
+ * Creates an operator for normalizing/reducing an angle.
+ * The output will be within the {@code [c - 180, c + 180[} interval.
+ *
+ * @param c Center.
+ * @return the normalization operator.
+ */
+ public static UnaryOperator<Deg> normalizer(Deg c) {
+ final Normalizer n = new Normalizer(c.value, TURN_TO_DEG);
+ return (Deg a) -> Deg.of(n.applyAsDouble(a.value));
+ }
+ }
+
+ /**
+ * Normalizes an angle around a center value.
+ */
+ private static final class Normalizer implements DoubleUnaryOperator {
+ /** Lower bound. */
+ private final double lowerBound;
+ /** Upper bound. */
+ private final double upperBound;
+ /** Period. */
+ private final double period;
+ /** Normalizer. */
+ private final Reduce reduce;
+
+ /**
+ * Note: It is assumed that both arguments have the same unit.
+ *
+ * @param center Center of the desired interval.
+ * @param period Circonference of the circle.
+ */
+ Normalizer(double center,
+ double period) {
+ final double halfPeriod = 0.5 * period;
+ this.period = period;
+ lowerBound = center - halfPeriod;
+ upperBound = center + halfPeriod;
+ reduce = new Reduce(lowerBound, 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).
+ */
+ @Override
+ public double applyAsDouble(double a) {
+ final double normalized = reduce.applyAsDouble(a) + lowerBound;
+ return normalized < upperBound ?
+ 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.
+ normalized - period;
+ }
+ }
+}
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
new file mode 100644
index 0000000..ae4c9bc
--- /dev/null
+++
b/commons-numbers-angle/src/test/java/org/apache/commons/numbers/angle/AngleTest.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.numbers.angle;
+
+import java.util.function.UnaryOperator;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test cases for the {@link Angle} class.
+ */
+class AngleTest {
+ @Test
+ void testConstants() {
+ Assertions.assertEquals(0d, Angle.Turn.ZERO.getAsDouble(), 0d);
+ Assertions.assertEquals(0d, Angle.Rad.ZERO.getAsDouble(), 0d);
+ Assertions.assertEquals(0d, Angle.Deg.ZERO.getAsDouble(), 0d);
+ Assertions.assertEquals(Math.PI, Angle.Rad.PI.getAsDouble(), 0d);
+ Assertions.assertEquals(2 * Math.PI, Angle.Rad.TWO_PI.getAsDouble(),
0d);
+ }
+
+ @Test
+ void testConversionTurns() {
+ final double value = 12.3456;
+ final Angle a = Angle.Turn.of(value);
+ Assertions.assertEquals(value, a.getAsDouble());
+ }
+
+ @Test
+ void testConversionRadians() {
+ final double one = 2 * Math.PI;
+ final double value = 12.3456 * one;
+ final Angle a = Angle.Rad.of(value);
+ Assertions.assertEquals(value, a.toRad().getAsDouble());
+ }
+
+ @Test
+ void testConversionDegrees() {
+ final double one = 360;
+ final double value = 12.3456 * one;
+ final Angle a = Angle.Deg.of(value);
+ Assertions.assertEquals(value, a.toDeg().getAsDouble());
+ }
+
+ @Test
+ void testNormalizeRadians() {
+ 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));
+ double twoK = Math.rint((a - c) / Math.PI);
+ Assertions.assertEquals(c, a - twoK * Math.PI, 1e-14);
+ }
+ }
+ }
+
+ @Test
+ void testNormalizeAroundZero1() {
+ 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 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();
+ 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();
+ final double tol = Math.ulp(expected);
+ Assertions.assertEquals(expected, actual, tol);
+ }
+ @Test
+ void testNormalizeAroundZero4() {
+ 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 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);
+
+ // act/assert
+ Assertions.assertEquals(-0.5,
nZero.apply(Angle.Turn.of(-0.5).toRad()).toTurn().getAsDouble(), 0d);
+ Assertions.assertEquals(-0.5,
nZero.apply(Angle.Turn.of(0.5).toRad()).toTurn().getAsDouble(), 0d);
+
+ Assertions.assertEquals(-0.5,
nZero.apply(Angle.Turn.of(-1.5).toRad()).toTurn().getAsDouble(), 0d);
+ Assertions.assertEquals(-0.5,
nZero.apply(Angle.Turn.of(1.5).toRad()).toTurn().getAsDouble(), 0d);
+
+ Assertions.assertEquals(0.0,
nPi.apply(Angle.Turn.of(0).toRad()).toTurn().getAsDouble(), 0d);
+ Assertions.assertEquals(0.0,
nPi.apply(Angle.Turn.of(1).toRad()).toTurn().getAsDouble(), 0d);
+
+ Assertions.assertEquals(0.0,
nPi.apply(Angle.Turn.of(-1).toRad()).toTurn().getAsDouble(), 0d);
+ Assertions.assertEquals(0.0,
nPi.apply(Angle.Turn.of(2).toRad()).toTurn().getAsDouble(), 0d);
+ }
+
+ @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);
+
+ // arrange
+ final double pi = Math.PI;
+ final double twopi = 2 * pi;
+ double small = Math.ulp(twopi);
+ double tiny = 5e-17; // pi + tiny = pi (the value is too small to add
to pi)
+
+ // act/assert
+ Assertions.assertEquals(twopi - small,
nPi.apply(Angle.Rad.of(-small)).getAsDouble(), 0d);
+ Assertions.assertEquals(small,
nPi.apply(Angle.Rad.of(small)).getAsDouble(), 0d);
+
+ Assertions.assertEquals(pi - small, nZero.apply(Angle.Rad.of(-pi -
small)).getAsDouble(), 0d);
+ Assertions.assertEquals(-pi + small, nZero.apply(Angle.Rad.of(pi +
small)).getAsDouble(), 0d);
+
+ Assertions.assertEquals(0d,
nPi.apply(Angle.Rad.of(-tiny)).getAsDouble(), 0d);
+ Assertions.assertEquals(tiny,
nPi.apply(Angle.Rad.of(tiny)).getAsDouble(), 0d);
+
+ Assertions.assertEquals(-pi, nZero.apply(Angle.Rad.of(-pi -
tiny)).getAsDouble(), 0d);
+ Assertions.assertEquals(-pi, nZero.apply(Angle.Rad.of(pi +
tiny)).getAsDouble(), 0d);
+ }
+
+ @Test
+ void testHashCode() {
+ // Test assumes that the internal representation is in "turns".
+ final double value = -123.456789;
+ final int expected = Double.valueOf(value).hashCode();
+ final int actual = Angle.Turn.of(value).hashCode();
+ Assertions.assertEquals(actual, expected);
+ }
+
+ @Test
+ void testEquals() {
+ final double value = 12345.6789;
+ final Angle a = Angle.Rad.of(value);
+ Assertions.assertTrue(a.equals(a));
+ Assertions.assertTrue(a.equals(Angle.Rad.of(value)));
+ Assertions.assertFalse(a.equals(Angle.Rad.of(Math.nextUp(value))));
+ Assertions.assertFalse(a.equals(new Object()));
+ Assertions.assertFalse(a.equals(null));
+ }
+
+ @Test
+ void testZero() {
+ Assertions.assertEquals(0, Angle.Rad.ZERO.getAsDouble());
+ }
+ @Test
+ void testPi() {
+ Assertions.assertEquals(Math.PI, Angle.Rad.PI.getAsDouble());
+ }
+}
diff --git a/src/main/resources/checkstyle/checkstyle.xml
b/src/main/resources/checkstyle/checkstyle.xml
index 23d566a..88725ad 100644
--- a/src/main/resources/checkstyle/checkstyle.xml
+++ b/src/main/resources/checkstyle/checkstyle.xml
@@ -157,7 +157,7 @@
<!-- Checks for common coding problems -->
<!-- See http://checkstyle.sourceforge.net/config_coding.html -->
<module name="EmptyStatement" />
- <module name="EqualsHashCode" />
+ <!-- <module name="EqualsHashCode" /> -->
<!-- Method parameters and local variables should not hide fields, except
in constructors and setters -->
<module name="HiddenField">
<property name="ignoreConstructorParameter" value="true" />