This is an automated email from the ASF dual-hosted git repository. aherbert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-statistics.git
The following commit(s) were added to refs/heads/master by this push: new 4633d9b STATISTICS-76: Max Implementation 4633d9b is described below commit 4633d9bb7989dc7b168ab04456b1a085d59d53b5 Author: AJoshi <janiru...@gmail.com> AuthorDate: Sun Jul 16 23:34:56 2023 +0530 STATISTICS-76: Max Implementation --- .../apache/commons/statistics/descriptive/Max.java | 134 +++++++++++++++ .../commons/statistics/descriptive/MaxTest.java | 188 +++++++++++++++++++++ src/conf/pmd/pmd-ruleset.xml | 2 +- 3 files changed, 323 insertions(+), 1 deletion(-) diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Max.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Max.java new file mode 100644 index 0000000..82d625a --- /dev/null +++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Max.java @@ -0,0 +1,134 @@ +/* + * 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.statistics.descriptive; + +import java.util.Arrays; + +/** + * Returns the maximum of the available values. + * + * <p>The result is <code>NaN</code> if any of the values is <code>NaN</code>. + * + * <p>The result is <code>NEGATIVE_INFINITY</code> if no values are added. + * + * <p>This class is designed to work with (though does not require) + * {@linkplain java.util.stream streams}. + * + * <p><strong>This implementation is not thread safe.</strong> + * If multiple threads access an instance of this class concurrently, + * and at least one of the threads invokes the <code>accept()</code> or + * <code>combine()</code> method, it must be synchronized externally. + * + * <p>However, it is safe to use <code>accept()</code> and <code>combine()</code> + * as <code>accumulator</code> and <code>combiner</code> functions of + * {@link java.util.stream.Collector Collector} on a parallel stream, + * because the parallel implementation of {@link java.util.stream.Stream#collect Stream.collect()} + * provides the necessary partitioning, isolation, and merging of results for + * safe and efficient parallel execution. + * + * @since 1.1 + */ +public abstract class Max implements DoubleStatistic, DoubleStatisticAccumulator<Max> { + + /** + * Create a Max instance. + */ + Max() { + //No-op + } + + /** + * Creates a {@code Max} implementation which does not store the input value(s) it consumes. + * + * <p>The result is <code>NaN</code> if any of the values is <code>NaN</code>. + * + * <p>The result is {@link Double#NEGATIVE_INFINITY NEGATIVE_INFINITY} + * if no values have been added. + * + * @return {@code Max} implementation. + */ + public static Max create() { + return new StorelessMax(); + } + + /** + * Returns a {@code Max} instance that has the maximum of all input value(s). + * + * <p>The result is <code>NaN</code> if any of the values is <code>NaN</code>. + * + * <p>When the input is an empty array, the result is + * {@link Double#NEGATIVE_INFINITY NEGATIVE_INFINITY}. + * + * @param values Values. + * @return {@code Max} instance. + */ + public static Max of(double... values) { + final StorelessMax max = new StorelessMax(); + Arrays.stream(values).forEach(max); + return max; + } + + /** + * Updates the state of the statistic to reflect the addition of {@code value}. + * @param value Value. + */ + @Override + public abstract void accept(double value); + + /** + * Gets the maximum of all input values. + * + * <p>When no values have been added, the result is + * {@link Double#NEGATIVE_INFINITY NEGATIVE_INFINITY}. + * + * @return {@code Maximum} of all values seen so far. + */ + @Override + public abstract double getAsDouble(); + + /** {@inheritDoc} */ + @Override + public abstract Max combine(Max other); + + /** + * {@code Max} implementation that does not store the input value(s) processed so far. + * + * <p>Uses JDK's {@link Math#max Math.max} as an underlying function + * to compute the {@code maximum}. + */ + private static class StorelessMax extends Max { + + /** Current max. */ + private double max = Double.NEGATIVE_INFINITY; + + @Override + public void accept(double value) { + max = Double.max(max, value); + } + + @Override + public double getAsDouble() { + return max; + } + + @Override + public Max combine(Max other) { + accept(other.getAsDouble()); + return this; + } + } +} diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/MaxTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/MaxTest.java new file mode 100644 index 0000000..0051d1d --- /dev/null +++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/MaxTest.java @@ -0,0 +1,188 @@ +/* + * 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.statistics.descriptive; + +import java.util.Arrays; +import java.util.function.DoubleSupplier; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link Max}. + */ +final class MaxTest { + + @Test + void testEmpty() { + Max max = Max.create(); + Assertions.assertEquals(Double.NEGATIVE_INFINITY, max.getAsDouble()); + } + + @Test + void testIncrement() { + // Test the max after each incremental update + // First parameter of testArray is the value that would be added + // Second parameter of testArray is the max we expect after adding the value + double[][] testArray = { + {1729.22, 1729.22}, + {2520.35, 2520.35}, + {2010.87, 2520.35}, + {100000000.1, 100000000.1}, + {+0.0, 100000000.1}, + {-0.0, 100000000.1}, + {Double.MAX_VALUE, Double.MAX_VALUE}, + {Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY}, + {Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY} + }; + + Max stat = Max.create(); + for (final double[] valueAndExpected: testArray) { + final double value = valueAndExpected[0]; + final double expected = valueAndExpected[1]; + stat.accept(value); + Assertions.assertEquals(expected, stat.getAsDouble()); + } + } + + @Test + void testNaN() { + // Test non-nan values cannot revert a NaN + double[] testArray = {Double.NaN, +0.0d, -0.0d, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}; + Max stat = Max.create(); + for (final double x : testArray) { + stat.accept(x); + Assertions.assertEquals(Double.NaN, stat.getAsDouble()); + } + } + + @ParameterizedTest + @MethodSource + void testMax(double[] values, double expected) { + Max stat = Max.create(); + Arrays.stream(values).forEach(stat); + double actual = stat.getAsDouble(); + Assertions.assertEquals(expected, actual, "max"); + Assertions.assertEquals(expected, Max.of(values).getAsDouble(), "max"); + } + + static Stream<Arguments> testMax() { + return Stream.of( + Arguments.of(new double[] {}, Double.NEGATIVE_INFINITY), + Arguments.of(new double[] {3.14}, 3.14), + Arguments.of(new double[] {12.34, 56.78, -2.0}, 56.78), + Arguments.of(new double[] {Double.NaN, 3.14, Double.NaN, Double.NaN}, Double.NaN), + Arguments.of(new double[] {-1d, 1d, Double.NaN}, Double.NaN), + Arguments.of(new double[] {Double.NaN, Double.NaN, Double.NaN}, Double.NaN), + Arguments.of(new double[] {0.0d, Double.NaN, +0.0d, -0.0d, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}, Double.NaN), + Arguments.of(new double[] {+0.0d, -0.0d, 1.0, 3.14}, 3.14), + Arguments.of(new double[] {-0.0, +0.0}, 0.0), + Arguments.of(new double[] {0.0, -0.0}, 0.0), + Arguments.of(new double[] {0.0, +0.0}, 0.0), + Arguments.of(new double[] {1.2, -34.56, 456.789, -5678.9012}, 456.789), + Arguments.of(new double[] {-23467824, 23648, 2368, 23749, -23424, -23492, -92397747}, 23749), + Arguments.of(new double[] {1.7976931348623157e+307, 1.7976931348623157e+306, 0.0, Double.MAX_VALUE}, Double.MAX_VALUE), + Arguments.of(new double[] {1.7976931348623157e+307, 1.7976931348623157e+306, 0.0, -Double.MAX_VALUE}, 1.7976931348623157e+307), + Arguments.of(new double[] {Double.NEGATIVE_INFINITY, -Double.MAX_VALUE, Double.MAX_VALUE, Double.POSITIVE_INFINITY}, Double.POSITIVE_INFINITY), + Arguments.of(new double[] {Double.NEGATIVE_INFINITY, -Double.MAX_VALUE, -Double.MIN_VALUE}, -Double.MIN_VALUE) + ); + } + + @ParameterizedTest + @MethodSource(value = "testMax") + void testParallelStream(double[] values, double expected) { + double actual = Arrays.stream(values).parallel().collect(Max::create, Max::accept, Max::combine).getAsDouble(); + Assertions.assertEquals(expected, actual); + } + + @ParameterizedTest + @MethodSource(value = "testMax") + void testMaxRandomOrder(double[] values, double expected) { + UniformRandomProvider rng = TestHelper.createRNG(); + for (int i = 0; i < 10; i++) { + testMax(TestHelper.shuffle(rng, values), expected); + } + } + + @ParameterizedTest + @MethodSource + void testCombine(double[] first, double[] second, double expected) { + Max firstMax = Max.create(); + Max secondMax = Max.create(); + + Arrays.stream(first).forEach(firstMax); + Arrays.stream(second).forEach(secondMax); + + double secondMaxBeforeCombine = secondMax.getAsDouble(); + firstMax.combine(secondMax); + Assertions.assertEquals(expected, firstMax.getAsDouble()); + Assertions.assertEquals(secondMaxBeforeCombine, secondMax.getAsDouble()); + } + + static Stream<Arguments> testCombine() { + return Stream.of( + Arguments.of(new double[] {}, new double[] {}, Double.NEGATIVE_INFINITY), + Arguments.of(new double[] {3.14}, new double[] {}, 3.14), + Arguments.of(new double[] {}, new double[] {2.718}, 2.718), + Arguments.of(new double[] {}, new double[] {Double.NaN}, Double.NaN), + Arguments.of(new double[] {Double.NaN, Double.NaN}, new double[] {}, Double.NaN), + Arguments.of(new double[] {3.14}, new double[] {2.718}, 3.14), + Arguments.of(new double[] {-1, 0, 1}, new double[] {1.1, 2.2, 3.3}, 3.3), + Arguments.of(new double[] {3.14, 1.1, 22.22}, new double[] {2.718, 1.1, 333.333}, 333.333), + Arguments.of(new double[] {12.34, 56.78, -2.0}, new double[] {0.0, 23.45}, 56.78), + Arguments.of(new double[] {-2023.79, 11.11, 333.333}, new double[] {1.1}, 333.333), + Arguments.of(new double[] {1.1, +0.0, 3.14}, new double[] {22.22, 2.718, -0.0}, 22.22), + Arguments.of(new double[] {0.0, -Double.MAX_VALUE, Double.POSITIVE_INFINITY}, + new double[] {Double.NEGATIVE_INFINITY, -0.0, Double.NEGATIVE_INFINITY, Double.MAX_VALUE}, + Double.POSITIVE_INFINITY), + Arguments.of(new double[] {0.0, Double.NaN, -Double.MAX_VALUE, Double.POSITIVE_INFINITY}, + new double[] {Double.NaN, -0.0, Double.NaN, Double.NEGATIVE_INFINITY, Double.MAX_VALUE}, + Double.NaN) + ); + } + + @ParameterizedTest + @MethodSource + void testArrayOfArrays(double[][] input, double expected) { + double actual = Arrays.stream(input) + .map(Max::of) + .reduce(Max::combine) + .map(DoubleSupplier::getAsDouble) + .orElseThrow(RuntimeException::new); + + Assertions.assertEquals(expected, actual); + } + + static Stream<Arguments> testArrayOfArrays() { + return Stream.of( + Arguments.of(new double[][] {{}, {}, {}}, Double.NEGATIVE_INFINITY), + Arguments.of(new double[][] {{}, {Double.NaN}, {-1.7}}, Double.NaN), + Arguments.of(new double[][] {{}, {Double.NaN}, {}}, Double.NaN), + Arguments.of(new double[][] {{}, {1.1, 2}, {-1.7}}, 2), + Arguments.of(new double[][] {{1, 2}, {3, 4}}, 4), + Arguments.of(new double[][] {{+0.0, 2.0}, {1.0, -0.0, 3.14}}, 3.14), + Arguments.of(new double[][] {{+0.0, Double.NEGATIVE_INFINITY}, {-0.0, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}}, Double.POSITIVE_INFINITY), + Arguments.of(new double[][] {{1.1, 22.22}, {34.56, -5678.9, 2.718}, {Double.NaN, 0}}, + Double.NaN), + Arguments.of(new double[][] {{Double.NaN, Double.NaN}, {Double.NaN}, {Double.NaN, Double.NaN, Double.NaN}}, Double.NaN) + ); + } +} diff --git a/src/conf/pmd/pmd-ruleset.xml b/src/conf/pmd/pmd-ruleset.xml index 8bfff93..03bb30b 100644 --- a/src/conf/pmd/pmd-ruleset.xml +++ b/src/conf/pmd/pmd-ruleset.xml @@ -90,7 +90,7 @@ <properties> <property name="violationSuppressXPath" value="./ancestor-or-self::ClassOrInterfaceDeclaration[@SimpleName='DD' - or @SimpleName='Two' or @SimpleName='One' or @SimpleName='Min']"/> + or @SimpleName='Two' or @SimpleName='One' or @SimpleName='Min' or @SimpleName='Max']"/> </properties> </rule> <rule ref="category/java/codestyle.xml/PrematureDeclaration">