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/02 22:48:34 UTC

[commons-numbers] branch master updated: NUMBERS-161: Class "Angle" (holding the user-supplied value without round-off).

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 <gi...@gmail.com>
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" />

Re: [commons-numbers] branch master updated: NUMBERS-161: Class "Angle" (holding the user-supplied value without round-off).

Posted by Gilles Sadowski <gi...@gmail.com>.
Le jeu. 3 juin 2021 à 01:17, Alex Herbert <al...@gmail.com> a écrit :
>
> On Wed, 2 Jun 2021 at 23:48, <er...@apache.org> wrote:
>
> -- SNIP --
>
>      <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" />
> >
>
> Disabling the checkstyle rule applies to all code. Some of the code in
> numbers should conform to this rule (Complex, Fraction, etc). This case
> should use the checkstyle exclusions file instead.
>
> You can mark the exclusions with a note to state that the hash code method
> is in the super class (Angle) for the Angle implementations, none of which
> have additional fields to include in the hash.

Done.

Thanks,
Gilles

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@commons.apache.org
For additional commands, e-mail: dev-help@commons.apache.org


Re: [commons-numbers] branch master updated: NUMBERS-161: Class "Angle" (holding the user-supplied value without round-off).

Posted by Alex Herbert <al...@gmail.com>.
On Wed, 2 Jun 2021 at 23:48, <er...@apache.org> wrote:

-- SNIP --

     <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" />
>

Disabling the checkstyle rule applies to all code. Some of the code in
numbers should conform to this rule (Complex, Fraction, etc). This case
should use the checkstyle exclusions file instead.

You can mark the exclusions with a note to state that the hash code method
is in the super class (Angle) for the Angle implementations, none of which
have additional fields to include in the hash.

Alex