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);
+        /** &pi;. */
+        public static final Rad PI = Rad.of(Math.PI);
+        /** 2&pi;. */
+        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 - &pi;, c + &pi;[</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" />

Reply via email to