You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ah...@apache.org on 2023/12/23 12:19:23 UTC

(commons-statistics) branch master updated (d6e94e9 -> 7239736)

This is an automated email from the ASF dual-hosted git repository.

aherbert pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/commons-statistics.git


    from d6e94e9  Pin GH plugin versions
     new 4b18cae  STATISTICS-81: Add StatisticResult interface
     new 3b2ae69  STATISTICS-81: Add integer min and max implementation
     new 3fda895  STATISTICS-81: Add integer mean and variance implementation
     new 8e1ec4c  Move default StatisticResult implementations to IntMath
     new 8c2cf0b  Drop DoubleStatisticAccumulator interface
     new 7239736  Correct javadoc in tests

The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .github/workflows/maven.yml                        |   14 +-
 .../statistics/descriptive/DoubleStatistic.java    |    7 +-
 .../statistics/descriptive/DoubleStatistics.java   |    2 +-
 .../statistics/descriptive/FirstMoment.java        |    4 +-
 .../statistics/descriptive/GeometricMean.java      |    6 +-
 .../commons/statistics/descriptive/Int128.java     |  237 +++
 .../commons/statistics/descriptive/IntMath.java    |  391 +++++
 .../descriptive/{Max.java => IntMax.java}          |   61 +-
 .../commons/statistics/descriptive/IntMean.java    |  144 ++
 .../descriptive/{Min.java => IntMin.java}          |   61 +-
 .../{package-info.java => IntStatistic.java}       |    9 +-
 .../statistics/descriptive/IntVariance.java        |  248 +++
 .../commons/statistics/descriptive/Kurtosis.java   |    6 +-
 .../descriptive/{Max.java => LongMax.java}         |   74 +-
 .../commons/statistics/descriptive/LongMean.java   |  141 ++
 .../descriptive/{Min.java => LongMin.java}         |   70 +-
 .../{package-info.java => LongStatistic.java}      |    9 +-
 .../statistics/descriptive/LongVariance.java       |  226 +++
 .../apache/commons/statistics/descriptive/Max.java |    8 +-
 .../commons/statistics/descriptive/Mean.java       |    6 +-
 .../apache/commons/statistics/descriptive/Min.java |    8 +-
 .../commons/statistics/descriptive/Product.java    |    6 +-
 .../commons/statistics/descriptive/Skewness.java   |    6 +-
 .../statistics/descriptive/StandardDeviation.java  |    6 +-
 ...cAccumulator.java => StatisticAccumulator.java} |    6 +-
 .../statistics/descriptive/StatisticResult.java    |   79 +
 .../commons/statistics/descriptive/Statistics.java |   35 +-
 .../apache/commons/statistics/descriptive/Sum.java |    6 +-
 .../descriptive/SumOfCubedDeviations.java          |    4 +-
 .../descriptive/SumOfFourthDeviations.java         |    4 +-
 .../commons/statistics/descriptive/SumOfLogs.java  |    6 +-
 .../descriptive/SumOfSquaredDeviations.java        |    4 +-
 .../statistics/descriptive/SumOfSquares.java       |    6 +-
 .../commons/statistics/descriptive/UInt128.java    |  238 +++
 .../commons/statistics/descriptive/UInt192.java    |  247 +++
 .../commons/statistics/descriptive/UInt96.java     |  155 ++
 .../commons/statistics/descriptive/Variance.java   |    6 +-
 .../descriptive/BaseDoubleStatisticTest.java       |   52 +-
 ...tatisticTest.java => BaseIntStatisticTest.java} |  706 ++++----
 ...atisticTest.java => BaseLongStatisticTest.java} |  706 ++++----
 .../statistics/descriptive/BaseStatisticTest.java  |  347 ++++
 .../descriptive/BigIntegerStatisticResult.java}    |   29 +-
 .../descriptive/DoubleStatisticsTest.java          |   10 +-
 .../commons/statistics/descriptive/Int128Test.java |  173 ++
 .../statistics/descriptive/IntMathTest.java        |  207 +++
 .../descriptive/{MinTest.java => IntMaxTest.java}  |   45 +-
 .../statistics/descriptive/IntMeanTest.java        |  116 ++
 .../descriptive/{MinTest.java => IntMinTest.java}  |   45 +-
 .../descriptive/IntStatisticResult.java}           |   29 +-
 .../statistics/descriptive/IntVarianceTest.java    |  191 +++
 .../descriptive/{MinTest.java => LongMaxTest.java} |   45 +-
 .../statistics/descriptive/LongMeanTest.java       |  124 ++
 .../descriptive/{MinTest.java => LongMinTest.java} |   45 +-
 .../descriptive/LongStatisticResult.java}          |   29 +-
 .../statistics/descriptive/LongVarianceTest.java   |  203 +++
 .../statistics/descriptive/ResultType.java}        |   16 +-
 .../descriptive/StatisticResultTest.java           |  158 ++
 .../descriptive/SumOfCubedDeviationsWrapper.java   |    2 +-
 .../descriptive/SumOfFourthDeviationsWrapper.java  |    2 +-
 .../commons/statistics/descriptive/TestHelper.java |  334 ++++
 .../statistics/descriptive/UInt128Test.java        |  239 +++
 .../statistics/descriptive/UInt192Test.java        |  208 +++
 .../commons/statistics/descriptive/UInt96Test.java |  140 ++
 commons-statistics-examples/examples-jmh/pom.xml   |   25 +-
 .../examples/jmh/descriptive/Int128.java           |  276 ++++
 .../examples/jmh/descriptive/IntMath.java          |  309 ++++
 .../jmh/descriptive/IntMomentPerformance.java      | 1719 ++++++++++++++++++++
 .../examples/jmh/descriptive/LongVariance2.java    |  196 +++
 .../examples/jmh/descriptive/UInt128.java          |  285 ++++
 .../examples/jmh/descriptive/UInt192.java          |  297 ++++
 .../examples/jmh/descriptive/UInt96.java           |  170 ++
 .../examples/jmh/descriptive/Int128Test.java       |  194 +++
 .../examples/jmh/descriptive/IntMathTest.java      |  152 ++
 .../examples/jmh/descriptive/LongVarianceTest.java |   47 +
 .../examples/jmh/descriptive/TestUtils.java        |  159 ++
 .../examples/jmh/descriptive/UInt128Test.java      |  238 +++
 .../examples/jmh/descriptive/UInt192Test.java      |  206 +++
 .../examples/jmh/descriptive/UInt96Test.java       |  150 ++
 src/conf/checkstyle/checkstyle-suppressions.xml    |    3 +
 src/conf/pmd/pmd-ruleset.xml                       |    7 +
 80 files changed, 10008 insertions(+), 1172 deletions(-)
 create mode 100644 commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Int128.java
 create mode 100644 commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMath.java
 copy commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/{Max.java => IntMax.java} (65%)
 create mode 100644 commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMean.java
 copy commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/{Min.java => IntMin.java} (65%)
 copy commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/{package-info.java => IntStatistic.java} (79%)
 create mode 100644 commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntVariance.java
 copy commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/{Max.java => LongMax.java} (60%)
 create mode 100644 commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongMean.java
 copy commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/{Min.java => LongMin.java} (61%)
 copy commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/{package-info.java => LongStatistic.java} (79%)
 create mode 100644 commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongVariance.java
 rename commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/{DoubleStatisticAccumulator.java => StatisticAccumulator.java} (84%)
 create mode 100644 commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StatisticResult.java
 create mode 100644 commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt128.java
 create mode 100644 commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt192.java
 create mode 100644 commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt96.java
 copy commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/{BaseDoubleStatisticTest.java => BaseIntStatisticTest.java} (59%)
 copy commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/{BaseDoubleStatisticTest.java => BaseLongStatisticTest.java} (59%)
 create mode 100644 commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java
 copy commons-statistics-descriptive/src/{main/java/org/apache/commons/statistics/descriptive/DoubleStatistic.java => test/java/org/apache/commons/statistics/descriptive/BigIntegerStatisticResult.java} (57%)
 create mode 100644 commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/Int128Test.java
 create mode 100644 commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMathTest.java
 copy commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/{MinTest.java => IntMaxTest.java} (60%)
 create mode 100644 commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMeanTest.java
 copy commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/{MinTest.java => IntMinTest.java} (60%)
 copy commons-statistics-descriptive/src/{main/java/org/apache/commons/statistics/descriptive/DoubleStatistic.java => test/java/org/apache/commons/statistics/descriptive/IntStatisticResult.java} (59%)
 create mode 100644 commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntVarianceTest.java
 copy commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/{MinTest.java => LongMaxTest.java} (60%)
 create mode 100644 commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMeanTest.java
 copy commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/{MinTest.java => LongMinTest.java} (60%)
 copy commons-statistics-descriptive/src/{main/java/org/apache/commons/statistics/descriptive/DoubleStatistic.java => test/java/org/apache/commons/statistics/descriptive/LongStatisticResult.java} (58%)
 create mode 100644 commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongVarianceTest.java
 copy commons-statistics-descriptive/src/{main/java/org/apache/commons/statistics/descriptive/package-info.java => test/java/org/apache/commons/statistics/descriptive/ResultType.java} (78%)
 create mode 100644 commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/StatisticResultTest.java
 create mode 100644 commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt128Test.java
 create mode 100644 commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt192Test.java
 create mode 100644 commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt96Test.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMath.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMomentPerformance.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVariance2.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128Test.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMathTest.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVarianceTest.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/TestUtils.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128Test.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192Test.java
 create mode 100644 commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96Test.java


(commons-statistics) 03/06: STATISTICS-81: Add integer mean and variance implementation

Posted by ah...@apache.org.
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

commit 3fda89532f8d49953ce41f9d79155ebbcfdd68a7
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Wed Dec 20 23:14:36 2023 +0000

    STATISTICS-81: Add integer mean and variance implementation
    
    Requires specialised aggregator classes for exact integer arithmetic.
    
    Add initial benchmark of integer based statistics.
---
 .github/workflows/maven.yml                        |   14 +-
 .../commons/statistics/descriptive/Int128.java     |  237 +++
 .../commons/statistics/descriptive/IntMath.java    |  391 +++++
 .../commons/statistics/descriptive/IntMean.java    |  144 ++
 .../statistics/descriptive/IntVariance.java        |  248 +++
 .../commons/statistics/descriptive/LongMean.java   |  141 ++
 .../statistics/descriptive/LongVariance.java       |  226 +++
 .../commons/statistics/descriptive/UInt128.java    |  238 +++
 .../commons/statistics/descriptive/UInt192.java    |  247 +++
 .../commons/statistics/descriptive/UInt96.java     |  155 ++
 .../descriptive/BaseIntStatisticTest.java          |   20 +-
 .../descriptive/BaseLongStatisticTest.java         |   50 +-
 .../statistics/descriptive/BaseStatisticTest.java  |   32 +-
 .../commons/statistics/descriptive/Int128Test.java |  173 ++
 .../statistics/descriptive/IntMathTest.java        |  206 +++
 .../statistics/descriptive/IntMeanTest.java        |  116 ++
 .../statistics/descriptive/IntVarianceTest.java    |  191 +++
 .../statistics/descriptive/LongMeanTest.java       |  124 ++
 .../statistics/descriptive/LongVarianceTest.java   |  203 +++
 .../commons/statistics/descriptive/TestHelper.java |  128 ++
 .../statistics/descriptive/UInt128Test.java        |  239 +++
 .../statistics/descriptive/UInt192Test.java        |  208 +++
 .../commons/statistics/descriptive/UInt96Test.java |  140 ++
 commons-statistics-examples/examples-jmh/pom.xml   |   25 +-
 .../examples/jmh/descriptive/Int128.java           |  276 ++++
 .../examples/jmh/descriptive/IntMath.java          |  309 ++++
 .../jmh/descriptive/IntMomentPerformance.java      | 1719 ++++++++++++++++++++
 .../examples/jmh/descriptive/LongVariance2.java    |  196 +++
 .../examples/jmh/descriptive/UInt128.java          |  285 ++++
 .../examples/jmh/descriptive/UInt192.java          |  297 ++++
 .../examples/jmh/descriptive/UInt96.java           |  170 ++
 .../examples/jmh/descriptive/Int128Test.java       |  194 +++
 .../examples/jmh/descriptive/IntMathTest.java      |  152 ++
 .../examples/jmh/descriptive/LongVarianceTest.java |   47 +
 .../examples/jmh/descriptive/TestUtils.java        |  159 ++
 .../examples/jmh/descriptive/UInt128Test.java      |  238 +++
 .../examples/jmh/descriptive/UInt192Test.java      |  206 +++
 .../examples/jmh/descriptive/UInt96Test.java       |  150 ++
 src/conf/checkstyle/checkstyle-suppressions.xml    |    1 +
 39 files changed, 8240 insertions(+), 55 deletions(-)

diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index e033075..fbef6c1 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -38,6 +38,16 @@ jobs:
         distribution: temurin
         java-version: ${{ matrix.java }}
         cache: 'maven'
-    - name: Build with Maven including examples
+    - name: Build with Maven
       # Use the default goal
-      run: mvn --show-version --batch-mode --no-transfer-progress -P examples
+      if: matrix.java == 8
+      run: mvn --show-version --batch-mode --no-transfer-progress
+    - name: Build with Maven including examples
+      # Examples require Java 11+.
+      # Building javadoc errors when run with the package phase with an error about
+      # the module path.
+      # Here we run the build and javadoc generation separately (which requires install of sources)
+      if: matrix.java > 8
+      run: |
+        mvn --show-version --batch-mode --no-transfer-progress -P examples clean install -Dmaven.javadoc.skip
+        mvn -P examples javadoc:javadoc -Dmaven.javadoc.skip=false
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Int128.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Int128.java
new file mode 100644
index 0000000..5ae6357
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Int128.java
@@ -0,0 +1,237 @@
+/*
+ * 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.math.BigInteger;
+import java.nio.ByteBuffer;
+import org.apache.commons.numbers.core.DD;
+
+/**
+ * A mutable 128-bit signed integer.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values.
+ *
+ * <p>Note: This number uses a signed long integer representation of:
+ *
+ * <pre>value = 2<sup>64</sup> * hi64 + lo64</pre>
+ *
+ * <p>If the high value is zero then the low value is the long representation of the
+ * number including the sign bit. Otherwise the low value corresponds to a correction
+ * term for the scaled high value which contains the sign-bit of the number.
+ *
+ * @since 1.1
+ */
+final class Int128 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    /** low 64-bits. */
+    private long lo;
+    /** high 64-bits. */
+    private long hi;
+
+    /**
+     * Create an instance.
+     */
+    private Int128() {
+        // No-op
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param x Value.
+     */
+    private Int128(long x) {
+        lo = x;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     */
+    Int128(long hi, long lo) {
+        this.lo = lo;
+        this.hi = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static Int128 create() {
+        return new Int128();
+    }
+
+    /**
+     * Create an instance of the {@code long} value.
+     *
+     * @param x Value.
+     * @return the instance
+     */
+    static Int128 of(long x) {
+        return new Int128(x);
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(long x) {
+        final long y = lo;
+        final long r = y + x;
+        // Overflow if the result has the opposite sign of both arguments
+        // (+,+) -> -
+        // (-,-) -> +
+        // Detect opposite sign:
+        if (((y ^ r) & (x ^ r)) < 0) {
+            // Carry overflow bit
+            hi += x < 0 ? -1 : 1;
+        }
+        lo = r;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(Int128 x) {
+        // Avoid issues adding to itself
+        final long l = x.lo;
+        final long h = x.hi;
+        add(l);
+        hi += h;
+    }
+
+    /**
+     * Compute the square of the low 64-bits of this number.
+     *
+     * <p>Warning: This ignores the upper 64-bits. Use with caution.
+     *
+     * @return the square
+     */
+    UInt128 squareLow() {
+        final long x = lo;
+        final long upper = IntMath.squareHigh(x);
+        return new UInt128(upper, x * x);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        long h = hi;
+        long l = lo;
+        // Special cases
+        if (h == 0) {
+            return BigInteger.valueOf(l);
+        }
+        if (l == 0) {
+            return BigInteger.valueOf(h).shiftLeft(64);
+        }
+
+        // The representation is 2^64 * hi64 + lo64.
+        // Here we avoid evaluating the addition:
+        // BigInteger.valueOf(l).add(BigInteger.valueOf(h).shiftLeft(64))
+        // It is faster to create from bytes.
+        // BigInteger bytes are an unsigned integer in BigEndian format, plus a sign.
+        // If both values are positive we can use the values unchanged.
+        // Otherwise selective negation is used to create a positive magnitude
+        // and we track the sign.
+        // Note: Negation of -2^63 is valid to create an unsigned 2^63.
+
+        int sign = 1;
+        if ((h ^ l) < 0) {
+            // Opposite signs and lo64 is not zero.
+            // The lo64 bits are an adjustment to the magnitude of hi64
+            // to make it smaller.
+            // Here we rearrange to [2^64 * (hi64-1)] + [2^64 - lo64].
+            // The second term [2^64 - lo64] can use lo64 as an unsigned 64-bit integer.
+            // The first term [2^64 * (hi64-1)] does not work if low is zero.
+            // It would work if zero was detected and we carried the overflow
+            // bit up to h to make it equal to: (h - 1) + 1 == h.
+            // Instead lo64 == 0 is handled as a special case above.
+
+            if (h >= 0) {
+                // Treat (unchanged) low as an unsigned add
+                h = h - 1;
+            } else {
+                // As above with negation
+                h = ~h; // -h - 1
+                l = -l;
+                sign = -1;
+            }
+        } else if (h < 0) {
+            // Invert negative values to create the equivalent positive magnitude.
+            h = -h;
+            l = -l;
+            sign = -1;
+        }
+
+        return new BigInteger(sign,
+            ByteBuffer.allocate(Long.BYTES * 2)
+                .putLong(h).putLong(l).array());
+    }
+
+    /**
+     * Convert to a double-double.
+     *
+     * @return the value
+     */
+    DD toDD() {
+        // Don't combine two 64-bit DD numbers:
+        // DD.of(hi).scalb(64).add(DD.of(lo))
+        // It is more accurate to create a 96-bit number and add the final 32-bits.
+        // Sum low to high.
+        // Note: Masking a negative hi number will create a small positive magnitude
+        // to add to a larger negative number:
+        // e.g. x = (x & 0xff) + ((x >> 8) << 8)
+        return DD.of(lo).add((hi & MASK32) * 0x1.0p64).add((hi >> Integer.SIZE) * 0x1.0p96);
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * <p>If the high value is zero then the low value is the long representation of the
+     * number including the sign bit. Otherwise this value corresponds to a correction
+     * term for the scaled high value which contains the sign-bit of the number
+     * (see {@link Int128}).
+     *
+     * @return the low 64-bits
+     */
+    long lo64() {
+        return lo;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return the high 64-bits
+     * @see #lo64()
+     */
+    long hi64() {
+        return hi;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMath.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMath.java
new file mode 100644
index 0000000..714fc41
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMath.java
@@ -0,0 +1,391 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * Support class for integer math.
+ *
+ * @since 1.1
+ */
+final class IntMath {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+    /** Mask for the lower 52-bits of a long. */
+    private static final long MASK52 = 0xf_ffff_ffff_ffffL;
+    /** Bias offset for the exponent of a double. */
+    private static final int EXP_BIAS = 1023;
+    /** Shift for the exponent of a double. */
+    private static final int EXP_SHIFT = 52;
+    /** 0.5. */
+    private static final double HALF = 0.5;
+
+    /** No instances. */
+    private IntMath() {}
+
+    /**
+     * Square the values as if an unsigned 64-bit long to produce the high 64-bits
+     * of the 128-bit unsigned result.
+     *a
+     * <p>This method computes the equivalent of:
+     * <pre>{@code
+     * Math.multiplyHigh(x, x)
+     * Math.unsignedMultiplyHigh(x, x) - (((x >> 63) & x) << 1)
+     * }</pre>
+     *
+     * <p>Note: The method {@code Math.multiplyHigh} was added in JDK 9
+     * and should be used as above when the source code targets Java 11
+     * to exploit the intrinsic method.
+     *
+     * <p>Note: The method uses the unsigned multiplication. When the input is negative
+     * it can be adjusted to the signed result by subtracting the argument twice from the
+     * result.
+     *
+     * @param x Value
+     * @return the high 64-bits of the 128-bit result
+     */
+    static long squareHigh(long x) {
+        // Computation is based on the following observation about the upper (a and x)
+        // and lower (b and y) bits of unsigned big-endian integers:
+        //   ab * xy
+        // =  b *  y
+        // +  b * x0
+        // + a0 *  y
+        // + a0 * x0
+        // = b * y
+        // + b * x * 2^32
+        // + a * y * 2^32
+        // + a * x * 2^64
+        //
+        // Summation using a character for each byte:
+        //
+        //             byby byby
+        // +      bxbx bxbx 0000
+        // +      ayay ayay 0000
+        // + axax axax 0000 0000
+        //
+        // The summation can be rearranged to ensure no overflow given
+        // that the result of two unsigned 32-bit integers multiplied together
+        // plus two full 32-bit integers cannot overflow 64 bits:
+        // > long x = (1L << 32) - 1
+        // > x * x + x + x == -1 (all bits set, no overflow)
+        //
+        // The carry is a composed intermediate which will never overflow:
+        //
+        //             byby byby
+        // +           bxbx 0000
+        // +      ayay ayay 0000
+        //
+        // +      bxbx 0000 0000
+        // + axax axax 0000 0000
+
+        final long a = x >>> 32;
+        final long b = x & MASK32;
+
+        final long aa = a * a;
+        final long ab = a * b;
+        final long bb = b * b;
+
+        // Cannot overflow
+        final long carry = (bb >>> 32) +
+                           (ab & MASK32) +
+                            ab;
+        // Note:
+        // low = (carry << 32) | (bb & MASK32)
+        // Benchmarking shows outputting low to a long[] output argument
+        // has no benefit over computing 'low = value * value' separately.
+
+        final long hi = (ab >>> 32) + (carry >>> 32) + aa;
+        // Adjust to the signed result:
+        // if x < 0:
+        //    hi - 2 * x
+        return hi - (((x >> 63) & x) << 1);
+    }
+
+    /**
+     * Multiply the two values as if unsigned 64-bit longs to produce the high 64-bits
+     * of the 128-bit unsigned result.
+     *
+     * <p>This method computes the equivalent of:
+     * <pre>{@code
+     * Math.multiplyHigh(a, b) + ((a >> 63) & b) + ((b >> 63) & a)
+     * }</pre>
+     *
+     * <p>Note: The method {@code Math.multiplyHigh} was added in JDK 9
+     * and should be used as above when the source code targets Java 11
+     * to exploit the intrinsic method.
+     *
+     * <p>Note: The method {@code Math.unsignedMultiplyHigh} was added in JDK 18
+     * and should be used when the source code target allows.
+     *
+     * <p>Taken from {@code o.a.c.rng.core.source64.LXMSupport}.
+     *
+     * @param value1 the first value
+     * @param value2 the second value
+     * @return the high 64-bits of the 128-bit result
+     */
+    static long unsignedMultiplyHigh(long value1, long value2) {
+        // Computation is based on the following observation about the upper (a and x)
+        // and lower (b and y) bits of unsigned big-endian integers:
+        //   ab * xy
+        // =  b *  y
+        // +  b * x0
+        // + a0 *  y
+        // + a0 * x0
+        // = b * y
+        // + b * x * 2^32
+        // + a * y * 2^32
+        // + a * x * 2^64
+        //
+        // Summation using a character for each byte:
+        //
+        //             byby byby
+        // +      bxbx bxbx 0000
+        // +      ayay ayay 0000
+        // + axax axax 0000 0000
+        //
+        // The summation can be rearranged to ensure no overflow given
+        // that the result of two unsigned 32-bit integers multiplied together
+        // plus two full 32-bit integers cannot overflow 64 bits:
+        // > long x = (1L << 32) - 1
+        // > x * x + x + x == -1 (all bits set, no overflow)
+        //
+        // The carry is a composed intermediate which will never overflow:
+        //
+        //             byby byby
+        // +           bxbx 0000
+        // +      ayay ayay 0000
+        //
+        // +      bxbx 0000 0000
+        // + axax axax 0000 0000
+
+        final long a = value1 >>> 32;
+        final long b = value1 & MASK32;
+        final long x = value2 >>> 32;
+        final long y = value2 & MASK32;
+
+        final long by = b * y;
+        final long bx = b * x;
+        final long ay = a * y;
+        final long ax = a * x;
+
+        // Cannot overflow
+        final long carry = (by >>> 32) +
+                           (bx & MASK32) +
+                            ay;
+        // Note:
+        // low = (carry << 32) | (by & INT_TO_UNSIGNED_BYTE_MASK)
+        // Benchmarking shows outputting low to a long[] output argument
+        // has no benefit over computing 'low = value1 * value2' separately.
+
+        return (bx >>> 32) + (carry >>> 32) + ax;
+    }
+
+    /**
+     * Multiply the arguments as if unsigned integers to a {@code double} result.
+     *
+     * @param x Value.
+     * @param y Value.
+     * @return the double
+     */
+    static double unsignedMultiplyToDouble(long x, long y) {
+        final long lo = x * y;
+        // Fast case: check the arguments cannot overflow a long.
+        // This is true if neither has the upper 33-bits set.
+        if (((x | y) >>> 31) == 0) {
+            // Implicit conversion to a double.
+            return lo;
+        }
+        return uin128ToDouble(unsignedMultiplyHigh(x, y), lo);
+    }
+
+    /**
+     * Convert an unsigned 128-bit integer to a {@code double}.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     * @return the double
+     */
+    static double uin128ToDouble(long hi, long lo) {
+        // Require the representation:
+        // 2^exp * mantissa / 2^53
+        // The mantissa has an implied leading 1-bit.
+
+        // We have the mantissa final bit as xxx0 or xxx1.
+        // To perform correct rounding we maintain the 54-th bit (a) and
+        // a check bit (b) of remaining bits.
+        // Cases:
+        // xxx0 00 - round-down              [1]
+        // xxx0 0b - round-down              [1]
+        // xxx0 a0 - half-even, round-down   [4]
+        // xxx0 ab - round-up                [2]
+        // xxx1 00 - round-down              [1]
+        // xxx1 0b - round-down              [1]
+        // xxx1 a0 - half-even, round-up     [3]
+        // xxx1 ab - round-up                [2]
+        // [1] If the 54-th bit is 0 always round-down.
+        // [2] Otherwise round-up if the check bit is set or
+        // [3] the final bit is odd (half-even rounding up).
+        // [4] half-even rounding down.
+
+        if (hi == 0) {
+            // If lo is a 63-bit result then we are done
+            if (lo >= 0) {
+                return lo;
+            }
+            // Create a 63-bit number with a sticky bit for rounding, rescale the result
+            return 2 * (double) ((lo >>> 1) | (lo & 0x1));
+        }
+
+        // Initially we create the most significant 64-bits.
+        final int shift = Long.numberOfLeadingZeros(hi);
+        // Shift the high bits and add trailing low bits.
+        // The mask is for the bits from low that are *not* used.
+        // Flipping the mask obtains the bits we concatenate
+        // after shifting (64 - shift).
+        final long maskLow = -1L >>> shift;
+        long bits64 = (hi << shift) | ((lo & ~maskLow) >>> -shift);
+        // exponent for 2^exp is the index of the highest bit in the 128 bit integer
+        final int exp = 127 - shift;
+        // Some of the low bits are lost. If non-zero set
+        // a sticky bit for rounding.
+        bits64 |= (lo & maskLow) == 0 ? 0 : 1;
+
+        // We have a 64-bit unsigned fraction magnitude and an exponent.
+        // This must be converted to a IEEE double by mapping the fraction to a base of 2^53.
+
+        // Create the 53-bit mantissa without the implicit 1-bit
+        long bits = (bits64 >>> 11) & MASK52;
+        // Extract 54-th bit and a sticky bit
+        final long a = (bits64 >>> 10) & 0x1;
+        final long b = (bits64 << 54) == 0 ? 0 : 1;
+        // Perform half-even rounding.
+        bits += a & (b | (bits & 0x1));
+
+        // Add the exponent.
+        // No worry about overflow to the sign bit as the max exponent is 127.
+        bits += (long) (exp + EXP_BIAS) << EXP_SHIFT;
+
+        return Double.longBitsToDouble(bits);
+    }
+
+    /**
+     * Return the whole number that is nearest to the {@code double} argument {@code x}
+     * as an {@code int}, with ties rounding towards positive infinity.
+     *
+     * <p>This will raise an {@link ArithmeticException} if the closest
+     * integer result is not within the range {@code [-2^31, 2^31)},
+     * i.e. it overflows an {@code int}; or the argument {@code x}
+     * is not finite.
+     *
+     * <p>Note: This method is equivalent to:
+     * <pre>
+     * Math.toIntExact(Math.round(x))
+     * </pre>
+     *
+     * <p>The behaviour has been re-implemented for consistent error handling
+     * for {@code int}, {@code long} and {@code BigInteger} types.
+     *
+     * @param x Value.
+     * @return rounded value
+     * @throws ArithmeticException if the {@code result} overflows an {@code int},
+     * or {@code x} is not finite
+     * @see Math#round(double)
+     * @see Math#toIntExact(long)
+     */
+    static int toIntExact(double x) {
+        final double r = roundToInteger(x);
+        if (r >= -0x1.0p31 && r < 0x1.0p31) {
+            return (int) r;
+        }
+        throw new ArithmeticException("integer overflow: " + x);
+    }
+
+    /**
+     * Return the whole number that is nearest to the {@code double} argument {@code x}
+     * as an {@code long}, with ties rounding towards positive infinity.
+     *
+     * <p>This will raise an {@link ArithmeticException} if the closest
+     * integer result is not within the range {@code [-2^63, 2^63)},
+     * i.e. it overflows a {@code long}; or the argument {@code x}
+     * is not finite.
+     *
+     * @param x Value.
+     * @return rounded value
+     * @throws ArithmeticException if the {@code result} overflows a {@code long},
+     * or {@code x} is not finite
+     */
+    static long toLongExact(double x) {
+        final double r = roundToInteger(x);
+        if (r >= -0x1.0p63 && r < 0x1.0p63) {
+            return (long) r;
+        }
+        throw new ArithmeticException("long integer overflow: " + x);
+    }
+
+    /**
+     * Return the whole number that is nearest to the {@code double} argument {@code x}
+     * as an {@code int}, with ties rounding towards positive infinity.
+     *
+     * <p>This will raise an {@link ArithmeticException} if the argument {@code x}
+     * is not finite.
+     *
+     * @param x Value.
+     * @return rounded value
+     * @throws ArithmeticException if {@code x} is not finite
+     */
+    static BigInteger toBigIntegerExact(double x) {
+        if (!Double.isFinite(x)) {
+            throw new ArithmeticException("BigInteger overflow: " + x);
+        }
+        final double r = roundToInteger(x);
+        if (r >= -0x1.0p63 && r < 0x1.0p63) {
+            // Representable as a long
+            return BigInteger.valueOf((long) r);
+        }
+        // Large result
+        return new BigDecimal(r).toBigInteger();
+    }
+
+    /**
+     * Get the whole number that is the nearest to x, with ties rounding towards positive infinity.
+     *
+     * <p>This method is intended to perform the equivalent of
+     * {@link Math#round(double)} without converting to a {@code long} primitive type.
+     * This allows the domain of the result to be checked against the range {@code [-2^63, 2^63)}.
+     *
+     * <p>Note: Adapted from {@code o.a.c.math4.AccurateMath.rint} and
+     * modified to perform rounding towards positive infinity.
+     *
+     * @param x Number from which nearest whole number is requested.
+     * @return a double number r such that r is an integer {@code r - 0.5 <= x < r + 0.5}
+     */
+    private static double roundToInteger(double x) {
+        final double y = Math.floor(x);
+        final double d = x - y;
+        if (d >= HALF) {
+            // Here we do not preserve the sign of the operand in the case
+            // of -0.5 < x <= -0.0 since the rounded result is required as an integer.
+            // if y == -1.0:
+            //    return -0.0
+            return y + 1.0;
+        }
+        return y;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMean.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMean.java
new file mode 100644
index 0000000..0713047
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMean.java
@@ -0,0 +1,144 @@
+/*
+ * 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;
+
+/**
+ * Computes the arithmetic mean of the available values. Uses the following definition
+ * of the <em>sample mean</em>:
+ *
+ * <p>\[ \frac{1}{n} \sum_{i=1}^n x_i \]
+ *
+ * <p>where \( n \) is the number of samples.
+ *
+ * <ul>
+ *   <li>The result is {@code NaN} if no values are added.
+ * </ul>
+ *
+ * <p>This class uses an exact integer sum to compute the mean. It supports up to 2<sup>63</sup>
+ * values as the count \( n \) is maintained as a {@code long}.
+ *
+ * <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 {@link java.util.function.IntConsumer#accept(int) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
+ *
+ * <p>However, it is safe to use {@link java.util.function.IntConsumer#accept(int) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
+ * as {@code accumulator} and {@code combiner} 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.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/Mean">Mean (Wikipedia)</a>
+ * @since 1.1 */
+public final class IntMean implements IntStatistic, StatisticAccumulator<IntMean> {
+    /** Limit for small sample size where the sum can exactly map to a double.
+     * This is conservatively set using 2^21 values of 2^31 (2^21 ~ 2 million). */
+    private static final long SMALL_N = 1L << 21;
+
+    /** Sum of the values. */
+    private final Int128 sum;
+    /** Count of values that have been added. */
+    private long n;
+
+    /**
+     * Create an instance.
+     */
+    private IntMean() {
+        this(Int128.create(), 0);
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param sum Sum of the values.
+     * @param n Count of values that have been added.
+     */
+    private IntMean(Int128 sum, int n) {
+        this.sum = sum;
+        this.n = n;
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * <p>The initial result is {@code NaN}.
+     *
+     * @return {@code IntMean} instance.
+     */
+    public static IntMean create() {
+        return new IntMean();
+    }
+
+    /**
+     * Returns an instance populated using the input {@code values}.
+     *
+     * @param values Values.
+     * @return {@code IntMean} instance.
+     */
+    public static IntMean of(int... values) {
+        // Sum of an array cannot exceed a 64-bit long
+        long s = 0;
+        for (final int x : values) {
+            s += x;
+        }
+        // Convert
+        return new IntMean(Int128.of(s), values.length);
+    }
+
+    /**
+     * Updates the state of the statistic to reflect the addition of {@code value}.
+     *
+     * @param value Value.
+     */
+    @Override
+    public void accept(int value) {
+        sum.add(value);
+        n++;
+    }
+
+    /**
+     * Gets the mean of all input values.
+     *
+     * <p>When no values have been added, the result is {@code NaN}.
+     *
+     * @return mean of all values.
+     */
+    @Override
+    public double getAsDouble() {
+        // Fast option when the sum fits within
+        // the mantissa of a double.
+        // Handles n=0 as NaN
+        if (n < SMALL_N) {
+            return (double) sum.lo64() / n;
+        }
+        // Extended precision.
+        // Could divide by DD.of(n) when |n| > 2^53.
+        return sum.toDD().divide(n).doubleValue();
+    }
+
+    @Override
+    public IntMean combine(IntMean other) {
+        sum.add(other.sum);
+        n += other.n;
+        return this;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntVariance.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntVariance.java
new file mode 100644
index 0000000..70d108b
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntVariance.java
@@ -0,0 +1,248 @@
+/*
+ * 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.math.BigInteger;
+
+/**
+ * Computes the variance of the available values. The default implementation uses the
+ * following definition of the <em>sample variance</em>:
+ *
+ * <p>\[ \tfrac{1}{n-1} \sum_{i=1}^n (x_i-\overline{x})^2 \]
+ *
+ * <p>where \( \overline{x} \) is the sample mean, and \( n \) is the number of samples.
+ *
+ * <ul>
+ *   <li>The result is {@code NaN} if no values are added.
+ *   <li>The result is zero if there is one value in the data set.
+ * </ul>
+ *
+ * <p>The use of the term \( n − 1 \) is called Bessel's correction. This is an unbiased
+ * estimator of the variance of a hypothetical infinite population. If the
+ * {@link #setBiased(boolean) biased} option is enabled the normalisation factor is
+ * changed to \( \frac{1}{n} \) for a biased estimator of the <em>sample variance</em>.
+ *
+ * <p>The implementation uses an exact integer sum to compute the scaled (by \( n \))
+ * sum of squared deviations from the mean; this is normalised by the scaled correction factor.
+ *
+ * <p>\[ \frac {n \times \sum_{i=1}^n x_i^2 - (\sum_{i=1}^n x_i)^2}{n \times (n - 1)} \]
+ *
+ * <p>It supports up to 2<sup>63</sup> values as the count \( n \) is maintained
+ * as a {@code long}.
+ *
+ * <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 {@link java.util.function.IntConsumer#accept(int) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
+ *
+ * <p>However, it is safe to use {@link java.util.function.IntConsumer#accept(int) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
+ * as {@code accumulator} and {@code combiner} 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.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/variance">variance (Wikipedia)</a>
+ * @see <a href="https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance">
+ *   Algorithms for computing the variance (Wikipedia)</a>
+ * @see <a href="https://en.wikipedia.org/wiki/Bessel%27s_correction">Bessel&#39;s correction</a>
+ * @since 1.1
+ */
+public final class IntVariance implements IntStatistic, StatisticAccumulator<IntVariance> {
+    /** Small array sample size.
+     * Used to avoid computing with UInt96 then converting to UInt128. */
+    private static final int SMALL_SAMPLE = 10;
+
+    /** Sum of the squared values. */
+    private final UInt128 sumSq;
+    /** Sum of the values. */
+    private final Int128 sum;
+    /** Count of values that have been added. */
+    private long n;
+
+    /** Flag to control if the statistic is biased, or should use a bias correction. */
+    private boolean biased;
+
+    /**
+     * Create an instance.
+     */
+    private IntVariance() {
+        this(UInt128.create(), Int128.create(), 0);
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param sumSq Sum of the squared values.
+     * @param sum Sum of the values.
+     * @param n Count of values that have been added.
+     */
+    private IntVariance(UInt128 sumSq, Int128 sum, int n) {
+        this.sumSq = sumSq;
+        this.sum = sum;
+        this.n = n;
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * <p>The initial result is {@code NaN}.
+     *
+     * @return {@code IntVariance} instance.
+     */
+    public static IntVariance create() {
+        return new IntVariance();
+    }
+
+    /**
+     * Returns an instance populated using the input {@code values}.
+     *
+     * @param values Values.
+     * @return {@code IntVariance} instance.
+     */
+    public static IntVariance of(int... values) {
+        // Small arrays can be processed using the object
+        if (values.length < SMALL_SAMPLE) {
+            final IntVariance stat = new IntVariance();
+            for (final int x : values) {
+                stat.accept(x);
+            }
+            return stat;
+        }
+
+        // Arrays can be processed using specialised counts knowing the maximum limit
+        // for an array is 2^31 values.
+        long s = 0;
+        final UInt96 ss = UInt96.create();
+        // Process pairs as we know two maximum value int^2 will not overflow
+        // an unsigned long.
+        final int end = values.length & ~0x1;
+        for (int i = 0; i < end; i += 2) {
+            final long x = values[i];
+            final long y = values[i + 1];
+            s += x + y;
+            ss.addPositive(x * x + y * y);
+        }
+        if (end < values.length) {
+            final long x = values[end];
+            s += x;
+            ss.addPositive(x * x);
+        }
+
+        // Convert
+        return new IntVariance(UInt128.of(ss), Int128.of(s), values.length);
+    }
+
+    /**
+     * Updates the state of the statistic to reflect the addition of {@code value}.
+     *
+     * @param value Value.
+     */
+    @Override
+    public void accept(int value) {
+        sumSq.addPositive((long) value * value);
+        sum.add(value);
+        n++;
+    }
+
+    /**
+     * Gets the variance of all input values.
+     *
+     * <p>When no values have been added, the result is {@code NaN}.
+     *
+     * @return variance of all values.
+     */
+    @Override
+    public double getAsDouble() {
+        if (n == 0) {
+            return Double.NaN;
+        }
+        // Avoid a divide by zero
+        if (n == 1) {
+            return 0;
+        }
+        final long n0 = biased ? n : n - 1;
+
+        // Sum-of-squared deviations: sum(x^2) - sum(x)^2 / n
+        // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+        // The precursor is computed in integer precision.
+        // The divide uses double precision.
+        // This ensures we avoid cancellation in the difference and use a fast divide.
+        // The result is limited to by the rounding in the double computation.
+
+        // Compute the term if possible using fast integer arithmetic.
+        // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+        // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+        // Both are safe when n < 2^32.
+        double diff;
+        if ((n >>> Integer.SIZE) == 0) {
+            diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDouble();
+        } else {
+            diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                .subtract(square(sum.toBigInteger())).doubleValue();
+        }
+        // Compute the divide in double precision
+        return diff / IntMath.unsignedMultiplyToDouble(n, n0);
+    }
+
+    /**
+     * Convenience method to square a BigInteger.
+     *
+     * @param x Value
+     * @return x^2
+     */
+    private static BigInteger square(BigInteger x) {
+        return x.multiply(x);
+    }
+
+    @Override
+    public IntVariance combine(IntVariance other) {
+        sumSq.add(other.sumSq);
+        sum.add(other.sum);
+        n += other.n;
+        return this;
+    }
+
+    /**
+     * Sets the value of the biased flag. The default value is {@code false}.
+     *
+     * <p>If {@code false} the sum of squared deviations from the sample mean is normalised by
+     * {@code n - 1} where {@code n} is the number of samples. This is Bessel's correction
+     * for an unbiased estimator of the variance of a hypothetical infinite population.
+     *
+     * <p>If {@code true} the sum of squared deviations is normalised by the number of samples
+     * {@code n}.
+     *
+     * <p>Note: This option only applies when {@code n > 1}. The variance of {@code n = 1} is
+     * always 0.
+     *
+     * <p>This flag only controls the final computation of the statistic. The value of this flag
+     * will not affect compatibility between instances during a {@link #combine(IntVariance) combine}
+     * operation.
+     *
+     * @param v Value.
+     * @return {@code this} instance
+     */
+    public IntVariance setBiased(boolean v) {
+        biased = v;
+        return this;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongMean.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongMean.java
new file mode 100644
index 0000000..3f84a9e
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongMean.java
@@ -0,0 +1,141 @@
+/*
+ * 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;
+
+/**
+ * Computes the arithmetic mean of the available values. Uses the following definition
+ * of the <em>sample mean</em>:
+ *
+ * <p>\[ \frac{1}{n} \sum_{i=1}^n x_i \]
+ *
+ * <p>where \( n \) is the number of samples.
+ *
+ * <ul>
+ *   <li>The result is {@code NaN} if no values are added.
+ * </ul>
+ *
+ * <p>This class uses an exact integer sum to compute the mean. It supports up to 2<sup>63</sup>
+ * values as the count \( n \) is maintained as a {@code long}.
+ *
+ * <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 {@link java.util.function.LongConsumer#accept(long) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
+ *
+ * <p>However, it is safe to use {@link java.util.function.LongConsumer#accept(long) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
+ * as {@code accumulator} and {@code combiner} 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 final class LongMean implements LongStatistic, StatisticAccumulator<LongMean> {
+    /** Limit where the absolute sum can exactly map to a double. Set to 2^53. */
+    private static final long SMALL_SUM = 1L << 53;
+
+    /** Sum of the values. */
+    private final Int128 sum;
+    /** Count of values that have been added. */
+    private long n;
+
+    /**
+     * Create an instance.
+     */
+    private LongMean() {
+        this(Int128.create(), 0);
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param sum Sum of the values.
+     * @param n Count of values that have been added.
+     */
+    private LongMean(Int128 sum, int n) {
+        this.sum = sum;
+        this.n = n;
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * <p>The initial result is {@code NaN}.
+     *
+     * @return {@code IntMean} instance.
+     */
+    public static LongMean create() {
+        return new LongMean();
+    }
+
+    /**
+     * Returns an instance populated using the input {@code values}.
+     *
+     * @param values Values.
+     * @return {@code IntMean} instance.
+     */
+    public static LongMean of(long... values) {
+        final Int128 s = Int128.create();
+        for (final long x : values) {
+            s.add(x);
+        }
+        return new LongMean(s, values.length);
+    }
+
+    /**
+     * Updates the state of the statistic to reflect the addition of {@code value}.
+     *
+     * @param value Value.
+     */
+    @Override
+    public void accept(long value) {
+        sum.add(value);
+        n++;
+    }
+
+    /**
+     * Gets the mean of all input values.
+     *
+     * <p>When no values have been added, the result is {@code NaN}.
+     *
+     * @return mean of all values.
+     */
+    @Override
+    public double getAsDouble() {
+        // Fast option when the sum fits within
+        // the mantissa of a double.
+        // Handles n=0 as NaN
+        if (sum.hi64() == 0 && Math.abs(sum.lo64()) < SMALL_SUM) {
+            return (double) sum.lo64() / n;
+        }
+        // Extended precision.
+        // Could divide by DD.of(n) when |n| > 2^53.
+        return sum.toDD().divide(n).doubleValue();
+    }
+
+    @Override
+    public LongMean combine(LongMean other) {
+        sum.add(other.sum);
+        n += other.n;
+        return this;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongVariance.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongVariance.java
new file mode 100644
index 0000000..a6cbe72
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongVariance.java
@@ -0,0 +1,226 @@
+/*
+ * 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.math.BigInteger;
+
+/**
+ * Computes the variance of the available values. The default implementation uses the
+ * following definition of the <em>sample variance</em>:
+ *
+ * <p>\[ \tfrac{1}{n-1} \sum_{i=1}^n (x_i-\overline{x})^2 \]
+ *
+ * <p>where \( \overline{x} \) is the sample mean, and \( n \) is the number of samples.
+ *
+ * <ul>
+ *   <li>The result is {@code NaN} if no values are added.
+ *   <li>The result is zero if there is one value in the data set.
+ * </ul>
+ *
+ * <p>The use of the term \( n − 1 \) is called Bessel's correction. This is an unbiased
+ * estimator of the variance of a hypothetical infinite population. If the
+ * {@link #setBiased(boolean) biased} option is enabled the normalisation factor is
+ * changed to \( \frac{1}{n} \) for a biased estimator of the <em>sample variance</em>.
+ *
+ * <p>The implementation uses an exact integer sum to compute the scaled (by \( n \))
+ * sum of squared deviations from the mean; this is normalised by the scaled correction factor.
+ *
+ * <p>\[ \frac {n \times \sum_{i=1}^n x_i^2 - (\sum_{i=1}^n x_i)^2}{n \times (n - 1)} \]
+ *
+ * <p>It supports up to 2<sup>63</sup> values as the count \( n \) is maintained
+ * as a {@code long}.
+ *
+ * <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 {@link java.util.function.LongConsumer#accept(long) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
+ *
+ * <p>However, it is safe to use {@link java.util.function.LongConsumer#accept(long) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
+ * as {@code accumulator} and {@code combiner} 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.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/variance">variance (Wikipedia)</a>
+ * @see <a href="https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance">
+ *   Algorithms for computing the variance (Wikipedia)</a>
+ * @see <a href="https://en.wikipedia.org/wiki/Bessel%27s_correction">Bessel&#39;s correction</a>
+ * @since 1.1
+ */
+public final class LongVariance implements LongStatistic, StatisticAccumulator<LongVariance> {
+
+    /** Sum of the squared values. */
+    private final UInt192 sumSq;
+    /** Sum of the values. */
+    private final Int128 sum;
+    /** Count of values that have been added. */
+    private long n;
+
+    /** Flag to control if the statistic is biased, or should use a bias correction. */
+    private boolean biased;
+
+    /**
+     * Create an instance.
+     */
+    private LongVariance() {
+        this(UInt192.create(), Int128.create(), 0);
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param sumSq Sum of the squared values.
+     * @param sum Sum of the values.
+     * @param n Count of values that have been added.
+     */
+    private LongVariance(UInt192 sumSq, Int128 sum, int n) {
+        this.sumSq = sumSq;
+        this.sum = sum;
+        this.n = n;
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * <p>The initial result is {@code NaN}.
+     *
+     * @return {@code IntVariance} instance.
+     */
+    public static LongVariance create() {
+        return new LongVariance();
+    }
+
+    /**
+     * Returns an instance populated using the input {@code values}.
+     *
+     * @param values Values.
+     * @return {@code IntVariance} instance.
+     */
+    public static LongVariance of(long... values) {
+        // Note: Arrays could be processed using specialised counts knowing the maximum limit
+        // for an array is 2^31 values. Requires a UInt160.
+
+        final Int128 s = Int128.create();
+        final UInt192 ss = UInt192.create();
+        for (final long x : values) {
+            s.add(x);
+            ss.addSquare(x);
+        }
+        return new LongVariance(ss, s, values.length);
+    }
+
+    /**
+     * Updates the state of the statistic to reflect the addition of {@code value}.
+     *
+     * @param value Value.
+     */
+    @Override
+    public void accept(long value) {
+        sumSq.addSquare(value);
+        sum.add(value);
+        n++;
+    }
+
+    /**
+     * Gets the variance of all input values.
+     *
+     * <p>When no values have been added, the result is {@code NaN}.
+     *
+     * @return variance of all values.
+     */
+    @Override
+    public double getAsDouble() {
+        if (n == 0) {
+            return Double.NaN;
+        }
+        // Avoid a divide by zero
+        if (n == 1) {
+            return 0;
+        }
+        final long n0 = biased ? n : n - 1;
+
+        // Sum-of-squared deviations: sum(x^2) - sum(x)^2 / n
+        // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+        // The precursor is computed in integer precision.
+        // The divide uses double precision.
+        // This ensures we avoid cancellation in the difference and use a fast divide.
+        // The result is limited to max 4 ulp by the rounding in the double computation
+        // When n0*n is < 2^53 the max error is reduced to two roundings.
+
+        // Compute the term if possible using fast integer arithmetic.
+        // 192-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+        // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+        // The first is safe when n < 2^32 but we must check the sum high bits.
+        double diff;
+        if (((n >>> Integer.SIZE) | sum.hi64()) == 0) {
+            diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDouble();
+        } else {
+            diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                .subtract(square(sum.toBigInteger())).doubleValue();
+        }
+        // Compute the divide in double precision
+        return diff / IntMath.unsignedMultiplyToDouble(n, n0);
+    }
+
+    /**
+     * Convenience method to square a BigInteger.
+     *
+     * @param x Value
+     * @return x^2
+     */
+    private static BigInteger square(BigInteger x) {
+        return x.multiply(x);
+    }
+
+    @Override
+    public LongVariance combine(LongVariance other) {
+        sumSq.add(other.sumSq);
+        sum.add(other.sum);
+        n += other.n;
+        return this;
+    }
+
+    /**
+     * Sets the value of the biased flag. The default value is {@code false}.
+     *
+     * <p>If {@code false} the sum of squared deviations from the sample mean is normalised by
+     * {@code n - 1} where {@code n} is the number of samples. This is Bessel's correction
+     * for an unbiased estimator of the variance of a hypothetical infinite population.
+     *
+     * <p>If {@code true} the sum of squared deviations is normalised by the number of samples
+     * {@code n}.
+     *
+     * <p>Note: This option only applies when {@code n > 1}. The variance of {@code n = 1} is
+     * always 0.
+     *
+     * <p>This flag only controls the final computation of the statistic. The value of this flag
+     * will not affect compatibility between instances during a {@link #combine(LongVariance) combine}
+     * operation.
+     *
+     * @param v Value.
+     * @return {@code this} instance
+     */
+    public LongVariance setBiased(boolean v) {
+        biased = v;
+        return this;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt128.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt128.java
new file mode 100644
index 0000000..c4b421c
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt128.java
@@ -0,0 +1,238 @@
+/*
+ * 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.math.BigInteger;
+import java.nio.ByteBuffer;
+
+/**
+ * A mutable 128-bit unsigned integer.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values
+ * generated by squaring {@code int} values.
+ *
+ * @since 1.1
+ */
+final class UInt128 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Data is stored using integers to allow efficient sum-with-carry addition
+
+    /** bits 32-1 (low 32-bits). */
+    private int d;
+    /** bits 64-33. */
+    private int c;
+    /** bits 128-65. */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt128() {
+        // No-op
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     *
+     * @param hi High 64-bits.
+     * @param mid Middle 32-bits.
+     * @param lo Low 32-bits.
+     */
+    private UInt128(long hi, int mid, int lo) {
+        this.d = lo;
+        this.c = mid;
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     */
+    UInt128(long hi, long lo) {
+        this.d = (int) lo;
+        this.c = (int) (lo >>> Integer.SIZE);
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt128 create() {
+        return new UInt128();
+    }
+
+    /**
+     * Create an instance of the {@code UInt96} value.
+     *
+     * @param x Value.
+     * @return the instance
+     */
+    static UInt128 of(UInt96 x) {
+        final int lo = x.lo32();
+        final long hi = x.hi64();
+        final UInt128 y = new UInt128();
+        y.d = lo;
+        y.c = (int) hi;
+        y.ab = hi >>> Integer.SIZE;
+        return y;
+    }
+
+    /**
+     * Adds the value in place. It is assumed to be positive, for example the square of an
+     * {@code int} value. However no check is performed for a negative value.
+     *
+     * <p>Note: This addition handles {@value Long#MIN_VALUE} as an unsigned
+     * value of 2^63.
+     *
+     * @param x Value.
+     */
+    void addPositive(long x) {
+        // Sum with carry.
+        // Assuming x is positive then x + lo will not overflow 64-bits
+        // so we do not have to split x into upper and lower 32-bit values.
+        long s = x + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value in-place.
+     *
+     * @param x Value.
+     */
+    void add(UInt128 x) {
+        // Avoid issues adding to itself
+        final int dd = x.d;
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        long s = (dd & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+    /**
+     * Multiply by the unsigned value.
+     * Any overflow bits are lost.
+     *
+     * @param x Value.
+     * @return the product
+     */
+    UInt128 unsignedMultiply(int x) {
+        final long xx = x & MASK32;
+        // Multiply with carry.
+        long product = xx * (d & MASK32);
+        final int dd = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (c & MASK32);
+        final int cc = (int) product;
+        // Possible overflow here and bits are lost
+        final long aabb = (product >>> Integer.SIZE) + xx * ab;
+        return new UInt128(aabb, cc, dd);
+    }
+
+    /**
+     * Subtracts the value.
+     * Any overflow bits (negative result) are lost.
+     *
+     * @param x Value.
+     * @return the difference
+     */
+    UInt128 subtract(UInt128 x) {
+        // Difference with carry.
+        long diff = (d & MASK32) - (x.d & MASK32);
+        final int dd = (int) diff;
+        diff = (diff >> Integer.SIZE) + (c & MASK32) - (x.c & MASK32);
+        final int cc = (int) diff;
+        // Possible overflow here and bits are lost containing info on the
+        // magnitude of the true negative value
+        final long aabb = (diff >> Integer.SIZE) + ab - x.ab;
+        return new UInt128(aabb, cc, dd);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        // Test if we have more than 63-bits
+        if (ab != 0 || c < 0) {
+            return new BigInteger(1, ByteBuffer.allocate(Integer.BYTES * 4)
+                .putLong(ab)
+                .putInt(c)
+                .putInt(d).array());
+        }
+        // Create from a long
+        return BigInteger.valueOf(lo64());
+    }
+
+    /**
+     * Convert to a {@code double}.
+     *
+     * @return the value
+     */
+    double toDouble() {
+        return IntMath.uin128ToDouble(hi64(), lo64());
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * @return bits 64-1
+     */
+    long lo64() {
+        return (d & MASK32) | ((c & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the low 32-bits as an {@code int} value.
+     *
+     * @return bits 32-1
+     */
+    int lo32() {
+        return d;
+    }
+
+    /**
+     * Return the middle 32-bits as an {@code int} value.
+     *
+     * @return bits 64-33
+     */
+    int mid32() {
+        return c;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 128-65
+     */
+    long hi64() {
+        return ab;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt192.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt192.java
new file mode 100644
index 0000000..f390944
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt192.java
@@ -0,0 +1,247 @@
+/*
+ * 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.math.BigInteger;
+import java.nio.ByteBuffer;
+
+/**
+ * A mutable 192-bit unsigned integer.
+ *
+ * <p>This is a specialised class to implement an accumulator of squared {@code long} values.
+ *
+ * @since 1.1
+ */
+final class UInt192 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Data is stored using integers to allow efficient sum-with-carry addition
+
+    /** bits 32-1 (low 32-bits). */
+    private int f;
+    /** bits 64-33. */
+    private int e;
+    /** bits 96-65. */
+    private int d;
+    /** bits 128-97. */
+    private int c;
+    /** bits 192-129 (high 64-bits). */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt192() {
+        // No-op
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param mid Middle 64-bits.
+     * @param lo Low 64-bits.
+     */
+    UInt192(long hi, long mid, long lo) {
+        this.f = (int) lo;
+        this.e = (int) (lo >>> Integer.SIZE);
+        this.d = (int) mid;
+        this.c = (int) (mid >>> Integer.SIZE);
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     *
+     * @param ab bits 192-129 (high 64-bits).
+     * @param c bits 128-97.
+     * @param d bits 96-65.
+     * @param e bits 64-33.
+     * @param f bits 32-1.
+     */
+    private UInt192(long ab, int c, int d, int e, int f) {
+        this.ab = ab;
+        this.c = c;
+        this.d = d;
+        this.e = e;
+        this.f = f;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt192 create() {
+        return new UInt192();
+    }
+
+    /**
+     * Adds the squared value {@code x * x}.
+     *
+     * @param x Value.
+     */
+    void addSquare(long x) {
+        final long lo = x * x;
+        final long hi = IntMath.squareHigh(x);
+
+        // Sum with carry.
+        long s = (lo & MASK32) + (f & MASK32);
+        f = (int) s;
+        s = (s >>> Integer.SIZE) + (lo >>> Integer.SIZE) + (e & MASK32);
+        e = (int) s;
+        s = (s >>> Integer.SIZE) + (hi & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (hi >>> Integer.SIZE) + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(UInt192 x) {
+        // Avoid issues adding to itself
+        final int ff = x.f;
+        final int ee = x.e;
+        final int dd = x.d;
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        long s = (ff & MASK32) + (f & MASK32);
+        f = (int) s;
+        s = (s >>> Integer.SIZE) + (ee & MASK32) + (e & MASK32);
+        e = (int) s;
+        s = (s >>> Integer.SIZE) + (dd & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+
+    /**
+     * Multiply by the unsigned value.
+     * Any overflow bits are lost.
+     *
+     * @param x Value.
+     * @return the product
+     */
+    UInt192 unsignedMultiply(int x) {
+        final long xx = x & MASK32;
+        // Multiply with carry.
+        long product = xx * (f & MASK32);
+        final int ff = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (e & MASK32);
+        final int ee = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (d & MASK32);
+        final int dd = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (c & MASK32);
+        final int cc = (int) product;
+        // Possible overflow here and bits are lost
+        final long aabb = (product >>> Integer.SIZE) + xx * ab;
+        return new UInt192(aabb, cc, dd, ee, ff);
+    }
+
+    /**
+     * Subtracts the value.
+     * Any overflow bits (negative result) are lost.
+     *
+     * @param x Value.
+     * @return the difference
+     */
+    UInt192 subtract(UInt128 x) {
+        // Difference with carry.
+        // Subtract common part.
+        long diff = (f & MASK32) - (x.lo32()  & MASK32);
+        final int ff = (int) diff;
+        diff = (diff >> Integer.SIZE) + (e & MASK32) - (x.mid32() & MASK32);
+        final int ee = (int) diff;
+        diff = (diff >> Integer.SIZE) + (d & MASK32) - (x.hi64() & MASK32);
+        final int dd = (int) diff;
+        diff = (diff >> Integer.SIZE) + (c & MASK32) - (x.hi64() >>> Integer.SIZE);
+        final int cc = (int) diff;
+        // Possible overflow here and bits are lost containing info on the
+        // magnitude of the true negative value
+        final long aabb = (diff >> Integer.SIZE) + ab;
+        return new UInt192(aabb, cc, dd, ee, ff);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        final ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * 6)
+            .putLong(ab)
+            .putInt(c)
+            .putInt(d)
+            .putInt(e)
+            .putInt(f);
+        // Sign is always positive. This works for zero.
+        return new BigInteger(1, bb.array());
+    }
+
+    /**
+     * Convert to a double.
+     *
+     * @return the value
+     */
+    double toDouble() {
+        final long h = hi64();
+        final long m = mid64();
+        final long l = lo64();
+        if (h == 0) {
+            return IntMath.uin128ToDouble(m, l);
+        }
+        // For correct rounding we use a sticky bit to represent magnitude
+        // lost from the low 64-bits. The result is scaled by 2^64.
+        return IntMath.uin128ToDouble(h, m | ((l == 0) ? 0 : 1)) * 0x1.0p64;
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * @return the low 64-bits
+     */
+    long lo64() {
+        return (f & MASK32) | ((e & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the middle 64-bits as a {@code long} value.
+     *
+     * @return bits 128-65
+     */
+    long mid64() {
+        return (d & MASK32) | ((c & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 192-129
+     */
+    long hi64() {
+        return ab;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt96.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt96.java
new file mode 100644
index 0000000..856a313
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt96.java
@@ -0,0 +1,155 @@
+/*
+ * 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.math.BigInteger;
+import java.nio.ByteBuffer;
+
+/**
+ * A mutable 96-bit unsigned integer.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values
+ * generated by squaring {@code int} values from an array (max observations=2^31).
+ *
+ * @since 1.1
+ */
+final class UInt96 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Low data is stored using an integer to allow efficient sum-with-carry addition
+
+    /** bits 32-1 (low 32-bits). */
+    private int c;
+    /** bits 96-33. */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt96() {
+        // No-op
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param x Value.
+     */
+    private UInt96(long x) {
+        c = (int) x;
+        ab = (int) (x >>> Integer.SIZE);
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 32-bits.
+     */
+    UInt96(long hi, int lo) {
+        this.c = lo;
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt96 create() {
+        return new UInt96();
+    }
+
+    /**
+     * Create an instance of the {@code long} value.
+     * The value is assumed to be an unsigned 64-bit integer.
+     *
+     * @param x Value (must be positive).
+     * @return the instance
+     */
+    static UInt96 of(long x) {
+        return new UInt96(x);
+    }
+
+    /**
+     * Adds the value. It is assumed to be positive, for example the square of an
+     * {@code int} value. However no check is performed for a negative value.
+     *
+     * <p>Note: This addition handles {@value Long#MIN_VALUE} as an unsigned
+     * value of 2^63.
+     *
+     * @param x Value.
+     */
+    void addPositive(long x) {
+        // Sum with carry.
+        // Assuming x is positive then x + lo will not overflow 64-bits
+        // so we do not have to split x into upper and lower 32-bit values.
+        final long s = x + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(UInt96 x) {
+        // Avoid issues adding to itself
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        final long s = (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        if (ab != 0) {
+            final ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * 3)
+                .putLong(ab)
+                .putInt(c);
+            return new BigInteger(1, bb.array());
+        }
+        return BigInteger.valueOf(c & MASK32);
+    }
+
+    /**
+     * Return the lower 32-bits as an {@code int} value.
+     *
+     * @return bits 32-1
+     */
+    int lo32() {
+        return c;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 96-33
+     */
+    long hi64() {
+        return ab;
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseIntStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseIntStatisticTest.java
index 7b05937..4460121 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseIntStatisticTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseIntStatisticTest.java
@@ -29,6 +29,7 @@ import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.statistics.distribution.DoubleTolerance;
 import org.apache.commons.statistics.distribution.TestUtils;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
@@ -395,6 +396,8 @@ abstract class BaseIntStatisticTest<S extends IntStatistic & StatisticAccumulato
      * Creates the equivalent {@link DoubleStatistic} from the {@code values}.
      * This is used to cross-validate the {@link IntStatistic} result.
      *
+     * <p>The test will be skipped if this method returns {@code null}.
+     *
      * @param values Values.
      * @return the statistic
      */
@@ -876,7 +879,9 @@ abstract class BaseIntStatisticTest<S extends IntStatistic & StatisticAccumulato
     @ParameterizedTest
     @MethodSource(value = {"testAccept"})
     final void testVsDoubleStatistic(int[] values) {
-        final double expected = createAsDoubleStatistic(values).getAsDouble();
+        final DoubleStatistic stat = createAsDoubleStatistic(values);
+        Assumptions.assumeTrue(stat != null);
+        final double expected = stat.getAsDouble();
         final DoubleTolerance tol = getToleranceAsDouble();
         TestUtils.assertEquals(expected, Statistics.add(create(), values).getAsDouble(), tol,
             () -> statisticName + " accept: " + format(values));
@@ -1212,17 +1217,4 @@ abstract class BaseIntStatisticTest<S extends IntStatistic & StatisticAccumulato
             .map(BaseIntStatisticTest::format)
             .collect(Collectors.joining(", "));
     }
-
-    /**
-     * Re-throw the error wrapped in an AssertionError with a message that appends the seed
-     * and repeat for the random order test.
-     *
-     * @param e Error.
-     * @param seed Seed.
-     * @param repeat Repeat of the total random permutations.
-     */
-    private static void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
-        throw new AssertionError(String.format("%s; Seed=%s; Repeat=%d/%d",
-            e.getMessage(), Arrays.toString(seed), repeat, RANDOM_PERMUTATIONS), e);
-    }
 }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseLongStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseLongStatisticTest.java
index c3a567d..f8015de 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseLongStatisticTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseLongStatisticTest.java
@@ -29,6 +29,7 @@ import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.statistics.distribution.DoubleTolerance;
 import org.apache.commons.statistics.distribution.TestUtils;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
@@ -67,7 +68,7 @@ import org.junit.jupiter.params.provider.MethodSource;
  *      This is used to verify that test cases are added using the correct type and
  *      test tolerances are configured for {@code double} result types.
  *  <li>{@link #create()}: Create an empty statistic.
- *  <li>{@link #create(int...)}: Create a statistic from a set of values.
+ *  <li>{@link #create(long...)}: Create a statistic from a set of values.
  *  <li>{@link #getEmptyValue()}: The expected value of a statistic when not enough values have
  *      been observed. The minimum number of values can be provided in {@link #getEmptySize()}.
  *  <li>{@link #getExpectedValue(long[])}: A method to compute an expected value for the
@@ -97,7 +98,7 @@ import org.junit.jupiter.params.provider.MethodSource;
  *      The default uses the class name without the "Test" suffix.
  *  <li>{@link #isCombineSymmetric()}: Specify whether to test {@code a.combine(b)} is exactly
  *      equal to {@code b.combine(a)}. The default is {@code true}.
- *  <li>{@link #mapValue(int)}: A method to update the sample data to the valid domain
+ *  <li>{@link #mapValue(long)}: A method to update the sample data to the valid domain
  *      for the statistic. This can be used to alter the default test data, for example
  *      by mapping any negative values to positive.
  * </ul>
@@ -395,6 +396,8 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
      * Creates the equivalent {@link DoubleStatistic} from the {@code values}.
      * This is used to cross-validate the {@link LongStatistic} result.
      *
+     * <p>The test will be skipped if this method returns {@code null}.
+     *
      * @param values Values.
      * @return the statistic
      */
@@ -606,8 +609,8 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
      *  <li>{@link java.util.function.LongConsumer#accept accept}
      *  <li>{@link java.util.function.LongConsumer#accept accept} and
      *      {@link StatisticAccumulator#combine(StatisticResult) combine}
-     *  <li>{@link #create(int...)}
-     *  <li>{@link #create(int...)} and
+     *  <li>{@link #create(long...)}
+     *  <li>{@link #create(long...)} and
      *      {@link StatisticAccumulator#combine(StatisticResult) combine}
      * </ol>
      *
@@ -649,7 +652,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link #create(int...)} method. The expected value and tolerance are supplied
+     * {@link #create(long...)} method. The expected value and tolerance are supplied
      * by the implementing class.
      *
      * @return the stream
@@ -666,7 +669,9 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
     @ParameterizedTest
     @MethodSource(value = {"testAccept"})
     final void testVsDoubleStatistic(long[] values) {
-        final double expected = createAsDoubleStatistic(values).getAsDouble();
+        final DoubleStatistic stat = createAsDoubleStatistic(values);
+        Assumptions.assumeTrue(stat != null);
+        final double expected = stat.getAsDouble();
         final DoubleTolerance tol = getToleranceAsDouble();
         TestUtils.assertEquals(expected, Statistics.add(create(), values).getAsDouble(), tol,
             () -> statisticName + " accept: " + format(values));
@@ -693,7 +698,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
+     * {@link java.util.function.LongConsumer#accept(long) accept} method for each
      * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method. The expected value and tolerance are supplied by the implementing class.
      *
@@ -705,7 +710,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link #create(int...)} method for each array, then the
+     * {@link #create(long...)} method for each array, then the
      * {@link StatisticAccumulator#combine(StatisticResult) combine} method. The
      * expected value and tolerance are supplied by the implementing class.
      *
@@ -717,7 +722,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
+     * {@link java.util.function.LongConsumer#accept(long) accept} method for each
      * element of a parallel stream, then the
      * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      * The expected value and tolerance are supplied by the implementing class.
@@ -863,9 +868,9 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method. The
+     * {@link java.util.function.LongConsumer#accept(long) accept} method. The
      * statistic is created using both the {@link #create()} and the
-     * {@link #create(int...)} methods; the two instances must compute the same result.
+     * {@link #create(long...)} methods; the two instances must compute the same result.
      */
     @ParameterizedTest
     @MethodSource
@@ -876,7 +881,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
     }
 
     /**
-     * Test the computation of the statistic using the {@link #create(int...)} method.
+     * Test the computation of the statistic using the {@link #create(long...)} method.
      */
     @ParameterizedTest
     @MethodSource
@@ -886,7 +891,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
+     * {@link java.util.function.LongConsumer#accept(long) accept} method for each
      * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      */
@@ -898,7 +903,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
+     * {@link java.util.function.LongConsumer#accept(long) accept} method for each
      * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      */
@@ -941,7 +946,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
     /**
      * Test the computation of the statistic using a parallel stream of {@code double}
      * values. The accumulator is the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method; the
+     * {@link java.util.function.LongConsumer#accept(long) accept} method; the
      * combiner is the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      *
@@ -964,7 +969,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Test the computation of the statistic using a parallel stream of {@code long[]}
-     * arrays. The arrays are mapped to a statistic using the {@link #create(int...)}
+     * arrays. The arrays are mapped to a statistic using the {@link #create(long...)}
      * method, and the stream reduced using the
      * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      *
@@ -1212,17 +1217,4 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
             .map(BaseLongStatisticTest::format)
             .collect(Collectors.joining(", "));
     }
-
-    /**
-     * Re-throw the error wrapped in an AssertionError with a message that appends the seed
-     * and repeat for the random order test.
-     *
-     * @param e Error.
-     * @param seed Seed.
-     * @param repeat Repeat of the total random permutations.
-     */
-    private static void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
-        throw new AssertionError(String.format("%s; Seed=%s; Repeat=%d/%d",
-            e.getMessage(), Arrays.toString(seed), repeat, RANDOM_PERMUTATIONS), e);
-    }
 }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java
index f058c84..ef28f72 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java
@@ -17,6 +17,7 @@
 package org.apache.commons.statistics.descriptive;
 
 import java.math.BigInteger;
+import java.util.Arrays;
 import org.apache.commons.statistics.distribution.DoubleTolerance;
 import org.apache.commons.statistics.distribution.DoubleTolerances;
 import org.junit.jupiter.api.Assertions;
@@ -81,7 +82,7 @@ abstract class BaseStatisticTest {
     protected abstract StatisticResult getEmptyValue();
 
     /**
-     * Creates the statistic result using an {@code double} value.
+     * Creates the statistic result using a {@code double} value.
      *
      * @param value Value.
      * @return the statistic result
@@ -148,7 +149,7 @@ abstract class BaseStatisticTest {
 
     /**
      * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testAccept(int[], StatisticResult, DoubleTolerance)} test.
+     * for a test using the primitive consumer {@code accept} method.
      *
      * <p>The default implementation uses {@link #getTolerance()}.
      *
@@ -160,7 +161,7 @@ abstract class BaseStatisticTest {
 
     /**
      * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testArray(int[], StatisticResult, DoubleTolerance)} test.
+     * for a test using creation from a primitive array.
      *
      * <p>The default implementation uses {@link #getTolerance()}.
      *
@@ -172,7 +173,8 @@ abstract class BaseStatisticTest {
 
     /**
      * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testAcceptAndCombine(int[][], StatisticResult, DoubleTolerance)} test.
+     * for a test using the primitive consumer {@code accept} method to create instances
+     * that are combined using {@link StatisticAccumulator#combine(StatisticResult)}.
      *
      * <p>The default implementation uses {@link #getTolerance()}.
      *
@@ -184,7 +186,8 @@ abstract class BaseStatisticTest {
 
     /**
      * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testArrayAndCombine(int[][], StatisticResult, DoubleTolerance)} test.
+     * for a test using creation from a primitive array to create instances
+     * that are combined using {@link StatisticAccumulator#combine(StatisticResult)}.
      *
      * <p>The default implementation uses {@link #getTolerance()}.
      *
@@ -199,8 +202,8 @@ abstract class BaseStatisticTest {
      * This method is used to cross-validate the statistic computation against the reference
      * {@code double} implementation.
      *
-     * <p>The default implementation return {@link #getTolerance()}.
-     * 
+     * <p>The default implementation uses {@link #getTolerance()}.
+     *
      * <p>Note: Computation using {@code double} values may not be as accurate as integer
      * specialisations. This tolerance can be set appropriately to detect errors, for example
      * using a relative tolerance of 1e-12.
@@ -307,7 +310,7 @@ abstract class BaseStatisticTest {
      *
      * @param s Statistic.
      * @return the native result
-     * @see #getNativeResult(IntStatistic)
+     * @see #getResultType()
      */
     private Object getNativeResult(StatisticResult s) {
         try {
@@ -328,4 +331,17 @@ abstract class BaseStatisticTest {
         }
         throw new IllegalStateException("Unrecognised result type: " + getResultType());
     }
+
+    /**
+     * Re-throw the error wrapped in an AssertionError with a message that appends the seed
+     * and repeat for the random order test.
+     *
+     * @param e Error.
+     * @param seed Seed.
+     * @param repeat Repeat of the total random permutations.
+     */
+    static void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
+        throw new AssertionError(String.format("%s; Seed=%s; Repeat=%d/%d",
+            e.getMessage(), Arrays.toString(seed), repeat, RANDOM_PERMUTATIONS), e);
+    }
 }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/Int128Test.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/Int128Test.java
new file mode 100644
index 0000000..042ef10
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/Int128Test.java
@@ -0,0 +1,173 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+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 Int128}.
+ */
+class Int128Test {
+    private static final BigInteger TWO_POW_128 = BigInteger.ONE.shiftLeft(128);
+    private static final BigInteger TWO_POW_127 = BigInteger.ONE.shiftLeft(127);
+    private static final BigInteger MINUS_TWO_POW_127 = BigInteger.ONE.shiftLeft(127).negate();
+
+    @Test
+    void testCreate() {
+        final Int128 v = Int128.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"testAddLong"})
+    void testToBigInteger(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).shiftLeft(64).add(BigInteger.valueOf(b));
+        final Int128 v = new Int128(a, b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final Int128 v = Int128.of(a);
+        v.add(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, 2, Long.MIN_VALUE, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+                builder.accept(Arguments.of(i, -j));
+                builder.accept(Arguments.of(-i, j));
+                builder.accept(Arguments.of(-i, -j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final Int128 v = Int128.create();
+        for (final long x : a) {
+            v.add(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        TestHelper.assertEquals(new BigDecimal(expected), v.toDD(), 0x1.0p-106, "DD");
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> -(x >>> 2)).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, long b, long c, long d) {
+        final Int128 x = new Int128(a, b);
+        final Int128 y = new Int128(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo64());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The Int128 result is a signed 128-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [2^-127, 2^127).
+        // Since the overflow will be at most 1-bit we can wrap the value
+        // using +/- 2^128.
+        if (expected.compareTo(TWO_POW_127) >= 0) {
+            // too high
+            expected = expected.subtract(TWO_POW_128);
+        } else if (expected.compareTo(MINUS_TWO_POW_127) < 0) {
+            // too low
+            expected = expected.add(TWO_POW_128);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check floating-point representation
+        TestHelper.assertEquals(new BigDecimal(expected), x.toDD(), 0x1.0p-106, "DD");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.compareTo(TWO_POW_127) >= 0) {
+            // too high
+            expected = expected.subtract(TWO_POW_128);
+        } else if (expected.compareTo(MINUS_TWO_POW_127) < 0) {
+            // too low
+            expected = expected.add(TWO_POW_128);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 1, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSquareLow(long a) {
+        final BigInteger expected = BigInteger.valueOf(a).pow(2);
+        final UInt128 v = Int128.of(a).squareLow();
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static LongStream testSquareLow() {
+        final LongStream.Builder builder = LongStream.builder();
+        final long[] x = {0, 1, Long.MIN_VALUE, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            builder.accept(i);
+            builder.accept(-i);
+        }
+        RandomSource.XO_RO_SHI_RO_128_PP.create().longs(20).forEach(builder);
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMathTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMathTest.java
new file mode 100644
index 0000000..c390041
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMathTest.java
@@ -0,0 +1,206 @@
+/*
+ * 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.math.BigInteger;
+import java.util.function.Supplier;
+import java.util.stream.DoubleStream;
+import java.util.stream.LongStream;
+import java.util.stream.LongStream.Builder;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Test for {@link IntMath}.
+ */
+class IntMathTest {
+    /** 2^63. */
+    private static final BigInteger TWO_POW_63 = BigInteger.ONE.shiftLeft(63);
+
+    @ParameterizedTest
+    @MethodSource
+    void testSquareHigh(long a) {
+        final long actual = IntMath.squareHigh(a);
+        final long expected = BigInteger.valueOf(a).pow(2).shiftRight(64).longValue();
+        Assertions.assertEquals(expected, actual);
+    }
+
+    static LongStream testSquareHigh() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Builder builder = LongStream.builder();
+        builder.accept(0);
+        builder.accept(Long.MAX_VALUE);
+        builder.accept(Long.MIN_VALUE);
+        rng.ints(5).forEach(builder::accept);
+        rng.longs(50).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 1).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 2).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 5).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 13).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 35).forEach(builder::accept);
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testUnsignedMultiplyHigh(long a, long b) {
+        final long actual = IntMath.unsignedMultiplyHigh(a, b);
+        final BigInteger bi1 = toUnsignedBigInteger(a);
+        final BigInteger bi2 = toUnsignedBigInteger(b);
+        final BigInteger expected = bi1.multiply(bi2);
+        Assertions.assertEquals(expected.shiftRight(Long.SIZE).longValue(), actual,
+            () -> String.format("%s * %s", bi1, bi2));
+        final double x = expected.doubleValue();
+        Assertions.assertEquals(x, IntMath.unsignedMultiplyToDouble(a, b),
+            () -> String.format("double2 %s * %s", bi1, bi2));
+    }
+
+    static Stream<Arguments> testUnsignedMultiplyHigh() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] values = {
+            -1, 0, 1, Long.MAX_VALUE, Long.MIN_VALUE,
+            0xffL, 0xff00L, 0xff0000L, 0xff000000L,
+            0xff00000000L, 0xff0000000000L, 0xff000000000000L, 0xff000000000000L,
+            0xffffL, 0xffff0000L, 0xffff00000000L, 0xffff000000000000L,
+            0xffffffffL, 0xffffffff00000000L
+        };
+        for (final long v1 : values) {
+            for (final long v2 : values) {
+                builder.accept(Arguments.of(v1, v2));
+                builder.accept(Arguments.of(v1 >>> 15, v2 >>> 18));
+            }
+        }
+        for (int i = 0; i < 200; i++) {
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    /**
+     * Create a big integer treating the value as unsigned.
+     *
+     * @param v Value
+     * @return the big integer
+     */
+    private static BigInteger toUnsignedBigInteger(long v) {
+        return v < 0 ?
+            TWO_POW_63.add(BigInteger.valueOf(v & Long.MAX_VALUE)) :
+            BigInteger.valueOf(v);
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testUin128ToDouble(long a, long b) {
+        final BigInteger bi1 = toUnsignedBigInteger(a).shiftLeft(Long.SIZE);
+        final BigInteger bi2 = toUnsignedBigInteger(b);
+        final double x = bi1.add(bi2).doubleValue();
+        Assertions.assertEquals(x, IntMath.uin128ToDouble(a, b),
+            () -> String.format("%s + %s", a, b));
+    }
+
+    static Stream<Arguments> testUin128ToDouble() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        for (int i = 0; i < 100; i++) {
+            long a = rng.nextLong();
+            long b = rng.nextLong();
+            builder.accept(Arguments.of(a, b));
+            builder.accept(Arguments.of(0, b));
+            // Edge cases where trailing bits are required for rounding.
+            // Create a 55-bit number. Ensure the highest bit is set.
+            a = (a << 9) | Long.MIN_VALUE;
+            // Shift right and carry bits down.
+            int shift = rng.nextInt(1, 64);
+            long c = a >>> shift;
+            long d = a << -shift;
+            // Check
+            Assertions.assertEquals(Long.bitCount(a), Long.bitCount(c) + Long.bitCount(d));
+            builder.accept(Arguments.of(c, d));
+            // Add a trailing bit that may change rounding
+            builder.accept(Arguments.of(c, d | 1));
+            // Repeat for special case of a 64-bit unsigned integer
+            builder.accept(Arguments.of(0, a | 1));
+        }
+        // At least one case where the trailing bit does effect rounding
+        // 54-bits all set is an odd number + 0.5
+        builder.accept(Arguments.of(1, (1L << 11)));
+        builder.accept(Arguments.of(1, (1L << 11) | 1));
+        // Unset the second to last bit and repeat above is an even number + 0.5
+        builder.accept(Arguments.of(1, ((1L & ~0x2) << 11)));
+        builder.accept(Arguments.of(1, ((1L & ~0x2) << 11) | 1));
+        return builder.build();
+    }
+
+    /**
+     * Test round-to-int exact matches the result generated using JDK Math functions.
+     * The only exception is NaN which will round to zero (see {@link #testToIntExactNaN()}.
+     *
+     * <p>Note: Rounding for all types is tested in StatisticResultTest.
+     */
+    @ParameterizedTest
+    @MethodSource
+    @ValueSource(doubles = {-1.265384, 67.346578, 72893.5, -42.678,
+        Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY})
+    void testToIntExact(double x) {
+        for (final int sign : new int[] {-1, 1}) {
+            final double y = x * sign;
+            final Supplier<String> intMsg = () -> String.valueOf(y);
+            Integer i = null;
+            try {
+                i = Math.toIntExact(Math.round(y));
+            } catch (Throwable t) {
+                Assertions.assertThrowsExactly(t.getClass(), () -> IntMath.toIntExact(y), intMsg);
+            }
+            if (i != null) {
+                Assertions.assertEquals(i.intValue(), IntMath.toIntExact(y), intMsg);
+            }
+        }
+    }
+
+    static DoubleStream testToIntExact() {
+        DoubleStream.Builder builder = DoubleStream.builder();
+        for (double x = 0; x < 3.5; x += 0.25) {
+            builder.accept(x);
+        }
+        for (double x = -1.5; x <= 1.5; x += 0.25) {
+            builder.accept(x + Integer.MAX_VALUE);
+            builder.accept(x + Integer.MIN_VALUE);
+        }
+        return builder.build();
+    }
+
+    /**
+     * Test round-to-int exact does not match the result generated using JDK Math functions
+     * for NaN.
+     *
+     * <p>Note: Rounding for all types is tested in StatisticResultTest.
+     */
+    @Test
+    void testToIntExactNaN() {
+        final double x = Double.NaN;
+        Assertions.assertEquals(0, Math.toIntExact(Math.round(x)));
+        Assertions.assertThrowsExactly(ArithmeticException.class, () -> IntMath.toIntExact(x));
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMeanTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMeanTest.java
new file mode 100644
index 0000000..60d5449
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMeanTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+/**
+ * Test for {@link IntMean}.
+ */
+final class IntMeanTest extends BaseIntStatisticTest<IntMean> {
+
+    @Override
+    protected IntMean create() {
+        return IntMean.create();
+    }
+
+    @Override
+    protected IntMean create(int... values) {
+        return IntMean.of(values);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(int... values) {
+        return Mean.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        // Large shifts in the rolling mean are not computed very accurately
+        return DoubleTolerances.relative(5e-8);
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Double.NaN);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(int[] values) {
+        // Use the JDK as a reference implementation
+        final double x = Arrays.stream(values).average().orElse(Double.NaN);
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected DoubleTolerance getTolerance() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        final Stream.Builder<StatisticTestData> builder = Stream.builder();
+        builder.accept(addCase(Integer.MAX_VALUE - 1, Integer.MAX_VALUE));
+        builder.accept(addCase(Integer.MIN_VALUE + 1, Integer.MIN_VALUE));
+        final int[] a = new int[2 * 512 * 512];
+        Arrays.fill(a, 0, a.length / 2, 10);
+        Arrays.fill(a, a.length / 2, a.length, 1);
+        builder.accept(addReference(5.5, a));
+
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Python Numpy v1.25.1: numpy.mean
+        builder.accept(addReference(2.5, tol, 1, 2, 3, 4));
+        builder.accept(addReference(12.0, tol, 5, 9, 13, 14, 10, 12, 11, 15, 19));
+        // R v4.3.1: mean(x)
+        builder.accept(addReference(5.5, tol, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
+        builder.accept(addReference(8.75, tol, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 50));
+        return builder.build();
+    }
+
+    /**
+     * Test a large integer sums that overflow a {@code long}.
+     * Overflow is created by repeat addition.
+     *
+     * <p>Note: Currently no check is made for overflow in the
+     * count of observations. If this overflows then the statistic
+     * will be incorrect so the test is limited to {@code n < 2^63}.
+     */
+    @ParameterizedTest
+    @CsvSource({
+        "-1628367811, -516725738, 60",
+        "627834682, 456456670, 61",
+        "2147483647, 2147483646, 61",
+        "-2147483648, -2147483647, 61",
+    })
+    void testLongOverflow(int x, int y, int exp) {
+        final IntMean s = IntMean.of(x, y);
+        final double mean = ((long) x + y) * 0.5;
+        for (int i = 0; i < exp; i++) {
+            // Assumes the sum as a long will overflow
+            s.combine(s);
+            Assertions.assertEquals(mean, s.getAsDouble());
+        }
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntVarianceTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntVarianceTest.java
new file mode 100644
index 0000000..5eea9e8
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntVarianceTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+import org.apache.commons.statistics.distribution.TestUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link IntVariance}.
+ */
+final class IntVarianceTest extends BaseIntStatisticTest<IntVariance> {
+
+    @Override
+    protected IntVariance create() {
+        return IntVariance.create();
+    }
+
+    @Override
+    protected IntVariance create(int... values) {
+        return IntVariance.of(values);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(int... values) {
+        return Variance.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        return DoubleTolerances.ulps(20);
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Double.NaN);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(int[] values) {
+        if (values.length == 1) {
+            return createStatisticResult(0.0);
+        }
+        final long s = Arrays.stream(values).asLongStream().sum();
+        final BigInteger ss = Arrays.stream(values)
+            .mapToObj(i -> BigInteger.valueOf((long) i * i))
+            .reduce(BigInteger.ZERO, BigInteger::add);
+        final MathContext mc = MathContext.DECIMAL128;
+        final int n = values.length;
+        // var = (n * sum(x^2) - sum(x)^2) / (n * (n-1))
+        // Exact numerator
+        final BigInteger num = ss.multiply(BigInteger.valueOf(n)).subtract(
+            BigInteger.valueOf(s).pow(2));
+        // Exact divide
+        final double x = new BigDecimal(num)
+            .divide(BigDecimal.valueOf(n * (n - 1L)), mc)
+            .doubleValue();
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected DoubleTolerance getTolerance() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        final Stream.Builder<StatisticTestData> builder = Stream.builder();
+        builder.accept(addCase(Integer.MAX_VALUE - 1, Integer.MAX_VALUE));
+        builder.accept(addCase(Integer.MIN_VALUE + 1, Integer.MIN_VALUE));
+
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Python Numpy v1.25.1: numpy.var(x, ddof=1)
+        builder.accept(addReference(1.6666666666666667, tol, 1, 2, 3, 4));
+        builder.accept(addReference(7.454545454545454, tol,
+            14, 8, 11, 10, 7, 9, 10, 11, 10, 15, 5, 10));
+        // R v4.3.1: var(x)
+        builder.accept(addReference(9.166666666666666, tol, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
+        builder.accept(addReference(178.75, tol, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 50));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testBiased(int[] values, double biased, double unbiased, DoubleTolerance tol) {
+        final IntVariance stat = IntVariance.of(values);
+        // Default is unbiased
+        final double actualUnbiased = stat.getAsDouble();
+        TestUtils.assertEquals(unbiased, actualUnbiased, tol, () -> "Unbiased: " + format(values));
+        Assertions.assertSame(stat, stat.setBiased(true));
+        final double acutalBiased = stat.getAsDouble();
+        TestUtils.assertEquals(biased, acutalBiased, tol, () -> "Biased: " + format(values));
+        // The mutable state can be switched back and forth
+        Assertions.assertSame(stat, stat.setBiased(false));
+        Assertions.assertEquals(actualUnbiased, stat.getAsDouble(), () -> "Unbiased: " + format(values));
+        Assertions.assertSame(stat, stat.setBiased(true));
+        Assertions.assertEquals(acutalBiased, stat.getAsDouble(), () -> "Biased: " + format(values));
+    }
+
+    static Stream<Arguments> testBiased() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Note: Biased variance is ((10-5.5)**2 + (1-5.5)**2)/2 = 20.25
+        // Scale by (2 * 512 * 512) / (2 * 512 * 512 - 1)
+        // The variance is invariant to shift
+        final int shift = 253674678;
+        final int[] a = new int[2 * 512 * 512];
+        Arrays.fill(a, 0, a.length / 2, 10 + shift);
+        Arrays.fill(a, a.length / 2, a.length, 1 + shift);
+        builder.accept(Arguments.of(a, 20.25, 20.250038623883484, tol));
+
+        // Python Numpy v1.25.1: numpy.var(x, ddof=0/1)
+        // Note: Numpy allows other degrees of freedom adjustment than 0 or 1.
+        builder.accept(Arguments.of(new int[] {1, 2, 3}, 0.6666666666666666, 1, tol));
+        builder.accept(Arguments.of(new int[] {1, 2}, 0.25, 0.5, tol));
+        // Matlab R2023s: var(x, 1/0)
+        // Matlab only allows turning the biased option on (1) or off (0).
+        // Note: Numpy will return NaN for ddof=1 when the array length is 1 (since 0 / 0 = NaN).
+        // This implementation matches the behaviour of Matlab which returns zero.
+        builder.accept(Arguments.of(new int[] {1}, 0, 0, tol));
+        builder.accept(Arguments.of(new int[] {1, 2, 4, 8}, 7.1875, 9.583333333333334, tol));
+        return builder.build();
+    }
+
+    /**
+     * Test a large integer sums that overflow a {@code long}.
+     * Overflow is created by repeat addition.
+     *
+     * <p>Note: Currently no check is made for overflow in the
+     * count of observations. If this overflows then the statistic
+     * will be incorrect so the test is limited to {@code n < 2^63}.
+     */
+    @ParameterizedTest
+    @CsvSource({
+        "-1628367811, -516725738, 60",
+        "627834682, 456456670, 61",
+        "2147483647, 2147483646, 61",
+        "-2147483648, -2147483647, 61",
+    })
+    void testLongOverflow(int x, int y, int exp) {
+        final IntVariance s = IntVariance.of(x, y);
+        // var = sum((x - mean)^2) / (n-1)
+        //     = (n * sum(x^2) - sum(x)^2) / (n * (n-1))
+        long n = 2;
+        BigInteger term1 = BigInteger.valueOf((long) x * x).add(BigInteger.valueOf((long) y * y));
+        BigInteger term2 = BigInteger.valueOf((long) x + y);
+        final DoubleTolerance tol = DoubleTolerances.ulps(2);
+        for (int i = 0; i < exp; i++) {
+            // Assumes the sum as a long will overflow
+            s.combine(s);
+            n <<= 1;
+            term1 = term1.add(term1);
+            term2 = term2.add(term2);
+            final double expected = new BigDecimal(
+                    term1.multiply(BigInteger.valueOf(n)).subtract(term2.pow(2)))
+                .divide(
+                    new BigDecimal(BigInteger.valueOf(n).multiply(BigInteger.valueOf(n - 1))),
+                    MathContext.DECIMAL128)
+                .doubleValue();
+            TestUtils.assertEquals(expected, s.getAsDouble(), tol);
+        }
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMeanTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMeanTest.java
new file mode 100644
index 0000000..0d0f86b
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMeanTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+/**
+ * Test for {@link LongMean}.
+ */
+final class LongMeanTest extends BaseLongStatisticTest<LongMean> {
+
+    @Override
+    protected LongMean create() {
+        return LongMean.create();
+    }
+
+    @Override
+    protected LongMean create(long... values) {
+        return LongMean.of(values);
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Double.NaN);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(long... values) {
+        return Mean.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        // Data with large shifts in the rolling mean is not computed very accurately
+        return DoubleTolerances.relative(5e-8);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(long[] values) {
+        final BigInteger sum = Arrays.stream(values)
+            .mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger.ZERO, BigInteger::add);
+        final double x = new BigDecimal(sum)
+            .divide(BigDecimal.valueOf(values.length), MathContext.DECIMAL128)
+            .doubleValue();
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected DoubleTolerance getTolerance() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        final Stream.Builder<StatisticTestData> builder = Stream.builder();
+        builder.accept(addCase(Long.MAX_VALUE - 1, Long.MAX_VALUE));
+        builder.accept(addCase(Long.MIN_VALUE + 1, Long.MIN_VALUE));
+        final long[] a = new long[2 * 512 * 512];
+        Arrays.fill(a, 0, a.length / 2, 10);
+        Arrays.fill(a, a.length / 2, a.length, 1);
+        builder.accept(addReference(5.5, a));
+
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Python Numpy v1.25.1: numpy.mean
+        builder.accept(addReference(2.5, tol, 1, 2, 3, 4));
+        builder.accept(addReference(12.0, tol, 5, 9, 13, 14, 10, 12, 11, 15, 19));
+        // R v4.3.1: mean(x)
+        builder.accept(addReference(5.5, tol, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
+        builder.accept(addReference(8.75, tol, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 50));
+        return builder.build();
+    }
+
+    /**
+     * Test a large integer sums that overflow a {@code long}.
+     * Overflow is created by repeat addition.
+     *
+     * <p>Note: Currently no check is made for overflow in the
+     * count of observations. If this overflows then the statistic
+     * will be incorrect so the test is limited to {@code n < 2^63}.
+     */
+    @ParameterizedTest
+    @CsvSource({
+        "-1628367672438123811, -97927322516725738, 60",
+        "3279208082627834682, 4234564566706285432, 61",
+        "9223372036854775807, 9223372036854775806, 61",
+        "-9223372036854775808, -9223372036854775807, 61",
+    })
+    void testLongOverflow(long x, long y, int exp) {
+        final LongMean s = LongMean.of(x, y);
+        final double mean = BigInteger.valueOf(x)
+            .add(BigInteger.valueOf(y)).doubleValue() * 0.5;
+        for (int i = 0; i < exp; i++) {
+            // Assumes the sum as a long will overflow
+            s.combine(s);
+            Assertions.assertEquals(mean, s.getAsDouble());
+        }
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongVarianceTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongVarianceTest.java
new file mode 100644
index 0000000..b17c2f4
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongVarianceTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+import org.apache.commons.statistics.distribution.TestUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link LongVariance}.
+ */
+final class LongVarianceTest extends BaseLongStatisticTest<LongVariance> {
+
+    @Override
+    protected LongVariance create() {
+        return LongVariance.create();
+    }
+
+    @Override
+    protected LongVariance create(long... values) {
+        return LongVariance.of(values);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(long... values) {
+        if (values.length == 0) {
+            return Variance.create();
+        }
+        // Detect cases where all the values are the same double (no variance)
+        // and map values with a shift.
+        if (values.length > 1) {
+            final double first = values[0];
+            if (!Arrays.stream(values).asDoubleStream().filter(x -> x != first).findAny().isPresent()) {
+                final long shift = Arrays.stream(values).min().orElse(0);
+                values = Arrays.stream(values).map(x -> x - shift).toArray();
+            }
+        }
+        return Variance.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        return DoubleTolerances.ulps(20);
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Double.NaN);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(long[] values) {
+        if (values.length == 1) {
+            return createStatisticResult(0.0);
+        }
+        final BigInteger s = Arrays.stream(values).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger.ZERO, BigInteger::add);
+        final BigInteger ss = Arrays.stream(values)
+            .mapToObj(i -> BigInteger.valueOf(i).pow(2))
+            .reduce(BigInteger.ZERO, BigInteger::add);
+        final MathContext mc = MathContext.DECIMAL128;
+        final int n = values.length;
+        // var = (n * sum(x^2) - sum(x)^2) / (n * (n-1))
+        // Exact numerator
+        final BigInteger num = ss.multiply(BigInteger.valueOf(n)).subtract(s.pow(2));
+        // Exact divide
+        final double x = new BigDecimal(num)
+            .divide(BigDecimal.valueOf(n * (n - 1L)), mc)
+            .doubleValue();
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected DoubleTolerance getTolerance() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        final Stream.Builder<StatisticTestData> builder = Stream.builder();
+        builder.accept(addCase(Long.MAX_VALUE - 1, Long.MAX_VALUE));
+        builder.accept(addCase(Long.MIN_VALUE + 1, Long.MIN_VALUE));
+
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Python Numpy v1.25.1: numpy.var(x, ddof=1)
+        builder.accept(addReference(1.6666666666666667, tol, 1, 2, 3, 4));
+        builder.accept(addReference(7.454545454545454, tol,
+            14, 8, 11, 10, 7, 9, 10, 11, 10, 15, 5, 10));
+        // R v4.3.1: var(x)
+        builder.accept(addReference(9.166666666666666, tol, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
+        builder.accept(addReference(178.75, tol, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 50));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testBiased(long[] values, double biased, double unbiased, DoubleTolerance tol) {
+        final LongVariance stat = LongVariance.of(values);
+        // Default is unbiased
+        final double actualUnbiased = stat.getAsDouble();
+        TestUtils.assertEquals(unbiased, actualUnbiased, tol, () -> "Unbiased: " + format(values));
+        Assertions.assertSame(stat, stat.setBiased(true));
+        final double acutalBiased = stat.getAsDouble();
+        TestUtils.assertEquals(biased, acutalBiased, tol, () -> "Biased: " + format(values));
+        // The mutable state can be switched back and forth
+        Assertions.assertSame(stat, stat.setBiased(false));
+        Assertions.assertEquals(actualUnbiased, stat.getAsDouble(), () -> "Unbiased: " + format(values));
+        Assertions.assertSame(stat, stat.setBiased(true));
+        Assertions.assertEquals(acutalBiased, stat.getAsDouble(), () -> "Biased: " + format(values));
+    }
+
+    static Stream<Arguments> testBiased() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Note: Biased variance is ((10-5.5)**2 + (1-5.5)**2)/2 = 20.25
+        // Scale by (2 * 512 * 512) / (2 * 512 * 512 - 1)
+        // The variance is invariant to shift
+        final long shift = -1379182644762676428L;
+        final long[] a = new long[2 * 512 * 512];
+        Arrays.fill(a, 0, a.length / 2, 10 + shift);
+        Arrays.fill(a, a.length / 2, a.length, 1 + shift);
+        builder.accept(Arguments.of(a, 20.25, 20.250038623883484, tol));
+
+        // Python Numpy v1.25.1: numpy.var(x, ddof=0/1)
+        // Note: Numpy allows other degrees of freedom adjustment than 0 or 1.
+        builder.accept(Arguments.of(new long[] {1, 2, 3}, 0.6666666666666666, 1, tol));
+        builder.accept(Arguments.of(new long[] {1, 2}, 0.25, 0.5, tol));
+        // Matlab R2023s: var(x, 1/0)
+        // Matlab only allows turning the biased option on (1) or off (0).
+        // Note: Numpy will return NaN for ddof=1 when the array length is 1 (since 0 / 0 = NaN).
+        // This implementation matches the behaviour of Matlab which returns zero.
+        builder.accept(Arguments.of(new long[] {1}, 0, 0, tol));
+        builder.accept(Arguments.of(new long[] {1, 2, 4, 8}, 7.1875, 9.583333333333334, tol));
+        return builder.build();
+    }
+
+    /**
+     * Test a large integer sums that overflow a {@code long}.
+     * Overflow is created by repeat addition.
+     *
+     * <p>Note: Currently no check is made for overflow in the
+     * count of observations. If this overflows then the statistic
+     * will be incorrect so the test is limited to {@code n < 2^63}.
+     */
+    @ParameterizedTest
+    @CsvSource({
+        "-1628367672438123811, -97927322516725738, 60",
+        "3279208082627834682, 4234564566706285432, 61",
+        "9223372036854775807, 9223372036854775806, 61",
+        "-9223372036854775808, -9223372036854775807, 61",
+    })
+    void testLongOverflow(long x, long y, int exp) {
+        final LongVariance s = LongVariance.of(x, y);
+        // var = sum((x - mean)^2) / (n-1)
+        //     = (n * sum(x^2) - sum(x)^2) / (n * (n-1))
+        long n = 2;
+        BigInteger term1 = BigInteger.valueOf(x).pow(2).add(BigInteger.valueOf(y).pow(2));
+        BigInteger term2 = BigInteger.valueOf(x).add(BigInteger.valueOf(y));
+        final DoubleTolerance tol = DoubleTolerances.ulps(2);
+        for (int i = 0; i < exp; i++) {
+            // Assumes the sum as a long will overflow
+            s.combine(s);
+            n <<= 1;
+            term1 = term1.add(term1);
+            term2 = term2.add(term2);
+            final double expected = new BigDecimal(
+                    term1.multiply(BigInteger.valueOf(n)).subtract(term2.pow(2)))
+                .divide(
+                    new BigDecimal(BigInteger.valueOf(n).multiply(BigInteger.valueOf(n - 1))),
+                    MathContext.DECIMAL128)
+                .doubleValue();
+            TestUtils.assertEquals(expected, s.getAsDouble(), tol);
+        }
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java
index 2eb35e9..4df0044 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java
@@ -21,6 +21,7 @@ import java.math.BigInteger;
 import java.math.MathContext;
 import java.util.Arrays;
 import java.util.function.Supplier;
+import org.apache.commons.numbers.core.DD;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.simple.RandomSource;
 import org.apache.commons.statistics.distribution.DoubleTolerance;
@@ -447,4 +448,131 @@ final class TestHelper {
             }
         }
     }
+
+    // DD equality checks adapted from o.a.c.numbers.core.TestUtils
+
+    /**
+     * Assert the two numbers are equal within the provided relative error.
+     *
+     * <p>The provided error is relative to the exact result in expected: (e - a) / e.
+     * If expected is zero this division is undefined. In this case the actual must be zero
+     * (no absolute tolerance is supported). The reporting of the error uses the absolute
+     * error and the return value of the relative error is 0. Cases of complete cancellation
+     * should be avoided for benchmarking relative accuracy.
+     *
+     * <p>Note that the actual double-double result is not validated using the high and low
+     * parts individually. These are summed and compared to the expected.
+     *
+     * <p>Set {@code eps} to negative to report the relative error to the stdout and
+     * ignore failures.
+     *
+     * <p>The relative error is signed. The sign of the error
+     * is the same as that returned from Double.compare(actual, expected); it is
+     * computed using {@code actual - expected}.
+     *
+     * @param expected expected value
+     * @param actual actual value
+     * @param eps maximum relative error between the two values
+     * @param msg failure message
+     * @return relative error difference between the values (signed)
+     * @throws NumberFormatException if {@code actual} contains non-finite values
+     */
+    static double assertEquals(BigDecimal expected, DD actual, double eps, String msg) {
+        return assertEquals(expected, actual, eps, () -> msg);
+    }
+
+    /**
+     * Assert the two numbers are equal within the provided relative error.
+     *
+     * <p>The provided error is relative to the exact result in expected: (e - a) / e.
+     * If expected is zero this division is undefined. In this case the actual must be zero
+     * (no absolute tolerance is supported). The reporting of the error uses the absolute
+     * error and the return value of the relative error is 0. Cases of complete cancellation
+     * should be avoided for benchmarking relative accuracy.
+     *
+     * <p>Note that the actual double-double result is not validated using the high and low
+     * parts individually. These are summed and compared to the expected.
+     *
+     * <p>Set {@code eps} to negative to report the relative error to the stdout and
+     * ignore failures.
+     *
+     * <p>The relative error is signed. The sign of the error
+     * is the same as that returned from Double.compare(actual, expected); it is
+     * computed using {@code actual - expected}.
+     *
+     * @param expected expected value
+     * @param actual actual value
+     * @param eps maximum relative error between the two values
+     * @param msg failure message
+     * @return relative error difference between the values (signed)
+     * @throws NumberFormatException if {@code actual} contains non-finite values
+     */
+    static double assertEquals(BigDecimal expected, DD actual, double eps, Supplier<String> msg) {
+        // actual - expected
+        final BigDecimal delta = new BigDecimal(actual.hi())
+            .add(new BigDecimal(actual.lo()))
+            .subtract(expected);
+        boolean equal;
+        if (expected.compareTo(BigDecimal.ZERO) == 0) {
+            // Edge case. Currently an absolute tolerance is not supported as summation
+            // to zero cases generated in testing all pass.
+            equal = actual.doubleValue() == 0;
+
+            // DEBUG:
+            if (eps < 0) {
+                if (!equal) {
+                    printf("%sexpected 0 != actual <%s + %s> (abs.error=%s)%n",
+                        prefix(msg), actual.hi(), actual.lo(), delta.doubleValue());
+                }
+            } else if (!equal) {
+                Assertions.fail(String.format("%sexpected 0 != actual <%s + %s> (abs.error=%s)",
+                    prefix(msg), actual.hi(), actual.lo(), delta.doubleValue()));
+            }
+
+            return 0;
+        }
+
+        final double rel = delta.divide(expected, MathContext.DECIMAL128).doubleValue();
+        // Allow input of a negative maximum ULPs
+        equal = Math.abs(rel) <= Math.abs(eps);
+
+        // DEBUG:
+        if (eps < 0) {
+            if (!equal) {
+                printf("%sexpected <%s> != actual <%s + %s> (rel.error=%s (%.3f x tol))%n",
+                    prefix(msg), expected.round(MathContext.DECIMAL128), actual.hi(), actual.lo(),
+                    rel, Math.abs(rel) / eps);
+            }
+        } else if (!equal) {
+            Assertions.fail(String.format("%sexpected <%s> != actual <%s + %s> (rel.error=%s (%.3f x tol))",
+                prefix(msg), expected.round(MathContext.DECIMAL128), actual.hi(), actual.lo(),
+                rel, Math.abs(rel) / eps));
+        }
+
+        return rel;
+    }
+
+    /**
+     * Print a formatted message to stdout.
+     * Provides a single point to disable checkstyle warnings on print statements and
+     * enable/disable all print debugging.
+     *
+     * @param format Format string.
+     * @param args Arguments.
+     */
+    static void printf(String format, Object... args) {
+        // CHECKSTYLE: stop regex
+        System.out.printf(format, args);
+        // CHECKSTYLE: resume regex
+    }
+
+    /**
+     * Get the prefix for the message.
+     *
+     * @param msg Message supplier
+     * @return the prefix
+     */
+    static String prefix(Supplier<String> msg) {
+        return msg == null ? "" : msg.get() + ": ";
+    }
 }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt128Test.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt128Test.java
new file mode 100644
index 0000000..22f3d5f
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt128Test.java
@@ -0,0 +1,239 @@
+/*
+ * 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.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+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 UInt128}.
+ */
+class UInt128Test {
+    private static final BigInteger TWO_POW_128 = BigInteger.ONE.shiftLeft(128);
+
+    @Test
+    void testCreate() {
+        final UInt128 v = UInt128.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @Test
+    void testAddLongMinValue() {
+        final UInt128 v = new UInt128(0, 1268361283468345237L);
+        final BigInteger x = BigInteger.ONE.shiftLeft(63);
+        BigInteger expected = v.toBigInteger();
+        for (int i = 1; i <= 5; i++) {
+            // Accepts a negative value without exception. This is
+            // computed correctly if the current low 32 bits
+            // added to the argument do not overflow. This is always
+            // true for min value as all lower 32-bits are zero.
+            v.addPositive(Long.MIN_VALUE);
+            expected = expected.add(x);
+            Assertions.assertEquals(expected, v.toBigInteger());
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final UInt128 v = new UInt128(0, a);
+        v.addPositive(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt128 v = UInt128.create();
+        for (final long x : a) {
+            Assertions.assertFalse(x < 0, "Value must be positive");
+            v.addPositive(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        Assertions.assertEquals(expected.doubleValue(), v.toDouble(), "double");
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, long b, long c, long d) {
+        final UInt128 x = new UInt128(a, b);
+        final UInt128 y = new UInt128(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo64());
+        Assertions.assertEquals((int) (b >>> Integer.SIZE), x.mid32());
+        Assertions.assertEquals((int) b, x.lo32());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 128-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^128).
+        if (expected.testBit(128)) {
+            expected = expected.flipBit(128);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check floating-point representation
+        Assertions.assertEquals(expected.doubleValue(), x.toDouble(), "double");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(128)) {
+            expected = expected.flipBit(128);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 1, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testOfInt96(long a, int b) {
+        final UInt96 x = new UInt96(a, b);
+        final UInt128 y = UInt128.of(x);
+        Assertions.assertEquals(x.toBigInteger(), y.toBigInteger());
+    }
+
+    static Stream<Arguments> testOfInt96() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final int b = rng.nextInt();
+            builder.accept(Arguments.of(a, b));
+            builder.accept(Arguments.of(0, b));
+            builder.accept(Arguments.of(a, 0));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testMultiplyInt(long a, long b, int n) {
+        assertMultiplyInt(a, b, n);
+        assertMultiplyInt(a >>> 32, b, n);
+        assertMultiplyInt(0, b, n);
+    }
+
+    private static void assertMultiplyInt(long a, long b, int n) {
+        final UInt128 v = new UInt128(a, b);
+        BigInteger expected = v.toBigInteger().multiply(BigInteger.valueOf(n & 0xffff_ffffL));
+        // Clip to 128-bits. Only required if the upper 32-bits are non-zero.
+        final int len = expected.bitLength();
+        if (len > 128 && (v.hi64() >>> Integer.SIZE) != 0) {
+            expected = expected.subtract(expected.shiftRight(128).shiftLeft(128));
+        }
+        Assertions.assertEquals(expected, v.unsignedMultiply(n).toBigInteger());
+    }
+
+    static Stream<Arguments> testMultiplyInt() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final int[] x = {0, 1, -1, Integer.MAX_VALUE, Integer.MIN_VALUE};
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            for (final int n : x) {
+                builder.accept(Arguments.of(a, b, n));
+            }
+            for (int j = 0; j < 5; j++) {
+                builder.accept(Arguments.of(a, b, rng.nextInt()));
+            }
+        }
+        builder.accept(Arguments.of(-1L >>> 32, -1L, -1));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSubtract(long a, long b, long c, long d) {
+        assertSubtract(a, b, c, d);
+        assertSubtract(c, d, a, b);
+    }
+
+    private static void assertSubtract(long a, long b, long c, long d) {
+        final UInt128 x = new UInt128(a, b);
+        final UInt128 y = new UInt128(c, d);
+        BigInteger expected = x.toBigInteger().subtract(y.toBigInteger());
+        if (expected.signum() < 0) {
+            expected = expected.add(TWO_POW_128);
+        }
+        Assertions.assertEquals(expected, x.subtract(y).toBigInteger());
+    }
+
+    static Stream<Arguments> testSubtract() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            final long d = rng.nextLong();
+            builder.accept(Arguments.of(a, b, c, d));
+            builder.accept(Arguments.of(0, 0, c, d));
+            builder.accept(Arguments.of(-1L, -1L, c, d));
+        }
+        builder.accept(Arguments.of(-1L, -1L, -1L, -1L));
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt192Test.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt192Test.java
new file mode 100644
index 0000000..8456855
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt192Test.java
@@ -0,0 +1,208 @@
+/*
+ * 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.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+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 UInt192}.
+ */
+class UInt192Test {
+    private static final BigInteger TWO_POW_192 = BigInteger.ONE.shiftLeft(192);
+
+    @Test
+    void testCreate() {
+        final UInt192 v = UInt192.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddSquareLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).pow(2)
+            .add(BigInteger.valueOf(b).pow(2));
+        final UInt192 v = UInt192.create();
+        v.addSquare(a);
+        v.addSquare(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddSquareLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 61278342166787978L, 42, 8652939272947492397L};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddSquareLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .map(x -> x.pow(2))
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt192 v = UInt192.create();
+        for (final long x : a) {
+            v.addSquare(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        Assertions.assertEquals(expected.doubleValue(), v.toDouble(), "double");
+    }
+
+    static Stream<Arguments> testAddSquareLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt192(long a, long b, long c, long d, long e, long f) {
+        final UInt192 x = new UInt192(a, b, c);
+        final UInt192 y = new UInt192(d, e, f);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.mid64());
+        Assertions.assertEquals(c, x.lo64());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 192-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^192).
+        if (expected.testBit(192)) {
+            expected = expected.flipBit(192);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d, %d) + (%d, %d, %d)", a, b, c, d, e, f));
+        // Check floating-point representation
+        Assertions.assertEquals(expected.doubleValue(), x.toDouble(), "double");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(192)) {
+            expected = expected.flipBit(192);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d, %d) self-addition", d, e, f));
+    }
+
+    static Stream<Arguments> testAddInt192() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testMultiplyInt(long a, long b, long c, int n) {
+        assertMultiplyInt(a, b, c, n);
+        assertMultiplyInt(a >>> 32, b, c, n);
+        assertMultiplyInt(0, b, c, n);
+    }
+
+    private static void assertMultiplyInt(long a, long b, long c, int n) {
+        final UInt192 v = new UInt192(a, b, c);
+        BigInteger expected = v.toBigInteger().multiply(BigInteger.valueOf(n & 0xffff_ffffL));
+        // Clip to 192-bits. Only required if the upper 32-bits are non-zero.
+        final int len = expected.bitLength();
+        if (len > 192 && (a >>> Integer.SIZE) != 0) {
+            expected = expected.subtract(expected.shiftRight(192).shiftLeft(192));
+        }
+        Assertions.assertEquals(expected, v.unsignedMultiply(n).toBigInteger());
+    }
+
+    static Stream<Arguments> testMultiplyInt() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final int[] x = {0, 1, -1, Integer.MAX_VALUE, Integer.MIN_VALUE};
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            for (final int n : x) {
+                builder.accept(Arguments.of(a, b, c, n));
+            }
+            for (int j = 0; j < 5; j++) {
+                builder.accept(Arguments.of(a, b, c, rng.nextInt()));
+            }
+        }
+        builder.accept(Arguments.of(-1L >>> 32, -1L, -1L, -1));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSubtract(long a, long b, long c, long d, long e) {
+        assertSubtract(a, b, c, d, e);
+    }
+
+    private static void assertSubtract(long a, long b, long c, long d, long e) {
+        final UInt192 x = new UInt192(a, b, c);
+        final UInt128 y = new UInt128(d, e);
+        BigInteger expected = x.toBigInteger().subtract(y.toBigInteger());
+        if (expected.signum() < 0) {
+            expected = expected.add(TWO_POW_192);
+        }
+        Assertions.assertEquals(expected, x.subtract(y).toBigInteger());
+    }
+
+    static Stream<Arguments> testSubtract() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            final long d = rng.nextLong();
+            final long e = rng.nextLong();
+            builder.accept(Arguments.of(a, b, c, d, e));
+            builder.accept(Arguments.of(0, 0, 0, d, e));
+            builder.accept(Arguments.of(-1L, -1L, -1L, d, e));
+        }
+        builder.accept(Arguments.of(-1L, -1L, -1L, -1L, -1L));
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt96Test.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt96Test.java
new file mode 100644
index 0000000..f198ab7
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt96Test.java
@@ -0,0 +1,140 @@
+/*
+ * 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.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+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 UInt96}.
+ */
+class UInt96Test {
+    @Test
+    void testCreate() {
+        final UInt96 v = UInt96.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @Test
+    void testAddLongMinValue() {
+        final UInt96 v = UInt96.of(5675757768682342956L);
+        final BigInteger x = BigInteger.ONE.shiftLeft(63);
+        BigInteger expected = v.toBigInteger();
+        for (int i = 1; i <= 5; i++) {
+            // Accepts a negative value without exception. This is
+            // computed correctly if the current low 32 bits
+            // added to the argument do not overflow. This is always
+            // true for min value as all lower 32-bits are zero.
+            v.addPositive(Long.MIN_VALUE);
+            expected = expected.add(x);
+            Assertions.assertEquals(expected, v.toBigInteger());
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final UInt96 v = UInt96.of(a);
+        v.addPositive(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt96 v = UInt96.create();
+        for (final long x : a) {
+            Assertions.assertFalse(x < 0, "Value must be positive");
+            v.addPositive(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, int b, long c, int d) {
+        final UInt96 x = new UInt96(a, b);
+        final UInt96 y = new UInt96(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo32());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 96-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^96).
+        if (expected.testBit(96)) {
+            expected = expected.flipBit(96);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(96)) {
+            expected = expected.flipBit(96);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextInt(), rng.nextLong() >>> 2, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextInt(), rng.nextLong() >>> 1, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextInt(), rng.nextLong() >>> 2, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextInt(), rng.nextLong(), rng.nextInt()));
+        }
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/pom.xml b/commons-statistics-examples/examples-jmh/pom.xml
index f2e9386..ce3319e 100644
--- a/commons-statistics-examples/examples-jmh/pom.xml
+++ b/commons-statistics-examples/examples-jmh/pom.xml
@@ -51,10 +51,14 @@
       <artifactId>commons-rng-simple</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-numbers-fraction</artifactId>
+    </dependency>
+
     <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-numbers-rootfinder</artifactId>
-      <version>${statistics.commons.numbers.version}</version>
     </dependency>
 
     <dependency>
@@ -79,8 +83,14 @@
     <!-- Workaround to avoid duplicating config files. -->
     <statistics.parent.dir>${basedir}/../..</statistics.parent.dir>
 
+    <!-- JDK 11+ required for Math.multiplyHigh -->
+    <maven.compiler.source>11</maven.compiler.source>
+    <maven.compiler.target>11</maven.compiler.target>
+    <maven.compiler.release>11</maven.compiler.release>
+    <commons.compiler.release>11</commons.compiler.release>
+
     <!-- JMH Benchmark related properties: version, name of the benchmarking uber jar. -->
-    <jmh.version>1.33</jmh.version>
+    <jmh.version>1.36</jmh.version>
     <uberjar.name>examples-jmh</uberjar.name>
     <project.mainClass>org.openjdk.jmh.Main</project.mainClass>
     <!-- Disable analysis for benchmarking code. -->
@@ -88,6 +98,17 @@
     <spotbugs.skip>true</spotbugs.skip>
     <!-- Disable JDK compatibility check for benchmarking code. -->
     <animal.sniffer.skip>true</animal.sniffer.skip>
+
+    <!--
+      NOTE:
+      This module uses Java 11 but does not have an explicit module-info for imported packages.
+      A module-info is generated by the moditect plugin. This does not appear to be discovered
+      as it is in the build output directory.
+      This can cause the javadoc plugin to fail when run with the package phase so skip to make
+      this module compatible with the default goal. Run javadoc plugin using e.g.:
+      mvn javadoc:javadoc -Dmaven.javadoc.skip=false
+    -->
+    <maven.javadoc.skip>true</maven.javadoc.skip>
   </properties>
 
   <build>
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128.java
new file mode 100644
index 0000000..66c9e92
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128.java
@@ -0,0 +1,276 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import org.apache.commons.numbers.core.DD;
+
+/**
+ * A mutable 128-bit signed integer.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.Int128} to allow benchmarking.
+ * Additional methods may have been added for comparative benchmarks.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values.
+ *
+ * <p>Note: This number uses a signed long integer representation of:
+ *
+ * <pre>value = 2<sup>64</sup> * hi64 + lo64</pre>
+ *
+ * <p>If the high value is zero then the low value is the long representation of the
+ * number including the sign bit. Otherwise the low value corresponds to a correction
+ * term for the scaled high value which contains the sign-bit of the number.
+ *
+ * @since 1.1
+ */
+final class Int128 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    /** low 64-bits. */
+    private long lo;
+    /** high 64-bits. */
+    private long hi;
+
+    /**
+     * Create an instance.
+     */
+    private Int128() {
+        // No-op
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param x Value.
+     */
+    private Int128(long x) {
+        lo = x;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     */
+    Int128(long hi, long lo) {
+        this.lo = lo;
+        this.hi = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static Int128 create() {
+        return new Int128();
+    }
+
+    /**
+     * Create an instance of the {@code long} value.
+     *
+     * @param x Value.
+     * @return the instance
+     */
+    static Int128 of(long x) {
+        return new Int128(x);
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(long x) {
+        final long y = lo;
+        final long r = y + x;
+        // Overflow if the result has the opposite sign of both arguments
+        // (+,+) -> -
+        // (-,-) -> +
+        // Detect opposite sign:
+        if (((y ^ r) & (x ^ r)) < 0) {
+            // Carry overflow bit
+            hi += x < 0 ? -1 : 1;
+        }
+        lo = r;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add2(long x) {
+        final long y = lo;
+        final long r = y + x;
+        // Overflow if the result has the opposite sign of both arguments
+        // (+,+) -> -
+        // (-,-) -> +
+        // Branchless.
+        // Extract sign bit.
+        long signMask = ((y ^ r) & (x ^ r)) >> 63;
+        // Carry using [0/1] * [+1/-1]
+        hi += signMask & (1 - ((x >>> 62) & 0x2));
+        lo = r;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(Int128 x) {
+        // Avoid issues adding to itself
+        final long l = x.lo;
+        final long h = x.hi;
+        add(l);
+        hi += h;
+    }
+
+    /**
+     * Compute the square of the low 64-bits of this number.
+     *
+     * <p>Warning: This ignores the upper 64-bits. Use with caution.
+     *
+     * @return the square
+     */
+    UInt128 squareLow() {
+        final long x = lo;
+        final long upper = IntMath.squareHigh(x);
+        return new UInt128(upper, x * x);
+    }
+
+    /**
+     * Compute the square of this number.
+     *
+     * @return the square
+     */
+    BigInteger square() {
+        if (hi == 0) {
+            final long x = lo;
+            final long upper = IntMath.squareHigh(x);
+            final long lower = x * x;
+            if (upper == 0) {
+                return BigInteger.valueOf(lower);
+            }
+            return new BigInteger(1, ByteBuffer.allocate(Long.BYTES * 2)
+                .putLong(upper).putLong(lower).array());
+        }
+        final BigInteger result = toBigInteger();
+        return result.multiply(result);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        long h = hi;
+        long l = lo;
+        // Special cases
+        if (h == 0) {
+            return BigInteger.valueOf(l);
+        }
+        if (l == 0) {
+            return BigInteger.valueOf(h).shiftLeft(64);
+        }
+
+        // The representation is 2^64 * hi64 + lo64.
+        // Here we avoid evaluating the addition:
+        // BigInteger.valueOf(l).add(BigInteger.valueOf(h).shiftLeft(64))
+        // It is faster to create from bytes.
+        // BigInteger bytes are an unsigned integer in BigEndian format, plus a sign.
+        // If both values are positive we can use the values unchanged.
+        // Otherwise selective negation is used to create a positive magnitude
+        // and we track the sign.
+        // Note: Negation of -2^63 is valid to create an unsigned 2^63.
+
+        int sign = 1;
+        if ((h ^ l) < 0) {
+            // Opposite signs and lo64 is not zero.
+            // The lo64 bits are an adjustment to the magnitude of hi64
+            // to make it smaller.
+            // Here we rearrange to [2^64 * (hi64-1)] + [2^64 - lo64].
+            // The second term [2^64 - lo64] can use lo64 as an unsigned 64-bit integer.
+            // The first term [2^64 * (hi64-1)] does not work if low is zero.
+            // It would work if zero was detected and we carried the overflow
+            // bit up to h to make it equal to: (h - 1) + 1 == h.
+            // Instead lo64 == 0 is handled as a special case above.
+
+            if (h >= 0) {
+                // Treat (unchanged) low as an unsigned add
+                h = h - 1;
+            } else {
+                // As above with negation
+                h = ~h; // -h - 1
+                l = -l;
+                sign = -1;
+            }
+        } else if (h < 0) {
+            // Invert negative values to create the equivalent positive magnitude.
+            h = -h;
+            l = -l;
+            sign = -1;
+        }
+
+        return new BigInteger(sign,
+            ByteBuffer.allocate(Long.BYTES * 2)
+                .putLong(h).putLong(l).array());
+    }
+
+    /**
+     * Convert to a double-double.
+     *
+     * @return the value
+     */
+    DD toDD() {
+        // Don't combine two 64-bit DD numbers:
+        // DD.of(hi).scalb(64).add(DD.of(lo))
+        // It is more accurate to create a 96-bit number and add the final 32-bits.
+        // Sum low to high.
+        return DD.of(lo).add((hi & MASK32) * 0x1.0p64).add((hi >> Integer.SIZE) * 0x1.0p96);
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * <p>If the high value is zero then the low value is the long representation of the
+     * number including the sign bit. Otherwise this value corresponds to a correction
+     * term for the scaled high value which contains the sign-bit of the number
+     * (see {@link Int128}).
+     *
+     * @return the low 64-bits
+     */
+    long lo64() {
+        return lo;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return the high 64-bits
+     * @see #lo64()
+     */
+    long hi64() {
+        return hi;
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMath.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMath.java
new file mode 100644
index 0000000..4c9292e
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMath.java
@@ -0,0 +1,309 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+
+/**
+ * Support class for integer math.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.IntMath} to allow benchmarking.
+ * Additional methods may have been added for comparative benchmarks.
+ *
+ * @since 1.1
+ */
+final class IntMath {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+    /** Mask for the lower 52-bits of a long. */
+    private static final long MASK52 = 0xf_ffff_ffff_ffffL;
+
+    /** No instances. */
+    private IntMath() {}
+
+    /**
+     * Square the values as if an unsigned 64-bit long to produce the high 64-bits
+     * of the 128-bit unsigned result.
+     *
+     * <p>This method computes the equivalent of:
+     * <pre>{@code
+     * Math.multiplyHigh(x, x)
+     * Math.unsignedMultiplyHigh(x, x) - (((x >> 63) & x) << 1)
+     * }</pre>
+     *
+     * <p>Note: The method {@code Math.multiplyHigh} was added in JDK 9
+     * and should be used as above when the source code targets Java 11
+     * to exploit the intrinsic method.
+     *
+     * <p>Note: The method uses the unsigned multiplication. When the input is negative
+     * it can be adjusted to the signed result by subtracting the argument twice from the
+     * result.
+     *
+     * @param x Value
+     * @return the high 64-bits of the 128-bit result
+     */
+    static long squareHigh(long x) {
+        // Computation is based on the following observation about the upper (a and x)
+        // and lower (b and y) bits of unsigned big-endian integers:
+        //   ab * xy
+        // =  b *  y
+        // +  b * x0
+        // + a0 *  y
+        // + a0 * x0
+        // = b * y
+        // + b * x * 2^32
+        // + a * y * 2^32
+        // + a * x * 2^64
+        //
+        // Summation using a character for each byte:
+        //
+        //             byby byby
+        // +      bxbx bxbx 0000
+        // +      ayay ayay 0000
+        // + axax axax 0000 0000
+        //
+        // The summation can be rearranged to ensure no overflow given
+        // that the result of two unsigned 32-bit integers multiplied together
+        // plus two full 32-bit integers cannot overflow 64 bits:
+        // > long x = (1L << 32) - 1
+        // > x * x + x + x == -1 (all bits set, no overflow)
+        //
+        // The carry is a composed intermediate which will never overflow:
+        //
+        //             byby byby
+        // +           bxbx 0000
+        // +      ayay ayay 0000
+        //
+        // +      bxbx 0000 0000
+        // + axax axax 0000 0000
+
+        final long a = x >>> 32;
+        final long b = x & MASK32;
+
+        final long aa = a * a;
+        final long ab = a * b;
+        final long bb = b * b;
+
+        // Cannot overflow
+        final long carry = (bb >>> 32) +
+                           (ab & MASK32) +
+                            ab;
+        // Note:
+        // low = (carry << 32) | (bb & MASK32)
+        // Benchmarking shows outputting low to a long[] output argument
+        // has no benefit over computing 'low = value * value' separately.
+
+        final long hi = (ab >>> 32) + (carry >>> 32) + aa;
+        // Adjust to the signed result:
+        // if x < 0:
+        //    hi - 2 * x
+        return hi - (((x >> 63) & x) << 1);
+    }
+
+    /**
+     * Multiply the two values as if unsigned 64-bit longs to produce the high 64-bits
+     * of the 128-bit unsigned result.
+     *
+     * <p>This method computes the equivalent of:
+     * <pre>{@code
+     * Math.multiplyHigh(a, b) + ((a >> 63) & b) + ((b >> 63) & a)
+     * }</pre>
+     *
+     * <p>Note: The method {@code Math.multiplyHigh} was added in JDK 9
+     * and should be used as above when the source code targets Java 11
+     * to exploit the intrinsic method.
+     *
+     * <p>Note: The method {@code Math.unsignedMultiplyHigh} was added in JDK 18
+     * and should be used when the source code target allows.
+     *
+     * <p>Taken from {@code o.a.c.rng.core.source64.LXMSupport}.
+     *
+     * @param value1 the first value
+     * @param value2 the second value
+     * @return the high 64-bits of the 128-bit result
+     */
+    static long unsignedMultiplyHigh(long value1, long value2) {
+        // Computation is based on the following observation about the upper (a and x)
+        // and lower (b and y) bits of unsigned big-endian integers:
+        //   ab * xy
+        // =  b *  y
+        // +  b * x0
+        // + a0 *  y
+        // + a0 * x0
+        // = b * y
+        // + b * x * 2^32
+        // + a * y * 2^32
+        // + a * x * 2^64
+        //
+        // Summation using a character for each byte:
+        //
+        //             byby byby
+        // +      bxbx bxbx 0000
+        // +      ayay ayay 0000
+        // + axax axax 0000 0000
+        //
+        // The summation can be rearranged to ensure no overflow given
+        // that the result of two unsigned 32-bit integers multiplied together
+        // plus two full 32-bit integers cannot overflow 64 bits:
+        // > long x = (1L << 32) - 1
+        // > x * x + x + x == -1 (all bits set, no overflow)
+        //
+        // The carry is a composed intermediate which will never overflow:
+        //
+        //             byby byby
+        // +           bxbx 0000
+        // +      ayay ayay 0000
+        //
+        // +      bxbx 0000 0000
+        // + axax axax 0000 0000
+
+        final long a = value1 >>> 32;
+        final long b = value1 & MASK32;
+        final long x = value2 >>> 32;
+        final long y = value2 & MASK32;
+
+        final long by = b * y;
+        final long bx = b * x;
+        final long ay = a * y;
+        final long ax = a * x;
+
+        // Cannot overflow
+        final long carry = (by >>> 32) +
+                           (bx & MASK32) +
+                            ay;
+        // Note:
+        // low = (carry << 32) | (by & INT_TO_UNSIGNED_BYTE_MASK)
+        // Benchmarking shows outputting low to a long[] output argument
+        // has no benefit over computing 'low = value1 * value2' separately.
+
+        return (bx >>> 32) + (carry >>> 32) + ax;
+    }
+
+    /**
+     * Multiply the arguments as if unsigned integers to a {@code double} result.
+     *
+     * @param a Value.
+     * @param b Value.
+     * @return the double
+     */
+    static double unsignedMultiplyToDoubleBigInteger(long a, long b) {
+        final long lo = a * b;
+
+        // Fast case: check the arguments cannot overflow a long.
+        // This is true if neither has the upper 33-bits set.
+        if (((a | b) >>> 31) == 0) {
+            // Implicit conversion to a double
+            return lo;
+        }
+
+        final long hi = unsignedMultiplyHigh(a, b);
+
+        // Convert to a double using BigInteger
+        return new BigInteger(1, ByteBuffer.allocate(Long.BYTES * 2)
+            .putLong(hi)
+            .putLong(lo)
+            .array()).doubleValue();
+    }
+
+    /**
+     * Multiply the arguments as if unsigned integers to a {@code double} result.
+     *
+     * @param x Value.
+     * @param y Value.
+     * @return the double
+     */
+    static double unsignedMultiplyToDouble(long x, long y) {
+        final long lo = x * y;
+        // Fast case: check the arguments cannot overflow a long.
+        // This is true if neither has the upper 33-bits set.
+        if (((x | y) >>> 31) == 0) {
+            // Implicit conversion to a double.
+            return lo;
+        }
+        return uin128ToDouble(unsignedMultiplyHigh(x, y), lo);
+    }
+
+    /**
+     * Convert an unsigned 128-bit integer to a {@code double}.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     * @return the double
+     */
+    static double uin128ToDouble(long hi, long lo) {
+        // Require the representation:
+        // 2^exp * mantissa / 2^53
+        // The mantissa has an implied leading 1-bit.
+
+        // We have the mantissa final bit as xxx0 or xxx1.
+        // To perform correct rounding we maintain the 54-th bit (a) and
+        // a check bit (b) of remaining bits.
+        // Cases:
+        // xxx0 00 - round-down              [1]
+        // xxx0 0b - round-down              [1]
+        // xxx0 a0 - half-even, round-down   [4]
+        // xxx0 ab - round-up                [2]
+        // xxx1 00 - round-down              [1]
+        // xxx1 0b - round-down              [1]
+        // xxx1 a0 - half-even, round-up     [3]
+        // xxx1 ab - round-up                [2]
+        // [1] If the 54-th bit is 0 always round-down.
+        // [2] Otherwise round-up if the check bit is set or
+        // [3] the final bit is odd (half-even rounding up).
+        // [4] half-even rounding down.
+
+        if (hi == 0) {
+            // If lo is a 63-bit result then we are done
+            if (lo >= 0) {
+                return lo;
+            }
+            // Create a 63-bit number with a sticky bit for rounding, rescale the result
+            return 2 * (double) ((lo >>> 1) | (lo & 0x1));
+        }
+
+        // Initially we create the most significant 64-bits.
+        final int shift = Long.numberOfLeadingZeros(hi);
+        // Shift the high bits and add trailing low bits.
+        // The mask is for the bits from low that are *not* used.
+        // Flipping the mask obtains the bits we concatenate
+        // after shifting (64 - shift).
+        final long maskLow = -1L >>> shift;
+        long bits64 = (hi << shift) | ((lo & ~maskLow) >>> -shift);
+        // exponent for 2^exp is the index of the highest bit in the 128 bit integer
+        final int exp = 127 - shift;
+        // Some of the low bits are lost. If non-zero set
+        // a sticky bit for rounding.
+        bits64 |= (lo & maskLow) == 0 ? 0 : 1;
+
+        // We have a 64-bit unsigned fraction magnitude and an exponent.
+        // This must be converted to a IEEE double by mapping the fraction to a base of 2^53.
+
+        // Create the 53-bit mantissa without the implicit 1-bit
+        long bits = (bits64 >>> 11) & MASK52;
+        // Extract 54-th bit and a sticky bit
+        final long a = (bits64 >>> 10) & 0x1;
+        final long b = (bits64 << 54) == 0 ? 0 : 1;
+        // Perform half-even rounding.
+        bits += a & (b | (bits & 0x1));
+        // Add the exponent.
+        // No worry about overflow to the sign bit as the max exponent is 127.
+        bits += (long) (exp + 1023) << 52;
+
+        return Double.longBitsToDouble(bits);
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMomentPerformance.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMomentPerformance.java
new file mode 100644
index 0000000..cbc4af6
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMomentPerformance.java
@@ -0,0 +1,1719 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.util.Arrays;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import java.util.function.DoubleConsumer;
+import java.util.function.DoubleSupplier;
+import java.util.function.IntConsumer;
+import java.util.function.LongConsumer;
+import java.util.function.Supplier;
+import java.util.function.ToDoubleFunction;
+import java.util.function.ToLongFunction;
+import java.util.stream.LongStream;
+import org.apache.commons.numbers.core.DD;
+import org.apache.commons.numbers.fraction.BigFraction;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.apache.commons.statistics.descriptive.DoubleStatistic;
+import org.apache.commons.statistics.descriptive.IntMean;
+import org.apache.commons.statistics.descriptive.IntStatistic;
+import org.apache.commons.statistics.descriptive.IntVariance;
+import org.apache.commons.statistics.descriptive.LongMean;
+import org.apache.commons.statistics.descriptive.LongStatistic;
+import org.apache.commons.statistics.descriptive.LongVariance;
+import org.apache.commons.statistics.descriptive.Mean;
+import org.apache.commons.statistics.descriptive.Variance;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Executes a benchmark of the moment-based statistics for integer values
+ * ({@code int} or {@code long}) compared to using {@code double} values.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@State(Scope.Benchmark)
+@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx512M"})
+public class IntMomentPerformance {
+    /** Commons Statistics Mean implementation. */
+    private static final String DOUBLE_MEAN = "DoubleMean";
+    /** Integer mean implementation. */
+    private static final String INT_MEAN = "IntMean";
+    /** Long mean implementation. */
+    private static final String LONG_MEAN = "LongMean";
+    /** Sum using a long mean implementation. */
+    private static final String LONG_SUM_MEAN = "LongSumMean";
+    /** Sum using a BigInteger mean implementation. */
+    private static final String BIG_INTEGER_SUM_MEAN = "BigIntegerSumMean";
+    /** JDK Stream mean implementation. */
+    private static final String STREAM_MEAN = "StreamMean";
+    /** Commons Statistics Variance implementation. */
+    private static final String DOUBLE_VAR = "DoubleVariance";
+    /** Integer variance implementation. */
+    private static final String INT_VAR = "IntVariance";
+    /** Long variance implementation. */
+    private static final String LONG_VAR = "LongVariance";
+    /** Long variance implementation using Math.multiplyHigh. */
+    private static final String LONG_VAR2 = "LongVariance2";
+
+    /**
+     * Source of array data.
+     */
+    @State(Scope.Benchmark)
+    public static class DataSource {
+        /** Data length. */
+        @Param({"2", "1000"})
+        private int length;
+
+        /** Data. */
+        private int[] data;
+
+        /** Data as a double. */
+        private double[] doubleData;
+
+        /** Data as a long. */
+        private long[] longData;
+
+        /**
+         * @return the data
+         */
+        public int[] getData() {
+            return data;
+        }
+
+        /**
+         * @return the data
+         */
+        public double[] getDoubleData() {
+            return doubleData;
+        }
+
+        /**
+         * @return the data
+         */
+        public long[] getLongData() {
+            return longData;
+        }
+
+        /**
+         * Create the data.
+         * Data will be randomized per iteration.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            longData = RandomSource.XO_RO_SHI_RO_128_PP.create().longs(length).toArray();
+            doubleData = Arrays.stream(longData).asDoubleStream().toArray();
+            data = Arrays.stream(longData).mapToInt(x -> (int) x).toArray();
+        }
+    }
+
+    /**
+     * Source of a {@link IntConsumer} action.
+     */
+    @State(Scope.Benchmark)
+    public static class IntActionSource {
+        /** Name of the source. */
+        @Param({DOUBLE_MEAN, INT_MEAN,
+                // Disabled: Run-time ~ IntMean
+                // LONG_SUM_MEAN
+                DOUBLE_VAR, INT_VAR})
+        private String name;
+
+        /** The action. */
+        private Supplier<IntStatistic> action;
+
+        /**
+         * @return the action
+         */
+        public IntStatistic getAction() {
+            return action.get();
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (DOUBLE_MEAN.equals(name)) {
+                action = () -> {
+                    final Mean m = Mean.create();
+                    return createIntStatistic(m, m);
+                };
+            } else if (INT_MEAN.equals(name)) {
+                action = () -> {
+                    final IntMean m = IntMean.create();
+                    return createIntStatistic(m, m);
+                };
+            } else if (LONG_SUM_MEAN.equals(name)) {
+                action = () -> {
+                    final LongSumMean m = new LongSumMean();
+                    return createIntStatistic(m, m);
+                };
+            } else if (DOUBLE_VAR.equals(name)) {
+                action = () -> {
+                    final Variance m = Variance.create();
+                    return createIntStatistic(m, m);
+                };
+            } else if (INT_VAR.equals(name)) {
+                action = () -> {
+                    final IntVariance m = IntVariance.create();
+                    return createIntStatistic(m, m);
+                };
+            } else {
+                throw new IllegalStateException("Unknown int action: " + name);
+            }
+        }
+
+        /**
+         * Creates the {@link IntStatistic}.
+         *
+         * @param c Consumer.
+         * @param s Supplier.
+         * @return the statistic
+         */
+        private static IntStatistic createIntStatistic(IntConsumer c, DoubleSupplier s) {
+            return new IntStatistic() {
+                @Override
+                public void accept(int value) {
+                    c.accept(value);
+                }
+                @Override
+                public double getAsDouble() {
+                    return s.getAsDouble();
+                }
+            };
+        }
+
+        /**
+         * Creates the {@link IntStatistic}.
+         *
+         * @param c Consumer.
+         * @param s Supplier.
+         * @return the statistic
+         */
+        private static IntStatistic createIntStatistic(DoubleConsumer c, DoubleSupplier s) {
+            return new IntStatistic() {
+                @Override
+                public void accept(int value) {
+                    c.accept(value);
+                }
+                @Override
+                public double getAsDouble() {
+                    return s.getAsDouble();
+                }
+            };
+        }
+    }
+
+    /**
+     * Source of a {@link DoubleConsumer} action.
+     */
+    @State(Scope.Benchmark)
+    public static class DoubleActionSource {
+        /** Name of the source. */
+        @Param({DOUBLE_MEAN, DOUBLE_VAR})
+        private String name;
+
+        /** The action. */
+        private Supplier<DoubleStatistic> action;
+
+        /**
+         * @return the action
+         */
+        public DoubleStatistic getAction() {
+            return action.get();
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (DOUBLE_MEAN.equals(name)) {
+                action = () -> {
+                    final Mean m = Mean.create();
+                    return createDoubleStatistic(m, m);
+                };
+            } else if (DOUBLE_VAR.equals(name)) {
+                action = () -> {
+                    final Variance m = Variance.create();
+                    return createDoubleStatistic(m, m);
+                };
+            } else {
+                throw new IllegalStateException("Unknown double action: " + name);
+            }
+        }
+
+        /**
+         * Creates the {@link DoubleStatistic}.
+         *
+         * <p>This method is here to provide parity when comparing actual instances
+         * of {@link DoubleStatistic} with composed objects for the equivalent
+         * int/long statistics.
+         *
+         * @param c Consumer.
+         * @param s Supplier.
+         * @return the statistic
+         */
+        private static DoubleStatistic createDoubleStatistic(DoubleConsumer c, DoubleSupplier s) {
+            return new DoubleStatistic() {
+                @Override
+                public void accept(double value) {
+                    c.accept(value);
+                }
+                @Override
+                public double getAsDouble() {
+                    return s.getAsDouble();
+                }
+            };
+        }
+    }
+
+    /**
+     * Source of a {@link LongConsumer} action.
+     */
+    @State(Scope.Benchmark)
+    public static class LongActionSource {
+        /** Name of the source. */
+        @Param({DOUBLE_MEAN, LONG_MEAN, BIG_INTEGER_SUM_MEAN,
+                DOUBLE_VAR, LONG_VAR, LONG_VAR2})
+        private String name;
+
+        /** The action. */
+        private Supplier<LongStatistic> action;
+
+        /**
+         * @return the action
+         */
+        public LongStatistic getAction() {
+            return action.get();
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (DOUBLE_MEAN.equals(name)) {
+                action = () -> {
+                    final Mean m = Mean.create();
+                    return createLongStatistic(m, m);
+                };
+            } else if (LONG_MEAN.equals(name)) {
+                action = () -> {
+                    final LongMean m = LongMean.create();
+                    return createLongStatistic(m, m);
+                };
+            } else if (BIG_INTEGER_SUM_MEAN.equals(name)) {
+                action = () -> {
+                    final BigIntegerSumMean m = new BigIntegerSumMean();
+                    return createLongStatistic(m, m);
+                };
+            } else if (DOUBLE_VAR.equals(name)) {
+                action = () -> {
+                    final Variance m = Variance.create();
+                    return createLongStatistic(m, m);
+                };
+            } else if (LONG_VAR.equals(name)) {
+                action = () -> {
+                    final LongVariance m = LongVariance.create();
+                    return createLongStatistic(m, m);
+                };
+            } else if (LONG_VAR2.equals(name)) {
+                action = () -> {
+                    final LongVariance2 m = LongVariance2.create();
+                    return createLongStatistic(m, m);
+                };
+            } else {
+                throw new IllegalStateException("Unknown long action: " + name);
+            }
+        }
+
+        /**
+         * Creates the {@link LongStatistic}.
+         *
+         * @param c Consumer.
+         * @param s Supplier.
+         * @return the statistic
+         */
+        private static LongStatistic createLongStatistic(LongConsumer c, DoubleSupplier s) {
+            return new LongStatistic() {
+                @Override
+                public void accept(long value) {
+                    c.accept(value);
+                }
+                @Override
+                public double getAsDouble() {
+                    return s.getAsDouble();
+                }
+            };
+        }
+
+        /**
+         * Creates the {@link LongStatistic}.
+         *
+         * @param c Consumer.
+         * @param s Supplier.
+         * @return the statistic
+         */
+        private static LongStatistic createLongStatistic(DoubleConsumer c, DoubleSupplier s) {
+            return new LongStatistic() {
+                @Override
+                public void accept(long value) {
+                    c.accept(value);
+                }
+                @Override
+                public double getAsDouble() {
+                    return s.getAsDouble();
+                }
+            };
+        }
+    }
+
+    /**
+     * Source of a {@link ToDoubleFunction} for a {@code int[]}.
+     */
+    @State(Scope.Benchmark)
+    public static class IntFunctionSource {
+        /** Name of the source. */
+        @Param({INT_MEAN,
+            // Disabled: Run-time ~ IntMean
+            //LONG_SUM_MEAN,
+            STREAM_MEAN, INT_VAR})
+        private String name;
+
+        /** The action. */
+        private ToDoubleFunction<int[]> function;
+
+        /**
+         * @return the function
+         */
+        public ToDoubleFunction<int[]> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (INT_MEAN.equals(name)) {
+                function = x -> IntMean.of(x).getAsDouble();
+            } else if (LONG_SUM_MEAN.equals(name)) {
+                function = LongSumMean::mean;
+            } else if (STREAM_MEAN.equals(name)) {
+                function = x -> Arrays.stream(x).average().orElse(Double.NaN);
+            } else if (INT_VAR.equals(name)) {
+                function = x -> IntVariance.of(x).getAsDouble();
+            } else {
+                throw new IllegalStateException("Unknown int function: " + name);
+            }
+        }
+    }
+
+    /**
+     * Source of a {@link ToDoubleFunction} for a {@code double[]}.
+     */
+    @State(Scope.Benchmark)
+    public static class DoubleFunctionSource {
+        /** Name of the source. */
+        @Param({DOUBLE_MEAN, DOUBLE_VAR})
+        private String name;
+
+        /** The action. */
+        private ToDoubleFunction<double[]> function;
+
+        /**
+         * @return the function
+         */
+        public ToDoubleFunction<double[]> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (DOUBLE_MEAN.equals(name)) {
+                function = x -> Mean.of(x).getAsDouble();
+            } else if (DOUBLE_VAR.equals(name)) {
+                function = x -> Variance.of(x).getAsDouble();
+            } else {
+                throw new IllegalStateException("Unknown double function: " + name);
+            }
+        }
+    }
+
+    /**
+     * Source of a {@link ToDoubleFunction} for a {@code long[]}.
+     */
+    @State(Scope.Benchmark)
+    public static class LongFunctionSource {
+        /** Name of the source. */
+        @Param({LONG_MEAN, BIG_INTEGER_SUM_MEAN, LONG_VAR, LONG_VAR2})
+        private String name;
+
+        /** The action. */
+        private ToDoubleFunction<long[]> function;
+
+        /**
+         * @return the function
+         */
+        public ToDoubleFunction<long[]> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (LONG_MEAN.equals(name)) {
+                function = x -> LongMean.of(x).getAsDouble();
+            } else if (BIG_INTEGER_SUM_MEAN.equals(name)) {
+                function = BigIntegerSumMean::mean;
+            } else if (LONG_VAR.equals(name)) {
+                function = x -> LongVariance.of(x).getAsDouble();
+            } else if (LONG_VAR2.equals(name)) {
+                function = x -> LongVariance2.of(x).getAsDouble();
+            } else {
+                throw new IllegalStateException("Unknown long function: " + name);
+            }
+        }
+    }
+
+    /**
+     * Class containing the variance data.
+     */
+    static class IntVarianceData {
+        /** Sum of the squared values. */
+        private final UInt128 sumSq;
+        /** Sum of the values. */
+        private final Int128 sum;
+        /** Count of values that have been added. */
+        private long n;
+
+        /**
+         * @param sumSq Sum of the squared values.
+         * @param sum Sum of the values.
+         * @param n Count of values that have been added.
+         */
+        IntVarianceData(UInt128 sumSq, Int128 sum, long n) {
+            this.sumSq = sumSq;
+            this.sum = sum;
+            this.n = n;
+        }
+
+        /**
+         * @return the sum of the squared values
+         */
+        UInt128 getSumSq() {
+            return new UInt128(sumSq.hi64(), sumSq.lo64());
+        }
+
+        /**
+         * @return the sum
+         */
+        Int128 getSum() {
+            return new Int128(sum.hi64(), sum.lo64());
+        }
+
+        /**
+         * @return the count of values that have been added
+         */
+        long getN() {
+            return n;
+        }
+
+        /**
+         * @return the copy
+         */
+        IntVarianceData copy() {
+            return new IntVarianceData(getSumSq(), getSum(), n);
+        }
+
+        /**
+         * Adds the other instance.
+         *
+         * @param other the other
+         * @return this instance
+         */
+        IntVarianceData add(IntVarianceData other) {
+            // Prevent the data from becoming too large
+            n = Math.addExact(n, other.n);
+            sumSq.add(other.sumSq);
+            sum.add(other.sum);
+            return this;
+        }
+    }
+
+    /**
+     * Source of {@code int} variance data.
+     *
+     * <p>This class generates a pool of variance data from a random sample of integers
+     * in a range. The pool objects are then combined with each other for a given number of
+     * rounds, effectively doubling the size of pool objects each round.
+     * Using the defaults will create objects in the pool of:
+     * <pre>
+     * E[ sum(x) ] = (511 / 2) mean value * (95 / 2) mean samples ~ 12136.25 ~ 2^8 * 2^5.5 {@code < 2^14}
+     * E[ sum(x^2) = ((511 / 2)^2 mean value^2) * (95 / 2) mean samples ~ 3100811.875 ~ 2^16 * 2^5.5 {@code < 2^22}
+     * Max[ sum(x) = 511 * 63 = 32193 {@code < 2^15}
+     * Max[ sum(x^2) = 15^2 * 63 = 14175 {@code < 2^16}
+     * man[ n ] = 63 {@code < 2^6}
+     * </pre>
+     * <p>The objects from this pool can be added together a maximum of 56 times before n overflows.
+     * The sum of the values will overflow a long at approximately 18 combines.
+     */
+    @State(Scope.Benchmark)
+    public static class IntVarianceDataSource {
+        /** Consistent seed. */
+        private static final Long SEED = ThreadLocalRandom.current().nextLong();
+        /** Lower limit. */
+        @Param({"0"})
+        private int origin;
+        /** Upper limit. */
+        @Param({"512"})
+        private int bound;
+        /** Minimum samples. */
+        @Param({"32"})
+        private int minSamples;
+        /** Maximum samples. */
+        @Param({"64"})
+        private int maxSamples;
+        /** Pool size. */
+        @Param({"64"})
+        private int poolSize;
+        /** Number of combine operations. */
+        @Param({"8", "16", "24", "32", "48"})
+        private int combine;
+
+        /** Data. */
+        private IntVarianceData[] data;
+
+        /**
+         * The number of data values.
+         *
+         * @return the size
+         */
+        public int size() {
+            return data.length;
+        }
+        /**
+         * Get a copy of the data for the specified index.
+         *
+         * @param i Index.
+         * @return the data
+         */
+        public IntVarianceData getData(int i) {
+            return data[i].copy();
+        }
+
+        /**
+         * Create the data.
+         */
+        @Setup
+        public void setup() {
+            // Consistent seed so the same data is provided to all methods
+            final UniformRandomProvider rng = RandomSource.XO_SHI_RO_512_SS.create(SEED);
+            // Initial pool
+            final IntVarianceData[] pool = new IntVarianceData[poolSize];
+            for (int i = 0; i < pool.length; i++) {
+                final int n = rng.nextInt(minSamples, maxSamples);
+                final UInt128 sumSq = UInt128.create();
+                final Int128 sum = Int128.create();
+                rng.ints(n, origin, bound).forEach(x -> {
+                    sumSq.addPositive((long) x * x);
+                    sum.add(x);
+                });
+                pool[i] = new IntVarianceData(sumSq, sum, n);
+            }
+            // Combine to grow the average size of the pool objects
+            for (int round = 0; round < combine; round++) {
+                final IntVarianceData[] last = pool.clone();
+                for (int i = 0; i < pool.length; i++) {
+                    // Copy the instance that will be the LHS of the add operation
+                    pool[i] = last[i].copy().add(last[rng.nextInt(poolSize)]);
+                }
+            }
+            data = pool;
+        }
+    }
+
+    /**
+     * Source of a {@link ToDoubleFunction} for a {@code IntVarianceData}.
+     */
+    @State(Scope.Benchmark)
+    public static class IntVarianceFunctionSource {
+        /** {@link MathContext} with 20 digits of precision. */
+        private static final MathContext MC_20_DIGITS = new MathContext(20);
+
+        /** Name of the source. */
+        @Param({"DD", "DD2", "BigIntegerPow", "BigIntegerMultiply",
+            "SumSquareBigInteger", "SumSquareMultiplyBigInteger",
+            "UIntBigInteger", "UIntDD", "UIntDD2", "UIntBigInteger2", "UIntBigInteger3",
+            "UIntDouble",
+            // Very slow
+            //"UIntBigFraction", "UIntBigDecimal"
+        })
+        private String name;
+
+        /** The action. */
+        private ToDoubleFunction<IntVarianceData> function;
+
+        /**
+         * @return the function
+         */
+        public ToDoubleFunction<IntVarianceData> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if ("DD".equals(name)) {
+                function = IntVarianceFunctionSource::varianceDD;
+            } else if ("DD2".equals(name)) {
+                function = IntVarianceFunctionSource::varianceDD2;
+            } else if ("BigIntegerPow".equals(name)) {
+                function = IntVarianceFunctionSource::varianceBigIntegerPow;
+            } else if ("BigIntegerMultiply".equals(name)) {
+                function = IntVarianceFunctionSource::varianceBigIntegerMultiply;
+            } else if ("SumSquareBigInteger".equals(name)) {
+                function = IntVarianceFunctionSource::varianceSumSquareBigInteger;
+            } else if ("SumSquareMultiplyBigInteger".equals(name)) {
+                function = IntVarianceFunctionSource::varianceSumSquareMultiplyIntBigInteger;
+            } else if ("UIntBigInteger".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntBigInteger;
+            } else if ("UIntDD".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntDD;
+            } else if ("UIntDD2".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntDD2;
+            } else if ("UIntBigInteger2".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntBigInteger2;
+            } else if ("UIntBigInteger3".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntBigInteger3;
+            } else if ("UIntDouble".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntDouble;
+            } else if ("UIntBigFraction".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntBigFraction;
+            } else if ("UIntBigDecimal".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntBigDecimal;
+            } else {
+                throw new IllegalStateException("Unknown int variance function: " + name);
+            }
+        }
+
+        /**
+         * Convenience method to square a BigInteger.
+         *
+         * @param x Value
+         * @return x^2
+         */
+        private static BigInteger square(BigInteger x) {
+            return x.multiply(x);
+        }
+
+        /**
+         * Compute the variance using double-double arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceDD(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            final DD diff = sumSq.toDD().multiply(n).subtract(sum.toDD().square());
+            if (diff.hi() < 0) {
+                return 0;
+            }
+            // Divisor is an exact double
+            if (n < (1L << 26)) {
+                // n0*n is safe as a long
+                return diff.divide(n0 * n).doubleValue();
+            }
+            return diff.divide(DD.of(n).multiply(DD.of(n0))).doubleValue();
+        }
+
+        /**
+         * Compute the variance using double-double arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceDD2(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations: sum(x^2) - sum(x)^2 / n
+            final DD ss = sumSq.toDD().subtract(sum.toDD().square().divide(n));
+            if (ss.hi() < 0) {
+                return 0;
+            }
+            return ss.divide(n0).doubleValue();
+        }
+
+        /**
+         * Compute the variance using BigInteger arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceBigIntegerPow(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            final BigInteger diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                .subtract(sum.toBigInteger().pow(2));
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using BigInteger arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceBigIntegerMultiply(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            final BigInteger diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                .subtract(square(sum.toBigInteger()));
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using Int128 and BigInteger arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceSumSquareBigInteger(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the second term if possible using fast integer arithmetic.
+            final BigInteger term1 = sumSq.toBigInteger().multiply(BigInteger.valueOf(n));
+            final BigInteger term2 = sum.hi64() == 0 ? sum.squareLow().toBigInteger() : square(sum.toBigInteger());
+            final BigInteger diff = term1.subtract(term2);
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using UInt128/Int128 and BigInteger arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceSumSquareMultiplyIntBigInteger(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // sum(x^2) * n will be OK when n < 2^32.
+            final BigInteger term1 = n < 1L << 32 ? sumSq.unsignedMultiply((int) n).toBigInteger() :
+                sumSq.toBigInteger().multiply(BigInteger.valueOf(n));
+            final BigInteger term2 = sum.hi64() == 0 ? sum.squareLow().toBigInteger() : square(sum.toBigInteger());
+            final BigInteger diff = term1.subtract(term2);
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using UInt128/Int128 and BigInteger arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntBigInteger(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            BigInteger diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toBigInteger();
+            } else {
+                // It may still be possible to compute the square
+                BigInteger sum2;
+                if (sum.hi64() == 0) {
+                    sum2 = sum.squareLow().toBigInteger();
+                } else {
+                    sum2 = sum.toBigInteger();
+                    sum2 = sum2.multiply(sum2);
+                }
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(sum2);
+            }
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using UInt128/Int128 and DD arithmetic.
+         * The final divide uses double precision.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntDD(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // Both are safe when n < 2^32.
+            if ((n >>> Integer.SIZE) == 0) {
+                DD diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDD();
+                // Divisor is an exact double
+                if (n < (1L << 26)) {
+                    // n0*n is safe as a long
+                    return diff.divide(n0 * n).doubleValue();
+                }
+                return diff.divide(DD.of(n).multiply(DD.of(n0))).doubleValue();
+            }
+            BigInteger diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(square(sum.toBigInteger()));
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using UInt128/Int128 and DD arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntDD2(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // Both are safe when n < 2^32.
+            if ((n >>> Integer.SIZE) == 0) {
+                DD diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDD();
+                // Divisor is an exact double
+                if (n < (1L << 26)) {
+                    // n0*n is safe as a long
+                    return diff.divide(n0 * n).doubleValue();
+                }
+                return diff.divide(DD.of(n).multiply(DD.of(n0))).doubleValue();
+            }
+            BigInteger diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(square(sum.toBigInteger()));
+            // Assume n is big to overflow the sum(x)
+            // Compute the divide in double-double precision
+            return DD.of(diff.doubleValue()).divide(DD.of(n).multiply(DD.of(n0))).doubleValue();
+        }
+
+        /**
+         * Compute the variance using unsigned integer (UInt128/Int128 or BigInteger) arithmetic.
+         * The final divide uses double precision.
+         *
+         * <p>Note: This is similar to {@link #varianceUIntBigInteger(IntVarianceData)} but does
+         * not fast compute the squared sum. This benchmarks as faster: the BigInteger multiply
+         * on small values for sum(x)^2 is efficient.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntBigInteger2(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            BigInteger diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toBigInteger();
+            } else {
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(square(sum.toBigInteger()));
+            }
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using unsigned integer (UInt128/Int128 or BigInteger) arithmetic.
+         * The final divide uses double precision.
+         *
+         * <p>Note: This is similar to {@link #varianceUIntBigInteger(IntVarianceData)} but does
+         * computes the squared sum in Int128. This benchmarks slower than converting to BigInteger
+         * and computing the square.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntBigInteger3(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            BigInteger diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toBigInteger();
+            } else {
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(sum.square());
+            }
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using unsigned integer (UInt128/Int128 or BigInteger) arithmetic.
+         * The final divide uses double precision.
+         *
+         * <p>Note: This is similar to {@link #varianceUIntBigInteger(IntVarianceData)} but does
+         * not fast compute the squared sum. This benchmarks as faster: the BigInteger multiply
+         * on small values for sum(x)^2 is efficient.
+         *
+         * <p>This method uses the {@link UInt128#toDouble()} to avoid going via BigInteger.
+         * The divisor is computed in extended precision.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntDouble(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            double diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDouble();
+            } else {
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                    .subtract(square(sum.toBigInteger())).doubleValue();
+            }
+            // Compute the divide in double precision
+            return diff / IntMath.unsignedMultiplyToDouble(n, n0);
+        }
+
+        /**
+         * Compute the variance using unsigned integer (UInt128/Int128 or BigInteger) arithmetic.
+         * The final divide uses double precision.
+         *
+         * <p>Note: This is similar to {@link #varianceUIntBigInteger(IntVarianceData)} but does
+         * not fast compute the squared sum. This benchmarks as faster: the BigInteger multiply
+         * on small values for sum(x)^2 is efficient.
+         *
+         * <p>The final divide uses BigFraction for large size, or double.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntBigFraction(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            BigInteger diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toBigInteger();
+            } else {
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(square(sum.toBigInteger()));
+            }
+            if (n < (1L << 26)) {
+                // Compute the divide in double precision
+                return diff.doubleValue() / ((double) n0 * n);
+            }
+            return BigFraction.of(diff, BigInteger.valueOf(n0).multiply(BigInteger.valueOf(n)))
+                .doubleValue();
+        }
+
+        /**
+         * Compute the variance using unsigned integer (UInt128/Int128 or BigInteger) arithmetic.
+         * The final divide uses double precision.
+         *
+         * <p>Note: This is similar to {@link #varianceUIntBigInteger(IntVarianceData)} but does
+         * not fast compute the squared sum. This benchmarks as faster: the BigInteger multiply
+         * on small values for sum(x)^2 is efficient.
+         *
+         * <p>The final divide uses BigDecimal for large size, or double.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntBigDecimal(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            BigInteger diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toBigInteger();
+            } else {
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(square(sum.toBigInteger()));
+            }
+            if (n < (1L << 26)) {
+                // Compute the divide in double precision
+                return diff.doubleValue() / ((double) n0 * n);
+            }
+            return new BigDecimal(diff).divide(new BigDecimal(
+                BigInteger.valueOf(n0).multiply(BigInteger.valueOf(n))), MC_20_DIGITS)
+                .doubleValue();
+        }
+    }
+
+    /**
+     * Source of {@code long} array data.
+     * The data is designed to overflow a sum as a long with a specified frequency.
+     * There are 3 cases: positive values; negative values; any sign. The amount
+     * of overflow is controlled using a shift to remove magnitude. No shift expects
+     * overflow 50% of the time when summing same sign values. If both signs are used then the
+     * random walk will be based around 0 with overflow occurring proportional to
+     * the magnitude. Chance of overflow will rapidly drop when the values are not full
+     * magnitude numbers.
+     */
+    @State(Scope.Benchmark)
+    public static class LongDataSource {
+        /** Data length: 2^10. If shift is above 10 then no overflow will occur. */
+        @Param({"1024"})
+        private int length;
+        /** Data sign. */
+        @Param({"positive", "negative", "both"})
+        private String sign;
+        /** Data bit shift. */
+        @Param({"0", "1", "2", "4", "8", "16"})
+        private int shift;
+
+        /** Data. */
+        private long[] data;
+
+        /**
+         * @return the data
+         */
+        public long[] getData() {
+            return data;
+        }
+
+        /**
+         * Create the data.
+         * Data will be randomized per iteration.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            LongStream s = RandomSource.XO_RO_SHI_RO_128_PP.create().longs(length);
+            if ("positive".equals(sign)) {
+                s = s.map(x -> x >>> 1);
+            } else if ("negative".equals(sign)) {
+                s = s.map(x -> x | Long.MIN_VALUE);
+            } else if (!"both".equals(sign)) {
+                throw new IllegalStateException("Unknown sign: " + sign);
+            }
+            if (shift > 0) {
+                final int bits = shift;
+                // Signed shift maintains negative values
+                s = s.map(x -> x >> bits);
+            }
+            data = s.toArray();
+        }
+    }
+
+    /**
+     * Source of a {@link ToLongFunction} for a {@code long[]}.
+     */
+    @State(Scope.Benchmark)
+    public static class LongSumFunctionSource {
+        /** Name of the source.
+         * The branchless 128bitAdd2 runs at constant speed but is slower than 128bitAdd. */
+        @Param({"128bitAdd", "128bitAdd2", "64bitSum"})
+        private String name;
+
+        /** The action. */
+        private ToLongFunction<long[]> function;
+
+        /**
+         * @return the function
+         */
+        public ToLongFunction<long[]> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if ("128bitAdd".equals(name)) {
+                function = x -> {
+                    final Int128 s = Int128.create();
+                    for (long y : x) {
+                        s.add(y);
+                    }
+                    return s.hi64();
+                };
+            } else if ("128bitAdd2".equals(name)) {
+                function = x -> {
+                    final Int128 s = Int128.create();
+                    for (long y : x) {
+                        s.add2(y);
+                    }
+                    return s.hi64();
+                };
+            } else if ("64bitSum".equals(name)) {
+                function = x -> {
+                    long s = 0;
+                    for (long y : x) {
+                        s += y;
+                    }
+                    return s;
+                };
+            } else {
+                throw new IllegalStateException("Unknown long sum function: " + name);
+            }
+        }
+    }
+
+    /**
+     * Source of {@code long} array data to multiply as unsigned pairs.
+     * Magnitude is approximately controlled using a bit shift on the values.
+     */
+    @State(Scope.Benchmark)
+    public static class MultiplyLongDataSource {
+        /** Data length. */
+        @Param({"1024"})
+        private int length;
+        /** Data bit shift. */
+        @Param({"0", "33"})
+        private int shift;
+
+        /** Data. */
+        private long[] data;
+
+        /**
+         * @return the data
+         */
+        public long[] getData() {
+            return data;
+        }
+
+        /**
+         * Create the data.
+         * Data will be randomized per iteration.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            LongStream s = RandomSource.XO_RO_SHI_RO_128_PP.create().longs(length * 2L);
+            if (shift > 0) {
+                final int bits = shift;
+                s = s.map(x -> x >>> bits);
+            }
+            data = s.toArray();
+        }
+    }
+
+    /**
+     * Source of a {@link ToDoubleFunction} for a {@code long[]}.
+     */
+    @State(Scope.Benchmark)
+    public static class MultiplyLongFunctionSource {
+        /** Name of the source. */
+        @Param({"double", "unsignedMultiplyToDoubleBigInteger", "unsignedMultiplyToDouble"})
+        private String name;
+
+        /** The action. */
+        private ToDoubleFunction<long[]> function;
+
+        /**
+         * Function for two long arguments.
+         */
+        interface LongLongToDoubleFunction {
+            /**
+             * Apply the function.
+             *
+             * @param a Value.
+             * @param b Value.
+             * @return the result
+             */
+            double apply(long a, long b);
+        }
+
+        /**
+         * @return the function
+         */
+        public ToDoubleFunction<long[]> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            final LongLongToDoubleFunction f = createFunction(name);
+            function = x -> applyAll(x, f);
+        }
+
+        /**
+         * Creates the function.
+         *
+         * @param functionName Function name.
+         * @return the function
+         */
+        private LongLongToDoubleFunction createFunction(String functionName) {
+            if ("double".equals(functionName)) {
+                return (x, y) -> (double) x * y;
+            } else if ("unsignedMultiplyToDoubleBigInteger".equals(name)) {
+                return IntMath::unsignedMultiplyToDoubleBigInteger;
+            } else if ("unsignedMultiplyToDouble".equals(name)) {
+                return IntMath::unsignedMultiplyToDouble;
+            } else {
+                throw new IllegalStateException("Unknown multiply long function: " + name);
+            }
+        }
+
+        /**
+         * Apply the function to all pairs in the data.
+         *
+         * @param array Data.
+         * @param f Function.
+         * @return the result
+         */
+        private static double applyAll(long[] array, LongLongToDoubleFunction f) {
+            double s = 0;
+            for (int i = 0; i < array.length; i += 2) {
+                s += f.apply(array[i], array[i + 1]);
+            }
+            return s;
+        }
+    }
+
+    /**
+     * A mean of {@code int} data using a {@code long} sum.
+     */
+    static class LongSumMean implements IntConsumer, DoubleSupplier {
+        /** Count of values that have been added. */
+        private long n;
+
+        /** Sum of values that have been added. */
+        private long s;
+
+        @Override
+        public void accept(int value) {
+            s += value;
+            n++;
+        }
+
+        @Override
+        public double getAsDouble() {
+            return (double) s / n;
+        }
+
+        /**
+         * Compute the mean using a sum.
+         *
+         * @param data Data.
+         * @return the mean
+         */
+        static double mean(int[] data) {
+            long s = 0;
+            for (final int x : data) {
+                s += x;
+            }
+            return (double) s / data.length;
+        }
+    }
+
+    /**
+     * A mean of {@code long} data using a {@code BigInteger} sum.
+     */
+    static class BigIntegerSumMean implements LongConsumer, DoubleSupplier {
+        /** Count of values that have been added. */
+        private long n;
+
+        /** Sum of values that have been added. */
+        private BigInteger s = BigInteger.ZERO;
+
+        @Override
+        public void accept(long value) {
+            s = s.add(BigInteger.valueOf(value));
+            n++;
+        }
+
+        @Override
+        public double getAsDouble() {
+            return s.doubleValue() / n;
+        }
+
+        /**
+         * Compute the mean using a sum.
+         *
+         * @param data Data.
+         * @return the mean
+         */
+        static double mean(long[] data) {
+            BigInteger s = BigInteger.ZERO;
+            for (final long x : data) {
+                s = s.add(BigInteger.valueOf(x));
+            }
+            return s.doubleValue() / data.length;
+        }
+    }
+
+    /**
+     * Apply the action to each {@code int} value.
+     *
+     * @param <T> the action type
+     * @param action Action.
+     * @param values Values.
+     * @return the value
+     */
+    static <T extends IntConsumer & DoubleSupplier> double forEach(T action, int[] values) {
+        for (final int x : values) {
+            action.accept(x);
+        }
+        return action.getAsDouble();
+    }
+
+    /**
+     * Apply the action to each {@code double} value.
+     *
+     * @param <T> the action type
+     * @param action Action.
+     * @param values Values.
+     * @return the value
+     */
+    static <T extends DoubleConsumer & DoubleSupplier> double forEach(T action, double[] values) {
+        for (final double x : values) {
+            action.accept(x);
+        }
+        return action.getAsDouble();
+    }
+
+    /**
+     * Apply the action to each {@code long} value.
+     *
+     * @param <T> the action type
+     * @param action Action.
+     * @param values Values.
+     * @return the value
+     */
+    static <T extends LongConsumer & DoubleSupplier> double forEach(T action, long[] values) {
+        for (final long x : values) {
+            action.accept(x);
+        }
+        return action.getAsDouble();
+    }
+
+    /**
+     * Create the statistic using a consumer of {@code int} values.
+     *
+     * @param action Source of the data action.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double forEachIntStatistic(IntActionSource action, DataSource source) {
+        return forEach(action.getAction(), source.getData());
+    }
+
+    /**
+     * Create the statistic using a consumer of {@code double} values.
+     *
+     * @param action Source of the data action.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double forEachDoubleStatistic(DoubleActionSource action, DataSource source) {
+        return forEach(action.getAction(), source.getDoubleData());
+    }
+
+    /**
+     * Create the statistic using a consumer of {@code long} values.
+     *
+     * @param action Source of the data action.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double forEachLongStatistic(LongActionSource action, DataSource source) {
+        return forEach(action.getAction(), source.getLongData());
+    }
+
+    /**
+     * Create the statistic using a {@code int[]} function.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double arrayIntStatistic(IntFunctionSource function, DataSource source) {
+        return function.getFunction().applyAsDouble(source.getData());
+    }
+
+    /**
+     * Create the statistic using a {@code double[]} function.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double arrayDoubleStatistic(DoubleFunctionSource function, DataSource source) {
+        return function.getFunction().applyAsDouble(source.getDoubleData());
+    }
+
+    /**
+     * Create the statistic using a {@code long[]} function.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double arrayLongStatistic(LongFunctionSource function, DataSource source) {
+        return function.getFunction().applyAsDouble(source.getLongData());
+    }
+
+    /**
+     * Create the variance using a aggregated {@code int[]} data.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @param bh Data sink.
+     */
+    @Benchmark
+    public void intVariance(IntVarianceFunctionSource function, IntVarianceDataSource source, Blackhole bh) {
+        final int size = source.size();
+        final ToDoubleFunction<IntVarianceData> f = function.getFunction();
+        for (int i = 0; i < size; i++) {
+            bh.consume(f.applyAsDouble(source.getData(i)));
+        }
+    }
+
+    /**
+     * Create the sum using a {@code long[]} function.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @return the sum
+     */
+    @Benchmark
+    public long longSum(LongSumFunctionSource function, LongDataSource source) {
+        return function.getFunction().applyAsLong(source.getData());
+    }
+
+    /**
+     * Create the product using a {@code long[]} function.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @return the sum
+     */
+    @Benchmark
+    public double multiplyToDouble(MultiplyLongFunctionSource function, MultiplyLongDataSource source) {
+        return function.getFunction().applyAsDouble(source.getData());
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVariance2.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVariance2.java
new file mode 100644
index 0000000..451d569
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVariance2.java
@@ -0,0 +1,196 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.util.function.DoubleSupplier;
+import java.util.function.LongConsumer;
+
+/**
+ * Computes the variance of the available values.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.LongVariance} to allow benchmarking.
+ * This uses {@code java.lang.Math.multiplyHigh(long, long)} and requires Java 11+.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/variance">variance (Wikipedia)</a>
+ * @since 1.1
+ */
+final class LongVariance2 implements LongConsumer, DoubleSupplier {
+
+    /** Sum of the squared values. */
+    private final UInt192 sumSq;
+    /** Sum of the values. */
+    private final Int128 sum;
+    /** Count of values that have been added. */
+    private long n;
+
+    /** Flag to control if the statistic is biased, or should use a bias correction. */
+    private boolean biased;
+
+    /**
+     * Create an instance.
+     */
+    private LongVariance2() {
+        this(UInt192.create(), Int128.create(), 0);
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param sumSq Sum of the squared values.
+     * @param sum Sum of the values.
+     * @param n Count of values that have been added.
+     */
+    private LongVariance2(UInt192 sumSq, Int128 sum, int n) {
+        this.sumSq = sumSq;
+        this.sum = sum;
+        this.n = n;
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * <p>The initial result is {@code NaN}.
+     *
+     * @return {@code IntVariance} instance.
+     */
+    public static LongVariance2 create() {
+        return new LongVariance2();
+    }
+
+    /**
+     * Returns an instance populated using the input {@code values}.
+     *
+     * @param values Values.
+     * @return {@code IntVariance} instance.
+     */
+    public static LongVariance2 of(long... values) {
+        // Note: Arrays could be processed using specialised counts knowing the maximum
+        // limit
+        // for an array is 2^31 values. Requires a UInt160.
+
+        final Int128 s = Int128.create();
+        final UInt192 ss = UInt192.create();
+        for (final long x : values) {
+            s.add(x);
+            ss.addSquare2(x);
+        }
+        return new LongVariance2(ss, s, values.length);
+    }
+
+    /**
+     * Updates the state of the statistic to reflect the addition of {@code value}.
+     *
+     * @param value Value.
+     */
+    @Override
+    public void accept(long value) {
+        sumSq.addSquare2(value);
+        sum.add(value);
+        n++;
+    }
+
+    /**
+     * Gets the variance of all input values.
+     *
+     * <p>When no values have been added, the result is {@code NaN}.
+     *
+     * @return variance of all values.
+     */
+    @Override
+    public double getAsDouble() {
+        if (n == 0) {
+            return Double.NaN;
+        }
+        // Avoid a divide by zero
+        if (n == 1) {
+            return 0;
+        }
+        final long n0 = biased ? n : n - 1;
+
+        // Sum-of-squared deviations: sum(x^2) - sum(x)^2 / n
+        // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+        // The precursor is computed in integer precision.
+        // The divide uses double precision.
+        // This ensures we avoid cancellation in the difference and use a fast divide.
+        // The result is limited to max 4 ulp by the rounding in the double computation
+        // When n0*n is < 2^53 the max error is reduced to two roundings.
+
+        // Compute the term if possible using fast integer arithmetic.
+        // 192-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+        // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+        // The first is safe when n < 2^32 but we must check the sum high bits.
+        double diff;
+        if (((n >>> Integer.SIZE) | sum.hi64()) == 0) {
+            diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDouble();
+        } else {
+            diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                .subtract(square(sum.toBigInteger())).doubleValue();
+        }
+        // Compute the divide in double precision
+        return diff / IntMath.unsignedMultiplyToDouble(n, n0);
+    }
+
+    /**
+     * Convenience method to square a BigInteger.
+     *
+     * @param x Value
+     * @return x^2
+     */
+    private static BigInteger square(BigInteger x) {
+        return x.multiply(x);
+    }
+
+    /**
+     * Combine with the {@code other} instance.
+     *
+     * @param other Other instance.
+     * @return this instance
+     */
+    public LongVariance2 combine(LongVariance2 other) {
+        sumSq.add(other.sumSq);
+        sum.add(other.sum);
+        n += other.n;
+        return this;
+    }
+
+    /**
+     * Sets the value of the biased flag. The default value is {@code false}.
+     *
+     * <p>If {@code false} the sum of squared deviations from the sample mean is
+     * normalised by {@code n - 1} where {@code n} is the number of samples. This is
+     * Bessel's correction for an unbiased estimator of the variance of a hypothetical
+     * infinite population.
+     *
+     * <p>If {@code true} the sum of squared deviations is normalised by the number of
+     * samples {@code n}.
+     *
+     * <p>Note: This option only applies when {@code n > 1}. The variance of {@code n = 1}
+     * is always 0.
+     *
+     * <p>This flag only controls the final computation of the statistic. The value of
+     * this flag will not affect compatibility between instances during a
+     * {@link #combine(LongVariance2) combine} operation.
+     *
+     * @param v Value.
+     * @return {@code this} instance
+     */
+    public LongVariance2 setBiased(boolean v) {
+        biased = v;
+        return this;
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128.java
new file mode 100644
index 0000000..5f6b5f3
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128.java
@@ -0,0 +1,285 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import org.apache.commons.numbers.core.DD;
+
+/**
+ * A 128-bit unsigned integer.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.UInt128} to allow benchmarking.
+ * Additional methods may have been added for comparative benchmarks.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values
+ * generated by squaring {@code int} values.
+ *
+ * @since 1.1
+ */
+final class UInt128 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Data is stored using integers to allow efficient sum-with-carry addition
+
+    /** low 32-bits. */
+    private int d;
+    /** low 32-bits. */
+    private int c;
+    /** high 64-bits. */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt128() {
+        // No-op
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param x Value.
+     */
+    private UInt128(long x) {
+        d = (int) x;
+        c = (int) (x >>> Integer.SIZE);
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     *
+     * @param hi High 64-bits.
+     * @param mid Middle 32-bits.
+     * @param lo Low 32-bits.
+     */
+    private UInt128(long hi, int mid, int lo) {
+        this.d = lo;
+        this.c = mid;
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     */
+    UInt128(long hi, long lo) {
+        this.d = (int) lo;
+        this.c = (int) (lo >>> Integer.SIZE);
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt128 create() {
+        return new UInt128();
+    }
+
+    /**
+     * Create an instance of the {@code long} value.
+     * The value is assumed to be an unsigned 64-bit integer.
+     *
+     * @param x Value (must be positive).
+     * @return the instance
+     */
+    static UInt128 of(long x) {
+        return new UInt128(x);
+    }
+
+    /**
+     * Create an instance of the {@code UInt96} value.
+     *
+     * @param x Value.
+     * @return the instance
+     */
+    static UInt128 of(UInt96 x) {
+        final int lo = x.lo32();
+        final long hi = x.hi64();
+        final UInt128 y = new UInt128();
+        y.d = lo;
+        y.c = (int) hi;
+        y.ab = hi >>> Integer.SIZE;
+        return y;
+    }
+
+    /**
+     * Adds the value in place. It is assumed to be positive, for example the square of an
+     * {@code int} value. However no check is performed for a negative value.
+     *
+     * <p>Note: This addition handles {@value Long#MIN_VALUE} as an unsigned
+     * value of 2^63.
+     *
+     * @param x Value.
+     */
+    void addPositive(long x) {
+        // Sum with carry.
+        // Assuming x is positive then x + lo will not overflow 64-bits
+        // so we do not have to split x into upper and lower 32-bit values.
+        long s = x + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value in-place.
+     *
+     * @param x Value.
+     */
+    void add(UInt128 x) {
+        // Avoid issues adding to itself
+        final int dd = x.d;
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        long s = (dd & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+    /**
+     * Multiply by the unsigned value.
+     * Any overflow bits are lost.
+     *
+     * @param x Value.
+     * @return the product
+     */
+    UInt128 unsignedMultiply(int x) {
+        final long xx = x & MASK32;
+        // Multiply with carry.
+        long product = xx * (d & MASK32);
+        final int dd = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (c & MASK32);
+        final int cc = (int) product;
+        // Possible overflow here and bits are lost
+        final long aabb = (product >>> Integer.SIZE) + xx * ab;
+        return new UInt128(aabb, cc, dd);
+    }
+
+    /**
+     * Subtracts the value.
+     * Any overflow bits (negative result) are lost.
+     *
+     * @param x Value.
+     * @return the difference
+     */
+    UInt128 subtract(UInt128 x) {
+        // Difference with carry.
+        long diff = (d & MASK32) - (x.d & MASK32);
+        final int dd = (int) diff;
+        diff = (diff >> Integer.SIZE) + (c & MASK32) - (x.c & MASK32);
+        final int cc = (int) diff;
+        // Possible overflow here and bits are lost containing info on the
+        // magnitude of the true negative value
+        final long aabb = (diff >> Integer.SIZE) + ab - x.ab;
+        return new UInt128(aabb, cc, dd);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        // Test if we have more than 63-bits
+        if (ab != 0 || c < 0) {
+            ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * 4)
+                .putLong(ab)
+                .putInt(c)
+                .putInt(d);
+            return new BigInteger(1, bb.array());
+        }
+        // Create from a long
+        return BigInteger.valueOf(((c & MASK32) << Integer.SIZE) | (d & MASK32));
+    }
+
+    /**
+     * Convert to a double-double.
+     *
+     * @return the value
+     */
+    DD toDD() {
+        // Sum low to high
+        return DD.ofSum(d & MASK32, (c & MASK32) * 0x1.0p32)
+            .add((ab & MASK32) * 0x1.0p64)
+            .add((ab >>> Integer.SIZE) * 0x1.0p96);
+    }
+
+    /**
+     * Convert to a {@code double}.
+     *
+     * @return the value
+     */
+    double toDouble() {
+        return IntMath.uin128ToDouble(hi64(), lo64());
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * @return the low 64-bits
+     */
+    long lo64() {
+        return (d & MASK32) | ((c & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the low 32-bits as an {@code int} value.
+     *
+     * @return bits 32-1
+     */
+    int lo32() {
+        return d;
+    }
+
+    /**
+     * Return the middle 32-bits as an {@code int} value.
+     *
+     * @return bits 64-33
+     */
+    int mid32() {
+        return c;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 128-65
+     */
+    long hi64() {
+        return ab;
+    }
+
+    /**
+     * Return the higher 32-bits as an {@code int} value.
+     *
+     * @return bits 128-97
+     */
+    int hi32() {
+        return (int) (ab >>> Integer.SIZE);
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192.java
new file mode 100644
index 0000000..7dd3de2
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192.java
@@ -0,0 +1,297 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import org.apache.commons.numbers.core.DD;
+
+/**
+ * A 192-bit unsigned integer.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.UInt192} to allow benchmarking.
+ * Additional methods may have been added for comparative benchmarks.
+ *
+ * <p>This is a specialised class to implement an accumulator of squared {@code long} values.
+ *
+ * @since 1.1
+ */
+final class UInt192 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Data is stored using integers to allow efficient sum-with-carry addition
+
+    /** bits 32-1 (low 32-bits). */
+    private int f;
+    /** bits 64-33. */
+    private int e;
+    /** bits 96-65. */
+    private int d;
+    /** bits 128-97. */
+    private int c;
+    /** bits 192-129 (high 64-bits). */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt192() {
+        // No-op
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param mid Middle 64-bits.
+     * @param lo Low 64-bits.
+     */
+    UInt192(long hi, long mid, long lo) {
+        this.f = (int) lo;
+        this.e = (int) (lo >>> Integer.SIZE);
+        this.d = (int) mid;
+        this.c = (int) (mid >>> Integer.SIZE);
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     *
+     * @param ab bits 192-129 (high 64-bits).
+     * @param c bits 128-97.
+     * @param d bits 96-65.
+     * @param e bits 64-33.
+     * @param f bits 32-1.
+     */
+    private UInt192(long ab, int c, int d, int e, int f) {
+        this.ab = ab;
+        this.c = c;
+        this.d = d;
+        this.e = e;
+        this.f = f;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt192 create() {
+        return new UInt192();
+    }
+
+    /**
+     * Adds the squared value {@code x * x}.
+     *
+     * @param x Value.
+     */
+    void addSquare(long x) {
+        final long lo = x * x;
+        final long hi = IntMath.squareHigh(x);
+
+        // Sum with carry.
+        long s = (lo & MASK32) + (f & MASK32);
+        f = (int) s;
+        s = (s >>> Integer.SIZE) + (lo >>> Integer.SIZE) + (e & MASK32);
+        e = (int) s;
+        s = (s >>> Integer.SIZE) + (hi & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (hi >>> Integer.SIZE) + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the squared value {@code x * x}.
+     *
+     * <p>This uses {@code java.lang.Math.multiplyHigh(long, long)} and requires Java 11+.
+     *
+     * @param x Value.
+     */
+    void addSquare2(long x) {
+        final long lo = x * x;
+        final long hi = Math.multiplyHigh(x, x);
+
+        // Sum with carry.
+        long s = (lo & MASK32) + (f & MASK32);
+        f = (int) s;
+        s = (s >>> Integer.SIZE) + (lo >>> Integer.SIZE) + (e & MASK32);
+        e = (int) s;
+        s = (s >>> Integer.SIZE) + (hi & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (hi >>> Integer.SIZE) + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(UInt192 x) {
+        // Avoid issues adding to itself
+        final int ff = x.f;
+        final int ee = x.e;
+        final int dd = x.d;
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        long s = (ff & MASK32) + (f & MASK32);
+        f = (int) s;
+        s = (s >>> Integer.SIZE) + (ee & MASK32) + (e & MASK32);
+        e = (int) s;
+        s = (s >>> Integer.SIZE) + (dd & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+
+    /**
+     * Multiply by the unsigned value.
+     * Any overflow bits are lost.
+     *
+     * @param x Value.
+     * @return the product
+     */
+    UInt192 unsignedMultiply(int x) {
+        final long xx = x & MASK32;
+        // Multiply with carry.
+        long product = xx * (f & MASK32);
+        final int ff = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (e & MASK32);
+        final int ee = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (d & MASK32);
+        final int dd = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (c & MASK32);
+        final int cc = (int) product;
+        // Possible overflow here and bits are lost
+        final long aabb = (product >>> Integer.SIZE) + xx * ab;
+        return new UInt192(aabb, cc, dd, ee, ff);
+    }
+
+    /**
+     * Subtracts the value.
+     * Any overflow bits (negative result) are lost.
+     *
+     * @param x Value.
+     * @return the difference
+     */
+    UInt192 subtract(UInt128 x) {
+        // Difference with carry.
+        // Subtract common part.
+        long diff = (f & MASK32) - (x.lo32()  & MASK32);
+        final int ff = (int) diff;
+        diff = (diff >> Integer.SIZE) + (e & MASK32) - (x.mid32() & MASK32);
+        final int ee = (int) diff;
+        diff = (diff >> Integer.SIZE) + (d & MASK32) - (x.hi64() & MASK32);
+        final int dd = (int) diff;
+        diff = (diff >> Integer.SIZE) + (c & MASK32) - (x.hi64() >>> Integer.SIZE);
+        final int cc = (int) diff;
+        // Possible overflow here and bits are lost containing info on the
+        // magnitude of the true negative value
+        final long aabb = (diff >> Integer.SIZE) + ab;
+        return new UInt192(aabb, cc, dd, ee, ff);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        final ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * 6)
+            .putLong(ab)
+            .putInt(c)
+            .putInt(d)
+            .putInt(e)
+            .putInt(f);
+        // Sign is always positive. This works for zero.
+        return new BigInteger(1, bb.array());
+    }
+
+    /**
+     * Convert to a double.
+     *
+     * @return the value
+     */
+    double toDouble() {
+        long h = hi64();
+        long m = mid64();
+        long l = lo64();
+        if (h == 0) {
+            return IntMath.uin128ToDouble(m, l);
+        }
+        // For correct rounding we use a sticky bit to represent magnitude
+        // lost from the low 64-bits. The result is scaled by 2^64.
+        return IntMath.uin128ToDouble(h, m | ((l != 0) ? 1 : 0)) * 0x1.0p64;
+    }
+
+    /**
+     * Convert to a double-double.
+     *
+     * @return the value
+     */
+    DD toDD() {
+        // Sum low to high
+        return DD.ofSum(f & MASK32, (e & MASK32) * 0x1.0p32)
+            .add((d & MASK32) * 0x1.0p64)
+            .add((c & MASK32) * 0x1.0p96)
+            .add((ab & MASK32) * 0x1.0p128)
+            .add((ab >>> Integer.SIZE) * 0x1.0p160);
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * @return the low 64-bits
+     */
+    long lo64() {
+        return (f & MASK32) | ((e & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the middle 64-bits as a {@code long} value.
+     *
+     * @return bits 128-65
+     */
+    long mid64() {
+        return (d & MASK32) | ((c & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 192-129
+     */
+    long hi64() {
+        return ab;
+    }
+
+    /**
+     * Return the higher 32-bits as an {@code int} value.
+     *
+     * @return the high 32-bits
+     */
+    int hi32() {
+        return (int) (ab >>> Integer.SIZE);
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96.java
new file mode 100644
index 0000000..d7e1659
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96.java
@@ -0,0 +1,170 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import org.apache.commons.numbers.core.DD;
+
+/**
+ * A 96-bit unsigned integer.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.Int96} to allow benchmarking.
+ * Additional methods may have been added for comparative benchmarks.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values
+ * generated by squaring {@code int} values from an array (max observations=2^31).
+ *
+ * @since 1.1
+ */
+final class UInt96 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Low data is stored using an integer to allow efficient sum-with-carry addition
+
+    /** bits 32-1 (low 32-bits). */
+    private int c;
+    /** bits 96-33. */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt96() {
+        // No-op
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param x Value.
+     */
+    private UInt96(long x) {
+        c = (int) x;
+        ab = (int) (x >>> Integer.SIZE);
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 32-bits.
+     */
+    UInt96(long hi, int lo) {
+        this.c = lo;
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt96 create() {
+        return new UInt96();
+    }
+
+    /**
+     * Create an instance of the {@code long} value.
+     * The value is assumed to be an unsigned 64-bit integer.
+     *
+     * @param x Value (must be positive).
+     * @return the instance
+     */
+    static UInt96 of(long x) {
+        return new UInt96(x);
+    }
+
+    /**
+     * Adds the value. It is assumed to be positive, for example the square of an
+     * {@code int} value. However no check is performed for a negative value.
+     *
+     * <p>Note: This addition handles {@value Long#MIN_VALUE} as an unsigned
+     * value of 2^63.
+     *
+     * @param x Value.
+     */
+    void addPositive(long x) {
+        // Sum with carry.
+        // Assuming x is positive then x + lo will not overflow 64-bits
+        // so we do not have to split x into upper and lower 32-bit values.
+        final long s = x + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(UInt96 x) {
+        // Avoid issues adding to itself
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        final long s = (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        if (ab != 0) {
+            final ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * 3)
+                .putLong(ab)
+                .putInt(c);
+            return new BigInteger(1, bb.array());
+        }
+        return BigInteger.valueOf(c & MASK32);
+    }
+
+    /**
+     * Convert to a double-double.
+     *
+     * @return the value
+     */
+    DD toDD() {
+        // Sum low to high
+        return DD.ofSum(c & MASK32, (ab & MASK32) * 0x1.0p32)
+            .add((ab >>> Integer.SIZE) * 0x1.0p64);
+    }
+
+    /**
+     * Return the lower 32-bits as an {@code int} value.
+     *
+     * @return bits 32-1
+     */
+    int lo32() {
+        return c;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 96-33
+     */
+    long hi64() {
+        return ab;
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128Test.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128Test.java
new file mode 100644
index 0000000..2e490f0
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128Test.java
@@ -0,0 +1,194 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+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 Int128}.
+ */
+class Int128Test {
+    private static final BigInteger TWO_POW_128 = BigInteger.ONE.shiftLeft(128);
+    private static final BigInteger TWO_POW_127 = BigInteger.ONE.shiftLeft(127);
+    private static final BigInteger MINUS_TWO_POW_127 = BigInteger.ONE.shiftLeft(127).negate();
+
+    @Test
+    void testCreate() {
+        final Int128 v = Int128.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"testAddLong"})
+    void testToBigInteger(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).shiftLeft(64).add(BigInteger.valueOf(b));
+        final Int128 v = new Int128(a, b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final Int128 v = Int128.of(a);
+        v.add(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"testAddLong"})
+    void testAddLong2(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final Int128 v = Int128.of(a);
+        v.add2(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, 2, Long.MIN_VALUE, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+                builder.accept(Arguments.of(i, -j));
+                builder.accept(Arguments.of(-i, j));
+                builder.accept(Arguments.of(-i, -j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final Int128 v = Int128.create();
+        for (final long x : a) {
+            v.add(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), v.toDD(), 0x1.0p-106, "DD");
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"testAddLongs"})
+    void testAddLongs2(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final Int128 v = Int128.create();
+        for (final long x : a) {
+            v.add2(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> -(x >>> 2)).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, long b, long c, long d) {
+        final Int128 x = new Int128(a, b);
+        final Int128 y = new Int128(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo64());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The Int128 result is a signed 128-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [2^-127, 2^127).
+        // Since the overflow will be at most 1-bit we can wrap the value
+        // using +/- 2^128.
+        if (expected.compareTo(TWO_POW_127) >= 0) {
+            // too high
+            expected = expected.subtract(TWO_POW_128);
+        } else if (expected.compareTo(MINUS_TWO_POW_127) < 0) {
+            // too low
+            expected = expected.add(TWO_POW_128);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), x.toDD(), 0x1.0p-106, "DD");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.compareTo(TWO_POW_127) >= 0) {
+            // too high
+            expected = expected.subtract(TWO_POW_128);
+        } else if (expected.compareTo(MINUS_TWO_POW_127) < 0) {
+            // too low
+            expected = expected.add(TWO_POW_128);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 1, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSquareLow(long a) {
+        final BigInteger expected = BigInteger.valueOf(a).pow(2);
+        final UInt128 v = Int128.of(a).squareLow();
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static LongStream testSquareLow() {
+        final LongStream.Builder builder = LongStream.builder();
+        final long[] x = {0, 1, Long.MIN_VALUE, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            builder.accept(i);
+            builder.accept(-i);
+        }
+        RandomSource.XO_RO_SHI_RO_128_PP.create().longs(20).forEach(builder);
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMathTest.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMathTest.java
new file mode 100644
index 0000000..d1bc9cf
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMathTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.util.stream.LongStream;
+import java.util.stream.LongStream.Builder;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link IntMath}.
+ */
+class IntMathTest {
+    /** 2^63. */
+    private static final BigInteger TWO_POW_63 = BigInteger.ONE.shiftLeft(63);
+
+    @ParameterizedTest
+    @MethodSource
+    void testSquareHigh(long a) {
+        final long actual = IntMath.squareHigh(a);
+        final long expected = BigInteger.valueOf(a).pow(2).shiftRight(64).longValue();
+        Assertions.assertEquals(expected, actual);
+    }
+
+    static LongStream testSquareHigh() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Builder builder = LongStream.builder();
+        builder.accept(0);
+        builder.accept(Long.MAX_VALUE);
+        builder.accept(Long.MIN_VALUE);
+        rng.ints(5).forEach(builder::accept);
+        rng.longs(50).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 1).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 2).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 5).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 13).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 35).forEach(builder::accept);
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testUnsignedMultiplyHigh(long a, long b) {
+        final long actual = IntMath.unsignedMultiplyHigh(a, b);
+        final BigInteger bi1 = toUnsignedBigInteger(a);
+        final BigInteger bi2 = toUnsignedBigInteger(b);
+        final BigInteger expected = bi1.multiply(bi2);
+        Assertions.assertEquals(expected.shiftRight(Long.SIZE).longValue(), actual,
+            () -> String.format("%s * %s", bi1, bi2));
+        final double x = expected.doubleValue();
+        Assertions.assertEquals(x, IntMath.unsignedMultiplyToDoubleBigInteger(a, b),
+            () -> String.format("double %s * %s", bi1, bi2));
+        Assertions.assertEquals(x, IntMath.unsignedMultiplyToDouble(a, b),
+            () -> String.format("double2 %s * %s", bi1, bi2));
+    }
+
+    static Stream<Arguments> testUnsignedMultiplyHigh() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] values = {
+            -1, 0, 1, Long.MAX_VALUE, Long.MIN_VALUE,
+            0xffL, 0xff00L, 0xff0000L, 0xff000000L,
+            0xff00000000L, 0xff0000000000L, 0xff000000000000L, 0xff000000000000L,
+            0xffffL, 0xffff0000L, 0xffff00000000L, 0xffff000000000000L,
+            0xffffffffL, 0xffffffff00000000L
+        };
+        for (final long v1 : values) {
+            for (final long v2 : values) {
+                builder.accept(Arguments.of(v1, v2));
+                builder.accept(Arguments.of(v1 >>> 15, v2 >>> 18));
+            }
+        }
+        for (int i = 0; i < 200; i++) {
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    /**
+     * Create a big integer treating the value as unsigned.
+     *
+     * @param v Value
+     * @return the big integer
+     */
+    private static BigInteger toUnsignedBigInteger(long v) {
+        return v < 0 ?
+            TWO_POW_63.add(BigInteger.valueOf(v & Long.MAX_VALUE)) :
+            BigInteger.valueOf(v);
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testUin128ToDouble(long a, long b) {
+        final BigInteger bi1 = toUnsignedBigInteger(a).shiftLeft(Long.SIZE);
+        final BigInteger bi2 = toUnsignedBigInteger(b);
+        final double x = bi1.add(bi2).doubleValue();
+        Assertions.assertEquals(x, IntMath.uin128ToDouble(a, b),
+            () -> String.format("%s + %s", a, b));
+    }
+
+    static Stream<Arguments> testUin128ToDouble() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        for (int i = 0; i < 100; i++) {
+            long a = rng.nextLong();
+            long b = rng.nextLong();
+            builder.accept(Arguments.of(a, b));
+            builder.accept(Arguments.of(0, b));
+            // Edge cases where trailing bits are required for rounding.
+            // Create a 55-bit number. Ensure the highest bit is set.
+            a = (a << 9) | Long.MIN_VALUE;
+            // Shift right and carry bits down.
+            int shift = rng.nextInt(1, 64);
+            long c = a >>> shift;
+            long d = a << -shift;
+            // Check
+            Assertions.assertEquals(Long.bitCount(a), Long.bitCount(c) + Long.bitCount(d));
+            builder.accept(Arguments.of(c, d));
+            // Add a trailing bit that may change rounding
+            builder.accept(Arguments.of(c, d | 1));
+        }
+        // At least one case where the trailing bit does effect rounding
+        // 54-bits all set is an odd number + 0.5
+        builder.accept(Arguments.of(1, (1L << 11)));
+        builder.accept(Arguments.of(1, (1L << 11) | 1));
+        // Unset the second to last bit and repeat above is an even number + 0.5
+        builder.accept(Arguments.of(1, ((1L & ~0x2) << 11)));
+        builder.accept(Arguments.of(1, ((1L & ~0x2) << 11) | 1));
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVarianceTest.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVarianceTest.java
new file mode 100644
index 0000000..6d4fa21
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVarianceTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.apache.commons.statistics.descriptive.LongVariance;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Test for {@link LongVariance2}. Tested against {@link LongVariance}.
+ */
+class LongVarianceTest {
+    @ParameterizedTest
+    @ValueSource(ints = {0, 1, 2, 5, 10, 13, 1000})
+    void testVariance(int n) {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final long[] values = rng.longs(n).toArray();
+        final LongVariance v1 = LongVariance.of(values);
+        final LongVariance2 v2 = LongVariance2.of(values);
+        double variance = v1.getAsDouble();
+        final double actual = v1.getAsDouble();
+        Assertions.assertEquals(variance, actual, "Variance");
+
+        if (n > 1) {
+            variance = v1.setBiased(true).getAsDouble();
+            Assertions.assertEquals(variance, v2.setBiased(true).getAsDouble(), "Variance biased");
+        }
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/TestUtils.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/TestUtils.java
new file mode 100644
index 0000000..7144a6a
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/TestUtils.java
@@ -0,0 +1,159 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.util.function.Supplier;
+import org.apache.commons.numbers.core.DD;
+import org.junit.jupiter.api.Assertions;
+
+/**
+ * Test utilities.
+ */
+final class TestUtils {
+    /** No instances. */
+    private TestUtils() {}
+
+    // DD equality checks adapted from o.a.c.numbers.core.TestUtils
+
+    /**
+     * Assert the two numbers are equal within the provided relative error.
+     *
+     * <p>The provided error is relative to the exact result in expected: (e - a) / e.
+     * If expected is zero this division is undefined. In this case the actual must be zero
+     * (no absolute tolerance is supported). The reporting of the error uses the absolute
+     * error and the return value of the relative error is 0. Cases of complete cancellation
+     * should be avoided for benchmarking relative accuracy.
+     *
+     * <p>Note that the actual double-double result is not validated using the high and low
+     * parts individually. These are summed and compared to the expected.
+     *
+     * <p>Set {@code eps} to negative to report the relative error to the stdout and
+     * ignore failures.
+     *
+     * <p>The relative error is signed. The sign of the error
+     * is the same as that returned from Double.compare(actual, expected); it is
+     * computed using {@code actual - expected}.
+     *
+     * @param expected expected value
+     * @param actual actual value
+     * @param eps maximum relative error between the two values
+     * @param msg failure message
+     * @return relative error difference between the values (signed)
+     * @throws NumberFormatException if {@code actual} contains non-finite values
+     */
+    static double assertEquals(BigDecimal expected, DD actual, double eps, String msg) {
+        return assertEquals(expected, actual, eps, () -> msg);
+    }
+
+    /**
+     * Assert the two numbers are equal within the provided relative error.
+     *
+     * <p>The provided error is relative to the exact result in expected: (e - a) / e.
+     * If expected is zero this division is undefined. In this case the actual must be zero
+     * (no absolute tolerance is supported). The reporting of the error uses the absolute
+     * error and the return value of the relative error is 0. Cases of complete cancellation
+     * should be avoided for benchmarking relative accuracy.
+     *
+     * <p>Note that the actual double-double result is not validated using the high and low
+     * parts individually. These are summed and compared to the expected.
+     *
+     * <p>Set {@code eps} to negative to report the relative error to the stdout and
+     * ignore failures.
+     *
+     * <p>The relative error is signed. The sign of the error
+     * is the same as that returned from Double.compare(actual, expected); it is
+     * computed using {@code actual - expected}.
+     *
+     * @param expected expected value
+     * @param actual actual value
+     * @param eps maximum relative error between the two values
+     * @param msg failure message
+     * @return relative error difference between the values (signed)
+     * @throws NumberFormatException if {@code actual} contains non-finite values
+     */
+    static double assertEquals(BigDecimal expected, DD actual, double eps, Supplier<String> msg) {
+        // actual - expected
+        final BigDecimal delta = new BigDecimal(actual.hi())
+            .add(new BigDecimal(actual.lo()))
+            .subtract(expected);
+        boolean equal;
+        if (expected.compareTo(BigDecimal.ZERO) == 0) {
+            // Edge case. Currently an absolute tolerance is not supported as summation
+            // to zero cases generated in testing all pass.
+            equal = actual.doubleValue() == 0;
+
+            // DEBUG:
+            if (eps < 0) {
+                if (!equal) {
+                    printf("%sexpected 0 != actual <%s + %s> (abs.error=%s)%n",
+                        prefix(msg), actual.hi(), actual.lo(), delta.doubleValue());
+                }
+            } else if (!equal) {
+                Assertions.fail(String.format("%sexpected 0 != actual <%s + %s> (abs.error=%s)",
+                    prefix(msg), actual.hi(), actual.lo(), delta.doubleValue()));
+            }
+
+            return 0;
+        }
+
+        final double rel = delta.divide(expected, MathContext.DECIMAL128).doubleValue();
+        // Allow input of a negative maximum ULPs
+        equal = Math.abs(rel) <= Math.abs(eps);
+
+        // DEBUG:
+        if (eps < 0) {
+            if (!equal) {
+                printf("%sexpected <%s> != actual <%s + %s> (rel.error=%s (%.3f x tol))%n",
+                    prefix(msg), expected.round(MathContext.DECIMAL128), actual.hi(), actual.lo(),
+                    rel, Math.abs(rel) / eps);
+            }
+        } else if (!equal) {
+            Assertions.fail(String.format("%sexpected <%s> != actual <%s + %s> (rel.error=%s (%.3f x tol))",
+                prefix(msg), expected.round(MathContext.DECIMAL128), actual.hi(), actual.lo(),
+                rel, Math.abs(rel) / eps));
+        }
+
+        return rel;
+    }
+
+    /**
+     * Print a formatted message to stdout.
+     * Provides a single point to disable checkstyle warnings on print statements and
+     * enable/disable all print debugging.
+     *
+     * @param format Format string.
+     * @param args Arguments.
+     */
+    static void printf(String format, Object... args) {
+        // CHECKSTYLE: stop regex
+        System.out.printf(format, args);
+        // CHECKSTYLE: resume regex
+    }
+
+    /**
+     * Get the prefix for the message.
+     *
+     * @param msg Message supplier
+     * @return the prefix
+     */
+    static String prefix(Supplier<String> msg) {
+        return msg == null ? "" : msg.get() + ": ";
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128Test.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128Test.java
new file mode 100644
index 0000000..db002c5
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128Test.java
@@ -0,0 +1,238 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+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 UInt128}.
+ */
+class UInt128Test {
+    @Test
+    void testCreate() {
+        final UInt128 v = UInt128.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @Test
+    void testAddLongMinValue() {
+        final UInt128 v = UInt128.of(1268361283468345237L);
+        final BigInteger x = BigInteger.ONE.shiftLeft(63);
+        BigInteger expected = v.toBigInteger();
+        for (int i = 1; i <= 5; i++) {
+            // Accepts a negative value without exception. This is
+            // computed correctly if the current low 32 bits
+            // added to the argument do not overflow. This is always
+            // true for min value as all lower 32-bits are zero.
+            v.addPositive(Long.MIN_VALUE);
+            expected = expected.add(x);
+            Assertions.assertEquals(expected, v.toBigInteger());
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final UInt128 v = UInt128.of(a);
+        v.addPositive(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt128 v = UInt128.create();
+        for (final long x : a) {
+            Assertions.assertFalse(x < 0, "Value must be positive");
+            v.addPositive(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), v.toDD(), 0x1.0p-106, "DD");
+        Assertions.assertEquals(expected.doubleValue(), v.toDouble(), "double");
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, long b, long c, long d) {
+        final UInt128 x = new UInt128(a, b);
+        final UInt128 y = new UInt128(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo64());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 128-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^128).
+        if (expected.testBit(128)) {
+            expected = expected.flipBit(128);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), x.toDD(), 0x1.0p-106, "DD");
+        Assertions.assertEquals(expected.doubleValue(), x.toDouble(), "double");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(128)) {
+            expected = expected.flipBit(128);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 1, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testOfInt96(long a, int b) {
+        final UInt96 x = new UInt96(a, b);
+        final UInt128 y = UInt128.of(x);
+        Assertions.assertEquals(x.toBigInteger(), y.toBigInteger());
+    }
+
+    static Stream<Arguments> testOfInt96() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final int b = rng.nextInt();
+            builder.accept(Arguments.of(a, b));
+            builder.accept(Arguments.of(0, b));
+            builder.accept(Arguments.of(a, 0));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testMultiplyInt(long a, long b, int n) {
+        assertMultiplyInt(a, b, n);
+        assertMultiplyInt(a >>> 32, b, n);
+        assertMultiplyInt(0, b, n);
+    }
+
+    private static void assertMultiplyInt(long a, long b, int n) {
+        final UInt128 v = new UInt128(a, b);
+        BigInteger expected = v.toBigInteger().multiply(BigInteger.valueOf(n & 0xffff_ffffL));
+        // Clip to 128-bits. Only required if the upper 32-bits are non-zero.
+        final int len = expected.bitLength();
+        if (len > 128 && v.hi32() != 0) {
+            expected = expected.subtract(expected.shiftRight(128).shiftLeft(128));
+        }
+        Assertions.assertEquals(expected, v.unsignedMultiply(n).toBigInteger());
+    }
+
+    static Stream<Arguments> testMultiplyInt() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final int[] x = {0, 1, -1, Integer.MAX_VALUE, Integer.MIN_VALUE};
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            for (final int n : x) {
+                builder.accept(Arguments.of(a, b, n));
+            }
+            for (int j = 0; j < 5; j++) {
+                builder.accept(Arguments.of(a, b, rng.nextInt()));
+            }
+        }
+        builder.accept(Arguments.of(-1L >>> 32, -1L, -1));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSubtract(long a, long b, long c, long d) {
+        assertSubtract(a, b, c, d);
+        assertSubtract(c, d, a, b);
+    }
+
+    private static void assertSubtract(long a, long b, long c, long d) {
+        final UInt128 x = new UInt128(a, b);
+        final UInt128 y = new UInt128(c, d);
+        BigInteger expected = x.toBigInteger().subtract(y.toBigInteger());
+        if (expected.signum() < 0) {
+            expected = expected.add(BigInteger.ONE.shiftLeft(128));
+        }
+        Assertions.assertEquals(expected, x.subtract(y).toBigInteger());
+    }
+
+    static Stream<Arguments> testSubtract() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            final long d = rng.nextLong();
+            builder.accept(Arguments.of(a, b, c, d));
+            builder.accept(Arguments.of(0, 0, c, d));
+            builder.accept(Arguments.of(-1L, -1L, c, d));
+        }
+        builder.accept(Arguments.of(-1L, -1L, -1L, -1L));
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192Test.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192Test.java
new file mode 100644
index 0000000..7f8d124
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192Test.java
@@ -0,0 +1,206 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+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 UInt192}.
+ */
+class UInt192Test {
+    @Test
+    void testCreate() {
+        final UInt192 v = UInt192.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddSquareLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).pow(2)
+            .add(BigInteger.valueOf(b).pow(2));
+        final UInt192 v = UInt192.create();
+        v.addSquare(a);
+        v.addSquare(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddSquareLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 61278342166787978L, 42, 8652939272947492397L};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddSquareLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .map(x -> x.pow(2))
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt192 v = UInt192.create();
+        for (final long x : a) {
+            v.addSquare(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), v.toDD(), 0x1.0p-106, "DD");
+        Assertions.assertEquals(expected.doubleValue(), v.toDouble(), "double");
+    }
+
+    static Stream<Arguments> testAddSquareLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt192(long a, long b, long c, long d, long e, long f) {
+        final UInt192 x = new UInt192(a, b, c);
+        final UInt192 y = new UInt192(d, e, f);
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 192-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^192).
+        if (expected.testBit(192)) {
+            expected = expected.flipBit(192);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d, %d) + (%d, %d, %d)", a, b, c, d, e, f));
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), x.toDD(), 0x1.0p-106, "DD");
+        Assertions.assertEquals(expected.doubleValue(), x.toDouble(), "double");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(192)) {
+            expected = expected.flipBit(192);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d, %d) self-addition", d, e, f));
+    }
+
+    static Stream<Arguments> testAddInt192() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testMultiplyInt(long a, long b, long c, int n) {
+        assertMultiplyInt(a, b, c, n);
+        assertMultiplyInt(a >>> 32, b, c, n);
+        assertMultiplyInt(0, b, c, n);
+    }
+
+    private static void assertMultiplyInt(long a, long b, long c, int n) {
+        final UInt192 v = new UInt192(a, b, c);
+        BigInteger expected = v.toBigInteger().multiply(BigInteger.valueOf(n & 0xffff_ffffL));
+        // Clip to 192-bits. Only required if the upper 32-bits are non-zero.
+        final int len = expected.bitLength();
+        if (len > 192 && v.hi32() != 0) {
+            expected = expected.subtract(expected.shiftRight(192).shiftLeft(192));
+        }
+        Assertions.assertEquals(expected, v.unsignedMultiply(n).toBigInteger());
+    }
+
+    static Stream<Arguments> testMultiplyInt() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final int[] x = {0, 1, -1, Integer.MAX_VALUE, Integer.MIN_VALUE};
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            for (final int n : x) {
+                builder.accept(Arguments.of(a, b, c, n));
+            }
+            for (int j = 0; j < 5; j++) {
+                builder.accept(Arguments.of(a, b, c, rng.nextInt()));
+            }
+        }
+        builder.accept(Arguments.of(-1L >>> 32, -1L, -1L, -1));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSubtract(long a, long b, long c, long d, long e) {
+        assertSubtract(a, b, c, d, e);
+    }
+
+    private static void assertSubtract(long a, long b, long c, long d, long e) {
+        final UInt192 x = new UInt192(a, b, c);
+        final UInt128 y = new UInt128(d, e);
+        BigInteger expected = x.toBigInteger().subtract(y.toBigInteger());
+        if (expected.signum() < 0) {
+            expected = expected.add(BigInteger.ONE.shiftLeft(192));
+        }
+        Assertions.assertEquals(expected, x.subtract(y).toBigInteger());
+    }
+
+    static Stream<Arguments> testSubtract() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            final long d = rng.nextLong();
+            final long e = rng.nextLong();
+            builder.accept(Arguments.of(a, b, c, d, e));
+            builder.accept(Arguments.of(0, 0, 0, d, e));
+            builder.accept(Arguments.of(-1L, -1L, -1L, d, e));
+        }
+        builder.accept(Arguments.of(-1L, -1L, -1L, -1L, -1L));
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96Test.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96Test.java
new file mode 100644
index 0000000..57b142c
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96Test.java
@@ -0,0 +1,150 @@
+/*
+ * 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.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.numbers.core.DD;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+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 UInt96}.
+ */
+class UInt96Test {
+    @Test
+    void testCreate() {
+        final UInt96 v = UInt96.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @Test
+    void testAddLongMinValue() {
+        final UInt96 v = UInt96.of(5675757768682342956L);
+        final BigInteger x = BigInteger.ONE.shiftLeft(63);
+        BigInteger expected = v.toBigInteger();
+        for (int i = 1; i <= 5; i++) {
+            // Accepts a negative value without exception. This is
+            // computed correctly if the current low 32 bits
+            // added to the argument do not overflow. This is always
+            // true for min value as all lower 32-bits are zero.
+            v.addPositive(Long.MIN_VALUE);
+            expected = expected.add(x);
+            Assertions.assertEquals(expected, v.toBigInteger());
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final UInt96 v = UInt96.of(a);
+        v.addPositive(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt96 v = UInt96.create();
+        for (final long x : a) {
+            Assertions.assertFalse(x < 0, "Value must be positive");
+            v.addPositive(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        Assertions.assertEquals(
+            DD.from(new BigDecimal(expected)),
+            v.toDD());
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, int b, long c, int d) {
+        final UInt96 x = new UInt96(a, b);
+        final UInt96 y = new UInt96(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo32());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 96-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^96).
+        if (expected.testBit(96)) {
+            expected = expected.flipBit(96);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check floating-point representation
+        Assertions.assertEquals(
+            DD.from(new BigDecimal(expected)),
+            x.toDD());
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(96)) {
+            expected = expected.flipBit(96);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextInt(), rng.nextLong() >>> 2, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextInt(), rng.nextLong() >>> 1, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextInt(), rng.nextLong() >>> 2, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextInt(), rng.nextLong(), rng.nextInt()));
+        }
+        return builder.build();
+    }
+}
diff --git a/src/conf/checkstyle/checkstyle-suppressions.xml b/src/conf/checkstyle/checkstyle-suppressions.xml
index 8216ef6..c363f46 100644
--- a/src/conf/checkstyle/checkstyle-suppressions.xml
+++ b/src/conf/checkstyle/checkstyle-suppressions.xml
@@ -41,4 +41,5 @@
   <suppress checks="MethodLength" files=".*[/\\]WilcoxonSignedRankTestTest.java" />
   <suppress checks="IllegalCatch" files=".*[/\\]TestHelper.java" lines="390-450" />
   <suppress checks="IllegalCatch" files=".*[/\\]BaseStatisticTest.java" lines="280-400" />
+  <suppress checks="IllegalCatch" files=".*[/\\]IntMathTest.java" lines="165-175" />
 </suppressions>


(commons-statistics) 06/06: Correct javadoc in tests

Posted by ah...@apache.org.
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

commit 7239736fdab9864666b216f966e6bd9a53ccf78e
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Sat Dec 23 10:27:46 2023 +0000

    Correct javadoc in tests
---
 .../descriptive/BaseDoubleStatisticTest.java       | 36 +++++++++++-----------
 .../descriptive/DoubleStatisticsTest.java          |  2 +-
 2 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
index a349e2f..8de8b44 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
@@ -51,10 +51,10 @@ import org.junit.jupiter.params.provider.MethodSource;
  * <ol>
  *  <li>{@link java.util.function.DoubleConsumer#accept accept}
  *  <li>{@link java.util.function.DoubleConsumer#accept accept} and
- *      {@link StatisticAccumulator#combine(DoubleStatistic) combine}
+ *      {@link StatisticAccumulator#combine(StatisticResult) combine}
  *  <li>{@link #create(double...)}
  *  <li>{@link #create(double...)} and
- *      {@link StatisticAccumulator#combine(DoubleStatistic) combine}
+ *      {@link StatisticAccumulator#combine(StatisticResult) combine}
  * </ol>
  *
  * <p>A test implementation has to provide these method implementations:
@@ -547,7 +547,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
 
     /**
      * Returns {@code true} if the
-     * {@link StatisticAccumulator#combine(DoubleStatistic) combine}
+     * {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method is symmetric. If {@code true} then the combine will be tested with
      * duplicate instances of the statistic, combined in left-to-right and right-to-left;
      * the result must have the same statistic value.
@@ -740,10 +740,10 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
      * <ol>
      *  <li>{@link java.util.function.DoubleConsumer#accept accept}
      *  <li>{@link java.util.function.DoubleConsumer#accept accept} and
-     *      {@link StatisticAccumulator#combine(DoubleStatistic) combine}
+     *      {@link StatisticAccumulator#combine(StatisticResult) combine}
      *  <li>{@link #create(double...)}
      *  <li>{@link #create(double...)} and
-     *      {@link StatisticAccumulator#combine(DoubleStatistic) combine}
+     *      {@link StatisticAccumulator#combine(StatisticResult) combine}
      * </ol>
      *
      * <p>To test the {@code combine} method the data is split into approximately even groups
@@ -822,7 +822,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
     /**
      * Stream the arguments to test the computation of the statistic using the
      * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link StatisticAccumulator#combine(DoubleStatistic) combine}
+     * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method. The expected value and tolerance are supplied by the implementing class.
      *
      * @return the stream
@@ -834,7 +834,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
     /**
      * Stream the arguments to test the computation of the statistic using the
      * {@link #create(double...)} method for each array, then the
-     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method. The
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method. The
      * expected value and tolerance are supplied by the implementing class.
      *
      * @return the stream
@@ -847,7 +847,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
      * Stream the arguments to test the computation of the statistic using the
      * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
      * element of a parallel stream, then the
-     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      * The expected value and tolerance are supplied by the implementing class.
      *
      * <p>Note that this method uses the tolerance from {@link #getToleranceAcceptAndCombine()}.
@@ -861,7 +861,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
     /**
      * Stream the arguments to test the computation of the statistic using the multiple
      * {@code double[]} arrays and the
-     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      *
      * @param tol Test tolerance.
      * @param tolCustom Tolerance for any custom test data.
@@ -1015,7 +1015,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
     /**
      * Test the computation of the statistic using the
      * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link StatisticAccumulator#combine(DoubleStatistic) combine}
+     * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      */
     @ParameterizedTest
@@ -1027,7 +1027,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
     /**
      * Test the computation of the statistic using the
      * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link StatisticAccumulator#combine(DoubleStatistic) combine}
+     * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      */
     @ParameterizedTest
@@ -1037,7 +1037,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
     }
 
     /**
-     * Test the {@link StatisticAccumulator#combine(DoubleStatistic) combine} method
+     * Test the {@link StatisticAccumulator#combine(StatisticResult) combine} method
      * with an empty instance combined with a non-empty instance.
      */
     @ParameterizedTest
@@ -1052,7 +1052,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
     }
 
     /**
-     * Test the {@link StatisticAccumulator#combine(DoubleStatistic) combine} method
+     * Test the {@link StatisticAccumulator#combine(StatisticResult) combine} method
      * with a non-empty instance combined with an empty instance.
      */
     @ParameterizedTest
@@ -1070,7 +1070,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
      * Test the computation of the statistic using a parallel stream of {@code double}
      * values. The accumulator is the
      * {@link java.util.function.DoubleConsumer#accept(double) accept} method; the
-     * combiner is the {@link StatisticAccumulator#combine(DoubleStatistic) combine}
+     * combiner is the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      *
      * <p>Note: This method is similar to the
@@ -1095,7 +1095,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
      * Test the computation of the statistic using a parallel stream of {@code double[]}
      * arrays. The arrays are mapped to a statistic using the {@link #create(double...)}
      * method, and the stream reduced using the
-     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      *
      * <p>Note: This method is similar to the
      * {@link #testArrayAndCombine(double[][], double, DoubleTolerance)} method and uses
@@ -1205,7 +1205,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
 
     /**
      * Test the computation of the statistic using an empty instance and the
-     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method with
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method with
      * instances containing single values. Adding statistic instances containing 1 value
      * should be the same precision as adding a {@code double} value, although the two
      * results are not required to be exactly equal.
@@ -1240,7 +1240,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
 
     /**
      * Assert the computation of the statistic using the
-     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      *
      * @param constructor Function to create the statistic.
      * @param values Values.
@@ -1319,7 +1319,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccu
 
     /**
      * Combine the two statistics. This method asserts the contract of the
-     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      * The left-hand side (LHS) argument must be returned. The right-hand side (RHS) must
      * be unchanged by the operation.
      *
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/DoubleStatisticsTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/DoubleStatisticsTest.java
index 576e435..a73d855 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/DoubleStatisticsTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/DoubleStatisticsTest.java
@@ -68,7 +68,7 @@ class DoubleStatisticsTest {
         private final int id;
         /** The sample values. */
         private final double[][] values;
-        /* The number of values. */
+        /** The number of values. */
         private final long size;
 
         /**


(commons-statistics) 01/06: STATISTICS-81: Add StatisticResult interface

Posted by ah...@apache.org.
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

commit 4b18caee0b82cf6b326b4dcf66a5a161598db384
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Mon Dec 18 11:19:20 2023 +0000

    STATISTICS-81: Add StatisticResult interface
    
    The result of a statistic is supported as int/long/double/BigInteger.
---
 .../statistics/descriptive/DoubleStatistic.java    |   7 +-
 .../statistics/descriptive/StatisticResult.java    | 101 +++++++++++++
 .../commons/statistics/descriptive/Statistics.java |  30 +++-
 .../descriptive/StatisticResultTest.java           | 158 +++++++++++++++++++++
 src/conf/pmd/pmd-ruleset.xml                       |   7 +
 5 files changed, 297 insertions(+), 6 deletions(-)

diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/DoubleStatistic.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/DoubleStatistic.java
index 4baa9c9..6be034c 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/DoubleStatistic.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/DoubleStatistic.java
@@ -17,15 +17,12 @@
 package org.apache.commons.statistics.descriptive;
 
 import java.util.function.DoubleConsumer;
-import java.util.function.DoubleSupplier;
 
 /**
- * Represents a state object for computing a single {@code Statistic} over {@code double} valued input(s).
- *
- * <p>Base interface implemented by all statistics.
+ * Represents a state object for computing a statistic over {@code double} valued input(s).
  *
  * @since 1.1
  */
-public interface DoubleStatistic extends DoubleConsumer, DoubleSupplier {
+public interface DoubleStatistic extends DoubleConsumer, StatisticResult {
     // Composite interface
 }
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StatisticResult.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StatisticResult.java
new file mode 100644
index 0000000..56108e7
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StatisticResult.java
@@ -0,0 +1,101 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.function.DoubleSupplier;
+import java.util.function.IntSupplier;
+import java.util.function.LongSupplier;
+
+/**
+ * Represents the result of a statistic computed over a set of values.
+ *
+ * <p>Base interface implemented by all statistics.
+ *
+ * @since 1.1
+ */
+@FunctionalInterface
+public interface StatisticResult extends DoubleSupplier, IntSupplier, LongSupplier {
+    /**
+     * {@inheritDoc}
+     *
+     * <p>The default implementation uses the closest representable {@code int} value of
+     * the {@link #getAsDouble()} result. In the event of ties the result is towards
+     * positive infinity. This will raise an {@link ArithmeticException} if the closest
+     * integer result is not within the range {@code [-2^31, 2^31)}.
+     *
+     * @throws ArithmeticException if the {@code result} overflows an int, or is not
+     * finite
+     */
+    @Override
+    default int getAsInt() {
+        // Note: Do not use (int) getAsLong() to avoid a narrowing primitive conversion.
+        final double x = getAsDouble();
+        final double r = Statistics.roundToInteger(x);
+        if (r >= -0x1.0p31 && r < 0x1.0p31) {
+            return (int) r;
+        }
+        throw new ArithmeticException("integer overflow: " + x);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>The default implementation uses the closest representable {@code long} value of
+     * the {@link #getAsDouble()} result. In the event of ties the result is rounded
+     * positive infinity. This will raise an {@link ArithmeticException} if the closest
+     * long integer result is not within the range {@code [-2^63, 2^63)}.
+     *
+     * @throws ArithmeticException if the {@code result} overflows a long, or is not
+     * finite
+     */
+    @Override
+    default long getAsLong() {
+        final double x = getAsDouble();
+        final double r = Statistics.roundToInteger(x);
+        if (r >= -0x1.0p63 && r < 0x1.0p63) {
+            return (long) r;
+        }
+        throw new ArithmeticException("long integer overflow: " + x);
+    }
+
+    /**
+     * Gets a result as a {@link BigInteger}.
+     *
+     * <p>The default implementation uses the closest representable {@code BigInteger}
+     * value of the {@link #getAsDouble()} result. In the event of ties the result is
+     * rounded positive infinity. This will raise an {@link ArithmeticException} if the
+     * result is not finite.
+     *
+     * @return a result
+     * @throws ArithmeticException if the {@code result} is not finite
+     */
+    default BigInteger getAsBigInteger() {
+        final double x = getAsDouble();
+        if (!Double.isFinite(x)) {
+            throw new ArithmeticException("BigInteger overflow: " + x);
+        }
+        final double r = Statistics.roundToInteger(x);
+        if (r >= -0x1.0p63 && r < 0x1.0p63) {
+            // Representable as a long
+            return BigInteger.valueOf((long) r);
+        }
+        // Large result
+        return new BigDecimal(r).toBigInteger();
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java
index 63c7ab9..34d5df8 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java
@@ -22,6 +22,8 @@ import java.util.function.DoubleConsumer;
  * Utility methods for statistics.
  */
 final class Statistics {
+    /** 0.5. */
+    private static final double HALF = 0.5;
 
     /** No instances. */
     private Statistics() {}
@@ -45,7 +47,7 @@ final class Statistics {
      * Returns {@code true} if the second central moment {@code m2} is effectively
      * zero given the magnitude of the first raw moment {@code m1}.
      *
-     * <p>This method shares the logic for detecting a zero variance among implmentations
+     * <p>This method shares the logic for detecting a zero variance among implementations
      * that divide by the variance (e.g. skewness, kurtosis).
      *
      * @param m1 First raw moment (mean).
@@ -61,4 +63,30 @@ final class Statistics {
         // (1e-15 ~ 4.5 eps where eps = 2^-52).
         return m2 <= Math.pow(1e-15 * m1, 2);
     }
+
+    /**
+     * Get the whole number that is the nearest to x, with ties rounding towards positive infinity.
+     *
+     * <p>This method is intended to perform the equivalent of
+     * {@link Math#round(double)} without converting to a {@code long} primitive type.
+     * This allows the domain of the result to be checked against the range {@code [-2^63, 2^63)}.
+     *
+     * <p>Note: Adapted from {@code o.a.c.math4.AccurateMath.rint} and
+     * modified to perform rounding towards positive infinity.
+     *
+     * @param x Number from which nearest whole number is requested.
+     * @return a double number r such that r is an integer {@code r - 0.5 <= x < r + 0.5}
+     */
+    static double roundToInteger(double x) {
+        final double y = Math.floor(x);
+        final double d = x - y;
+        if (d >= HALF) {
+            // Here we do not preserve the sign of the operand in the case
+            // of -0.5 < x <= -0.0 since the rounded result is required as an integer.
+            // if y == -1.0:
+            //    return -0.0
+            return y + 1.0;
+        }
+        return y;
+    }
 }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/StatisticResultTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/StatisticResultTest.java
new file mode 100644
index 0000000..a6a13af
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/StatisticResultTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Test for {@link StatisticResult}.
+ *
+ * <p>Note: Rounding
+ *
+ * <p>Use of {@link Math#rint(double)} will round ties to the nearest even number.
+ * This may not be desirable. {@link Math#round(double)} will round ties to positive infinity
+ * which leads to different rounding for positive and negatives; it also returns a {@code long}.
+ * Casting will discard the fractional part and may not return the closest integer.
+ * Using a rounding mode of half-up is consistent across signs and ties. However it does not
+ * create a result invariant to integer shift.
+ * <pre>
+ * value     rint     round      cast     RoundingMode.HALF_UP
+ * -3.5      4        -3         -3       -4
+ * -2.5      2        -2         -2       -3
+ * -1.5      2        -1         -1       -2
+ * -0.5      0        0          0        -1
+ * 0.0       0        0          0        0
+ * 0.5       0        1          0        1
+ * 1.5       2        2          1        2
+ * 2.5       2        3          2        3
+ * 3.5       4        4          3        4
+ * </pre>
+ *
+ * <p>Rounding uses the equivalent of {@link Math#round(double)}. A test asserts that the result
+ * is invariant to shift.
+ */
+final class StatisticResultTest {
+    private static final int[] SIGNS = {-1, 1};
+
+    @ParameterizedTest
+    @ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY})
+    void testNonFinite(double x) {
+        final StatisticResult r = () -> x;
+        Assertions.assertThrows(ArithmeticException.class, () -> r.getAsInt(), "int result: " + x);
+        Assertions.assertThrows(ArithmeticException.class, () -> r.getAsLong(), "long result: " + x);
+        Assertions.assertThrows(ArithmeticException.class, () -> r.getAsBigInteger(), "BigInteger result: " + x);
+    }
+
+    @ParameterizedTest
+    @ValueSource(doubles = {0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3,
+        Integer.MAX_VALUE - 0.75, Integer.MAX_VALUE - 0.5, Integer.MAX_VALUE - 0.25,
+        Integer.MAX_VALUE, Integer.MAX_VALUE + 0.25,
+        Integer.MIN_VALUE - 0.5, Integer.MIN_VALUE - 0.25, Integer.MIN_VALUE,
+        Integer.MIN_VALUE + 0.25, Integer.MIN_VALUE + 0.5, Integer.MIN_VALUE + 0.75 })
+    void testRepresentableInt(double x) {
+        // Do not test both signs for the large negative integers
+        final int[] signs = x < 0 ? new int[] {1} : SIGNS;
+        for (final int sign : signs) {
+            final double y = x * sign;
+            final StatisticResult r = () -> y;
+            final BigDecimal expected = round(y);
+            Assertions.assertEquals(expected.intValue(), r.getAsInt(), () -> "int result: " + y);
+            Assertions.assertEquals(expected.longValue(), r.getAsLong(), () -> "long result: " + y);
+            Assertions.assertEquals(expected.toBigInteger(), r.getAsBigInteger(), () -> "BigInteger result: " + y);
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(doubles = {
+        Integer.MAX_VALUE + 0.5, Integer.MAX_VALUE + 1.25,
+        Integer.MIN_VALUE - 0.75, Integer.MIN_VALUE - 1.75,
+        Long.MIN_VALUE,
+        // Cannot represent Long.MAX_VALUE. Use the next value down:
+        // Math.nextDown(0x1.0p63)
+        9.223372036854775E18})
+    void testNonRepresentableInt(double x) {
+        final StatisticResult r = () -> x;
+        final BigDecimal expected = round(x);
+        Assertions.assertThrows(ArithmeticException.class, () -> r.getAsInt(), () -> "int result: " + x);
+        Assertions.assertEquals(expected.longValue(), r.getAsLong(), () -> "long result: " + x);
+        Assertions.assertEquals(expected.toBigInteger(), r.getAsBigInteger(), () -> "BigInteger result: " + x);
+    }
+
+    @ParameterizedTest
+    @ValueSource(doubles = {
+        Long.MAX_VALUE,
+        // Math.nextDown((double) Long.MIN_VALUE)
+        -9.223372036854778E18,
+        Double.MAX_VALUE,
+        -Double.MAX_VALUE,
+        })
+    void testNonRepresentableLong(double x) {
+        final StatisticResult r = () -> x;
+        final BigDecimal expected = round(x);
+        Assertions.assertThrows(ArithmeticException.class, () -> r.getAsInt(), () -> "int result: " + x);
+        Assertions.assertThrows(ArithmeticException.class, () -> r.getAsLong(), () -> "long result: " + x);
+        Assertions.assertEquals(expected.toBigInteger(), r.getAsBigInteger(), () -> "BigInteger result: " + x);
+    }
+
+    /**
+     * Round the value to the nearest integer, with ties rounding towards positive infinity.
+     * This matches the rounding of {@link Math#round(double)} but returns a floating-point result.
+     *
+     * @param x Value.
+     * @return the rounded result
+     */
+    private static BigDecimal round(double x) {
+        return x < 0 ?
+            new BigDecimal(x).setScale(0, RoundingMode.HALF_DOWN) :
+            new BigDecimal(x).setScale(0, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * Test the rounding of the double is invariant to shift. This is true if the rounding of
+     * ties is in a consistent direction.
+     */
+    @ParameterizedTest
+    @ValueSource(doubles = {-1.75, -1.5, -1.25, -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75})
+    void testShift(double x) {
+        final StatisticResult expected = () -> x;
+        for (final int shift : new int[] {-566, 42}) {
+            final StatisticResult r = () -> x + shift;
+            Assertions.assertEquals(expected.getAsInt() + shift, r.getAsInt(), () -> "int result: " + x + " + " + shift);
+            Assertions.assertEquals(expected.getAsLong() + shift, r.getAsLong(), () -> "long result: " + x + " + " + shift);
+            Assertions.assertEquals(expected.getAsBigInteger().add(BigInteger.valueOf(shift)),
+                r.getAsBigInteger(), () -> "BigInteger result: " + x + " + " + shift);
+        }
+    }
+
+    /**
+     * Test the rounding of a double is equivalent to {@link Math#round(double)}.
+     */
+    @ParameterizedTest
+    @ValueSource(doubles = {-1.75, -1.5, -1.25, -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75})
+    void testMatchesMathRound(double x) {
+        final StatisticResult r = () -> x;
+        final long expected = Math.round(x);
+        Assertions.assertEquals((int) expected, r.getAsInt(), () -> "int result: " + x);
+        Assertions.assertEquals(expected, r.getAsLong(), () -> "long result: " + x);
+        Assertions.assertEquals(BigInteger.valueOf(expected), r.getAsBigInteger(), () -> "BigInteger result: " + x);
+    }
+}
diff --git a/src/conf/pmd/pmd-ruleset.xml b/src/conf/pmd/pmd-ruleset.xml
index e008d05..ff41429 100644
--- a/src/conf/pmd/pmd-ruleset.xml
+++ b/src/conf/pmd/pmd-ruleset.xml
@@ -208,6 +208,13 @@
         value=".*'BinomialTest'.*|.*'ChiSquareTest'.*|.*'FisherExactTest'.*|.*'GTest'.*|.*'KolmogorovSmirnovTest'.*|.*'MannWhitneyUTest'.*|.*'TTest'.*|.*'WilcoxonSignedRankTest'.*|.*'UnconditionedExactTest'.*"/>
     </properties>
   </rule>
+  <rule ref="category/java/errorprone.xml/AvoidDecimalLiteralsInBigDecimalConstructor">
+    <properties>
+      <!-- Exact conversion from double to BigInteger is required. -->
+      <property name="violationSuppressXPath"
+        value="./ancestor-or-self::MethodDeclaration[@Name='getAsBigInteger']"/>
+    </properties>
+  </rule>
 
   <rule ref="category/java/performance.xml/AvoidArrayLoops">
     <properties>


(commons-statistics) 04/06: Move default StatisticResult implementations to IntMath

Posted by ah...@apache.org.
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

commit 8e1ec4cfb3ce498bde9f0908054ce2c3f9b36e07
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Thu Dec 21 20:38:05 2023 +0000

    Move default StatisticResult implementations to IntMath
---
 .../statistics/descriptive/StatisticResult.java    | 50 ++++++----------------
 .../commons/statistics/descriptive/Statistics.java | 29 -------------
 .../statistics/descriptive/IntMathTest.java        |  3 +-
 src/conf/pmd/pmd-ruleset.xml                       |  2 +-
 4 files changed, 17 insertions(+), 67 deletions(-)

diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StatisticResult.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StatisticResult.java
index 56108e7..79af4ca 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StatisticResult.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StatisticResult.java
@@ -16,7 +16,6 @@
  */
 package org.apache.commons.statistics.descriptive;
 
-import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.util.function.DoubleSupplier;
 import java.util.function.IntSupplier;
@@ -35,67 +34,46 @@ public interface StatisticResult extends DoubleSupplier, IntSupplier, LongSuppli
      * {@inheritDoc}
      *
      * <p>The default implementation uses the closest representable {@code int} value of
-     * the {@link #getAsDouble()} result. In the event of ties the result is towards
-     * positive infinity. This will raise an {@link ArithmeticException} if the closest
-     * integer result is not within the range {@code [-2^31, 2^31)}.
+     * the {@link #getAsDouble()} {@code result}. In the event of ties the result is
+     * rounded towards positive infinity. This will raise an {@link ArithmeticException}
+     * if the closest integer result is not within the range {@code [-2^31, 2^31)}.
      *
-     * @throws ArithmeticException if the {@code result} overflows an int, or is not
+     * @throws ArithmeticException if the {@code result} overflows an {@code int} or is not
      * finite
      */
     @Override
     default int getAsInt() {
-        // Note: Do not use (int) getAsLong() to avoid a narrowing primitive conversion.
-        final double x = getAsDouble();
-        final double r = Statistics.roundToInteger(x);
-        if (r >= -0x1.0p31 && r < 0x1.0p31) {
-            return (int) r;
-        }
-        throw new ArithmeticException("integer overflow: " + x);
+        return IntMath.toIntExact(getAsDouble());
     }
 
     /**
      * {@inheritDoc}
      *
      * <p>The default implementation uses the closest representable {@code long} value of
-     * the {@link #getAsDouble()} result. In the event of ties the result is rounded
-     * positive infinity. This will raise an {@link ArithmeticException} if the closest
-     * long integer result is not within the range {@code [-2^63, 2^63)}.
+     * the {@link #getAsDouble()} {@code result}. In the event of ties the result is
+     * rounded towards positive infinity. This will raise an {@link ArithmeticException}
+     * if the closest integer result is not within the range {@code [-2^63, 2^63)}.
      *
-     * @throws ArithmeticException if the {@code result} overflows a long, or is not
+     * @throws ArithmeticException if the {@code result} overflows a {@code long} or is not
      * finite
      */
     @Override
     default long getAsLong() {
-        final double x = getAsDouble();
-        final double r = Statistics.roundToInteger(x);
-        if (r >= -0x1.0p63 && r < 0x1.0p63) {
-            return (long) r;
-        }
-        throw new ArithmeticException("long integer overflow: " + x);
+        return IntMath.toLongExact(getAsDouble());
     }
 
     /**
      * Gets a result as a {@link BigInteger}.
      *
      * <p>The default implementation uses the closest representable {@code BigInteger}
-     * value of the {@link #getAsDouble()} result. In the event of ties the result is
-     * rounded positive infinity. This will raise an {@link ArithmeticException} if the
-     * result is not finite.
+     * value of the {@link #getAsDouble()} {@code result}. In the event of ties the result
+     * is rounded towards positive infinity. This will raise an
+     * {@link ArithmeticException} if the {@code result} is not finite.
      *
      * @return a result
      * @throws ArithmeticException if the {@code result} is not finite
      */
     default BigInteger getAsBigInteger() {
-        final double x = getAsDouble();
-        if (!Double.isFinite(x)) {
-            throw new ArithmeticException("BigInteger overflow: " + x);
-        }
-        final double r = Statistics.roundToInteger(x);
-        if (r >= -0x1.0p63 && r < 0x1.0p63) {
-            // Representable as a long
-            return BigInteger.valueOf((long) r);
-        }
-        // Large result
-        return new BigDecimal(r).toBigInteger();
+        return IntMath.toBigIntegerExact(getAsDouble());
     }
 }
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java
index 73c8a40..f43218c 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java
@@ -24,9 +24,6 @@ import java.util.function.LongConsumer;
  * Utility methods for statistics.
  */
 final class Statistics {
-    /** 0.5. */
-    private static final double HALF = 0.5;
-
     /** No instances. */
     private Statistics() {}
 
@@ -95,30 +92,4 @@ final class Statistics {
         // (1e-15 ~ 4.5 eps where eps = 2^-52).
         return m2 <= Math.pow(1e-15 * m1, 2);
     }
-
-    /**
-     * Get the whole number that is the nearest to x, with ties rounding towards positive infinity.
-     *
-     * <p>This method is intended to perform the equivalent of
-     * {@link Math#round(double)} without converting to a {@code long} primitive type.
-     * This allows the domain of the result to be checked against the range {@code [-2^63, 2^63)}.
-     *
-     * <p>Note: Adapted from {@code o.a.c.math4.AccurateMath.rint} and
-     * modified to perform rounding towards positive infinity.
-     *
-     * @param x Number from which nearest whole number is requested.
-     * @return a double number r such that r is an integer {@code r - 0.5 <= x < r + 0.5}
-     */
-    static double roundToInteger(double x) {
-        final double y = Math.floor(x);
-        final double d = x - y;
-        if (d >= HALF) {
-            // Here we do not preserve the sign of the operand in the case
-            // of -0.5 < x <= -0.0 since the rounded result is required as an integer.
-            // if y == -1.0:
-            //    return -0.0
-            return y + 1.0;
-        }
-        return y;
-    }
 }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMathTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMathTest.java
index c390041..09721ff 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMathTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMathTest.java
@@ -26,6 +26,7 @@ import java.util.stream.Stream;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.simple.RandomSource;
 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;
@@ -73,7 +74,7 @@ class IntMathTest {
             () -> String.format("%s * %s", bi1, bi2));
         final double x = expected.doubleValue();
         Assertions.assertEquals(x, IntMath.unsignedMultiplyToDouble(a, b),
-            () -> String.format("double2 %s * %s", bi1, bi2));
+            () -> String.format("double %s * %s", bi1, bi2));
     }
 
     static Stream<Arguments> testUnsignedMultiplyHigh() {
diff --git a/src/conf/pmd/pmd-ruleset.xml b/src/conf/pmd/pmd-ruleset.xml
index ff41429..72d7b54 100644
--- a/src/conf/pmd/pmd-ruleset.xml
+++ b/src/conf/pmd/pmd-ruleset.xml
@@ -212,7 +212,7 @@
     <properties>
       <!-- Exact conversion from double to BigInteger is required. -->
       <property name="violationSuppressXPath"
-        value="./ancestor-or-self::MethodDeclaration[@Name='getAsBigInteger']"/>
+        value="./ancestor-or-self::MethodDeclaration[@Name='toBigIntegerExact']"/>
     </properties>
   </rule>
 


(commons-statistics) 02/06: STATISTICS-81: Add integer min and max implementation

Posted by ah...@apache.org.
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

commit 3b2ae69e190675c63e22e3fa618df09bd0109719
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Tue Dec 19 10:11:07 2023 +0000

    STATISTICS-81: Add integer min and max implementation
    
    Added new interfaces for int and long statistics:
    IntStatistic and LongStatistic.
    
    Base tests have been created for each type using the format from
    BaseDoubleStatisticTest.
---
 .../descriptive/{Max.java => IntMax.java}          |  61 +-
 .../descriptive/{Min.java => IntMin.java}          |  61 +-
 .../statistics/descriptive/IntStatistic.java       |  28 +
 .../descriptive/{Max.java => LongMax.java}         |  74 ++-
 .../descriptive/{Min.java => LongMin.java}         |  70 ++-
 .../statistics/descriptive/LongStatistic.java      |  28 +
 .../apache/commons/statistics/descriptive/Max.java |   2 +-
 .../apache/commons/statistics/descriptive/Min.java |   2 +-
 .../descriptive/StatisticAccumulator.java          |  35 ++
 .../commons/statistics/descriptive/Statistics.java |  32 +
 .../descriptive/BaseDoubleStatisticTest.java       |   8 +-
 ...tatisticTest.java => BaseIntStatisticTest.java} | 690 ++++++++-------------
 ...atisticTest.java => BaseLongStatisticTest.java} | 690 ++++++++-------------
 .../statistics/descriptive/BaseStatisticTest.java  | 331 ++++++++++
 .../descriptive/BigIntegerStatisticResult.java     |  48 ++
 .../commons/statistics/descriptive/IntMaxTest.java |  73 +++
 .../commons/statistics/descriptive/IntMinTest.java |  73 +++
 .../statistics/descriptive/IntStatisticResult.java |  48 ++
 .../statistics/descriptive/LongMaxTest.java        |  73 +++
 .../statistics/descriptive/LongMinTest.java        |  73 +++
 .../descriptive/LongStatisticResult.java           |  48 ++
 .../commons/statistics/descriptive/ResultType.java |  31 +
 .../commons/statistics/descriptive/TestHelper.java | 206 ++++++
 src/conf/checkstyle/checkstyle-suppressions.xml    |   2 +
 24 files changed, 1841 insertions(+), 946 deletions(-)

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/IntMax.java
similarity index 65%
copy from commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Max.java
copy to commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMax.java
index 8ac52c6..77f68a6 100644
--- 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/IntMax.java
@@ -16,14 +16,14 @@
  */
 package org.apache.commons.statistics.descriptive;
 
+import java.math.BigInteger;
+
 /**
- * Returns the maximum of the available values. Uses {@link Math#max Math.max} as an
+ * Returns the maximum of the available values. Uses {@link Math#max(int, int) Math.max} as an
  * underlying function to compute the {@code maximum}.
  *
  * <ul>
- *   <li>The result is {@link Double#NEGATIVE_INFINITY negative infinity} if no values are added.
- *   <li>The result is {@code NaN} if any of the values is {@code NaN}.
- *   <li>The value {@code -0.0} is considered strictly smaller than {@code 0.0}.
+ *   <li>The result is {@link Integer#MIN_VALUE} if no values are added.
  * </ul>
  *
  * <p>This class is designed to work with (though does not require)
@@ -31,11 +31,11 @@ package org.apache.commons.statistics.descriptive;
  *
  * <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 {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * and at least one of the threads invokes the {@link java.util.function.IntConsumer#accept(int) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
- * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * <p>However, it is safe to use {@link java.util.function.IntConsumer#accept(int) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} 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()}
@@ -43,44 +43,42 @@ package org.apache.commons.statistics.descriptive;
  * safe and efficient parallel execution.
  *
  * @since 1.1
- * @see Math#max(double, double)
+ * @see Math#max(int, int)
  */
-public final class Max implements DoubleStatistic, DoubleStatisticAccumulator<Max> {
+public final class IntMax implements IntStatistic, StatisticAccumulator<IntMax> {
 
     /** Current maximum. */
-    private double maximum = Double.NEGATIVE_INFINITY;
+    private int maximum = Integer.MIN_VALUE;
 
     /**
      * Create an instance.
      */
-    private Max() {
+    private IntMax() {
         // No-op
     }
 
     /**
      * Creates an instance.
      *
-     * <p>The initial result is {@link Double#NEGATIVE_INFINITY negative infinity}.
+     * <p>The initial result is {@link Integer#MIN_VALUE}.
      *
      * @return {@code Max} instance.
      */
-    public static Max create() {
-        return new Max();
+    public static IntMax create() {
+        return new IntMax();
     }
 
     /**
      * Returns an instance populated using the input {@code values}.
      *
-     * <p>The result is {@code NaN} if any of the values is {@code NaN}.
-     *
      * <p>When the input is an empty array, the result is
-     * {@link Double#NEGATIVE_INFINITY negative infinity}.
+     * {@link Integer#MIN_VALUE}.
      *
      * @param values Values.
      * @return {@code Max} instance.
      */
-    public static Max of(double... values) {
-        return Statistics.add(new Max(), values);
+    public static IntMax of(int... values) {
+        return Statistics.add(new IntMax(), values);
     }
 
     /**
@@ -89,7 +87,7 @@ public final class Max implements DoubleStatistic, DoubleStatisticAccumulator<Ma
      * @param value Value.
      */
     @Override
-    public void accept(double value) {
+    public void accept(int value) {
         maximum = Math.max(maximum, value);
     }
 
@@ -97,18 +95,33 @@ public final class Max implements DoubleStatistic, DoubleStatisticAccumulator<Ma
      * Gets the maximum of all input values.
      *
      * <p>When no values have been added, the result is
-     * {@link Double#NEGATIVE_INFINITY negative infinity}.
+     * {@link Integer#MIN_VALUE}.
      *
      * @return maximum of all values.
      */
+    @Override
+    public int getAsInt() {
+        return maximum;
+    }
+
+    @Override
+    public long getAsLong() {
+        return maximum;
+    }
+
     @Override
     public double getAsDouble() {
         return maximum;
     }
 
     @Override
-    public Max combine(Max other) {
-        accept(other.getAsDouble());
+    public BigInteger getAsBigInteger() {
+        return BigInteger.valueOf(maximum);
+    }
+
+    @Override
+    public IntMax combine(IntMax other) {
+        accept(other.getAsInt());
         return this;
     }
 }
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMin.java
similarity index 65%
copy from commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java
copy to commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMin.java
index a7f923f..83ca12a 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMin.java
@@ -16,14 +16,14 @@
  */
 package org.apache.commons.statistics.descriptive;
 
+import java.math.BigInteger;
+
 /**
- * Returns the minimum of the available values. Uses {@link Math#min Math.min} as an
+ * Returns the minimum of the available values. Uses {@link Math#min(int, int) Math.min} as an
  * underlying function to compute the {@code minimum}.
  *
  * <ul>
- *   <li>The result is {@link Double#POSITIVE_INFINITY positive infinity} if no values are added.
- *   <li>The result is {@code NaN} if any of the values is {@code NaN}.
- *   <li>The value {@code -0.0} is considered strictly smaller than {@code 0.0}.
+ *   <li>The result is {@link Integer#MAX_VALUE} if no values are added.
  * </ul>
  *
  * <p>This class is designed to work with (though does not require)
@@ -31,11 +31,11 @@ package org.apache.commons.statistics.descriptive;
  *
  * <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 {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * and at least one of the threads invokes the {@link java.util.function.IntConsumer#accept(int) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
- * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * <p>However, it is safe to use {@link java.util.function.IntConsumer#accept(int) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} 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()}
@@ -43,44 +43,42 @@ package org.apache.commons.statistics.descriptive;
  * safe and efficient parallel execution.
  *
  * @since 1.1
- * @see Math#min(double, double)
+ * @see Math#min(int, int)
  */
-public final class Min implements DoubleStatistic, DoubleStatisticAccumulator<Min> {
+public final class IntMin implements IntStatistic, StatisticAccumulator<IntMin> {
 
     /** Current minimum. */
-    private double minimum = Double.POSITIVE_INFINITY;
+    private int minimum = Integer.MAX_VALUE;
 
     /**
      * Create an instance.
      */
-    private Min() {
+    private IntMin() {
         // No-op
     }
 
     /**
      * Creates an instance.
      *
-     * <p>The initial result is {@link Double#POSITIVE_INFINITY positive infinity}.
+     * <p>The initial result is {@link Integer#MAX_VALUE}.
      *
      * @return {@code Min} instance.
      */
-    public static Min create() {
-        return new Min();
+    public static IntMin create() {
+        return new IntMin();
     }
 
     /**
      * Returns an instance populated using the input {@code values}.
      *
-     * <p>The result is {@code NaN} if any of the values is {@code NaN}.
-     *
      * <p>When the input is an empty array, the result is
-     * {@link Double#POSITIVE_INFINITY positive infinity}.
+     * {@link Integer#MAX_VALUE}.
      *
      * @param values Values.
      * @return {@code Min} instance.
      */
-    public static Min of(double... values) {
-        return Statistics.add(new Min(), values);
+    public static IntMin of(int... values) {
+        return Statistics.add(new IntMin(), values);
     }
 
     /**
@@ -89,7 +87,7 @@ public final class Min implements DoubleStatistic, DoubleStatisticAccumulator<Mi
      * @param value Value.
      */
     @Override
-    public void accept(double value) {
+    public void accept(int value) {
         minimum = Math.min(minimum, value);
     }
 
@@ -97,18 +95,33 @@ public final class Min implements DoubleStatistic, DoubleStatisticAccumulator<Mi
      * Gets the minimum of all input values.
      *
      * <p>When no values have been added, the result is
-     * {@link Double#POSITIVE_INFINITY positive infinity}.
+     * {@link Integer#MAX_VALUE}.
      *
      * @return minimum of all values.
      */
+    @Override
+    public int getAsInt() {
+        return minimum;
+    }
+
+    @Override
+    public long getAsLong() {
+        return minimum;
+    }
+
     @Override
     public double getAsDouble() {
         return minimum;
     }
 
     @Override
-    public Min combine(Min other) {
-        accept(other.getAsDouble());
+    public BigInteger getAsBigInteger() {
+        return BigInteger.valueOf(minimum);
+    }
+
+    @Override
+    public IntMin combine(IntMin other) {
+        accept(other.getAsInt());
         return this;
     }
 }
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntStatistic.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntStatistic.java
new file mode 100644
index 0000000..24d53f7
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntStatistic.java
@@ -0,0 +1,28 @@
+/*
+ * 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.function.IntConsumer;
+
+/**
+ * Represents a state object for computing a statistic over {@code int} valued input(s).
+ *
+ * @since 1.1
+ */
+public interface IntStatistic extends IntConsumer, StatisticResult {
+    // Composite interface
+}
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/LongMax.java
similarity index 60%
copy from commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Max.java
copy to commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongMax.java
index 8ac52c6..5eab331 100644
--- 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/LongMax.java
@@ -16,14 +16,14 @@
  */
 package org.apache.commons.statistics.descriptive;
 
+import java.math.BigInteger;
+
 /**
- * Returns the maximum of the available values. Uses {@link Math#max Math.max} as an
+ * Returns the maximum of the available values. Uses {@link Math#max(long, long) Math.max} as an
  * underlying function to compute the {@code maximum}.
  *
  * <ul>
- *   <li>The result is {@link Double#NEGATIVE_INFINITY negative infinity} if no values are added.
- *   <li>The result is {@code NaN} if any of the values is {@code NaN}.
- *   <li>The value {@code -0.0} is considered strictly smaller than {@code 0.0}.
+ *   <li>The result is {@link Long#MIN_VALUE} if no values are added.
  * </ul>
  *
  * <p>This class is designed to work with (though does not require)
@@ -31,11 +31,11 @@ package org.apache.commons.statistics.descriptive;
  *
  * <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 {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * and at least one of the threads invokes the {@link java.util.function.LongConsumer#accept(long) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
- * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * <p>However, it is safe to use {@link java.util.function.LongConsumer#accept(long) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} 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()}
@@ -43,44 +43,42 @@ package org.apache.commons.statistics.descriptive;
  * safe and efficient parallel execution.
  *
  * @since 1.1
- * @see Math#max(double, double)
+ * @see Math#max(long, long)
  */
-public final class Max implements DoubleStatistic, DoubleStatisticAccumulator<Max> {
+public final class LongMax implements LongStatistic, StatisticAccumulator<LongMax> {
 
     /** Current maximum. */
-    private double maximum = Double.NEGATIVE_INFINITY;
+    private long maximum = Long.MIN_VALUE;
 
     /**
      * Create an instance.
      */
-    private Max() {
+    private LongMax() {
         // No-op
     }
 
     /**
      * Creates an instance.
      *
-     * <p>The initial result is {@link Double#NEGATIVE_INFINITY negative infinity}.
+     * <p>The initial result is {@link Long#MIN_VALUE}.
      *
-     * @return {@code Max} instance.
+     * @return {@code Min} instance.
      */
-    public static Max create() {
-        return new Max();
+    public static LongMax create() {
+        return new LongMax();
     }
 
     /**
      * Returns an instance populated using the input {@code values}.
      *
-     * <p>The result is {@code NaN} if any of the values is {@code NaN}.
-     *
      * <p>When the input is an empty array, the result is
-     * {@link Double#NEGATIVE_INFINITY negative infinity}.
+     * {@link Long#MIN_VALUE}.
      *
      * @param values Values.
-     * @return {@code Max} instance.
+     * @return {@code Min} instance.
      */
-    public static Max of(double... values) {
-        return Statistics.add(new Max(), values);
+    public static LongMax of(long... values) {
+        return Statistics.add(new LongMax(), values);
     }
 
     /**
@@ -89,7 +87,7 @@ public final class Max implements DoubleStatistic, DoubleStatisticAccumulator<Ma
      * @param value Value.
      */
     @Override
-    public void accept(double value) {
+    public void accept(long value) {
         maximum = Math.max(maximum, value);
     }
 
@@ -97,18 +95,42 @@ public final class Max implements DoubleStatistic, DoubleStatisticAccumulator<Ma
      * Gets the maximum of all input values.
      *
      * <p>When no values have been added, the result is
-     * {@link Double#NEGATIVE_INFINITY negative infinity}.
+     * {@link Long#MIN_VALUE}.
+     *
+     * @return maximum of all values.
+     */
+    @Override
+    public long getAsLong() {
+        return maximum;
+    }
+
+    /**
+     * Gets the maximum of all input values.
+     *
+     * <p>This method will throw an {@link ArithmeticException} if the {@code long}
+     * maximum overflows an {@code int}; or no values have been added.
      *
      * @return maximum of all values.
+     * @see Math#toIntExact(long)
      */
+    @Override
+    public int getAsInt() {
+        return Math.toIntExact(maximum);
+    }
+
     @Override
     public double getAsDouble() {
         return maximum;
     }
 
     @Override
-    public Max combine(Max other) {
-        accept(other.getAsDouble());
+    public BigInteger getAsBigInteger() {
+        return BigInteger.valueOf(maximum);
+    }
+
+    @Override
+    public LongMax combine(LongMax other) {
+        accept(other.getAsLong());
         return this;
     }
 }
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongMin.java
similarity index 61%
copy from commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java
copy to commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongMin.java
index a7f923f..cb9df20 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongMin.java
@@ -16,14 +16,14 @@
  */
 package org.apache.commons.statistics.descriptive;
 
+import java.math.BigInteger;
+
 /**
- * Returns the minimum of the available values. Uses {@link Math#min Math.min} as an
+ * Returns the minimum of the available values. Uses {@link Math#min(long, long) Math.min} as an
  * underlying function to compute the {@code minimum}.
  *
  * <ul>
- *   <li>The result is {@link Double#POSITIVE_INFINITY positive infinity} if no values are added.
- *   <li>The result is {@code NaN} if any of the values is {@code NaN}.
- *   <li>The value {@code -0.0} is considered strictly smaller than {@code 0.0}.
+ *   <li>The result is {@link Long#MAX_VALUE} if no values are added.
  * </ul>
  *
  * <p>This class is designed to work with (though does not require)
@@ -31,11 +31,11 @@ package org.apache.commons.statistics.descriptive;
  *
  * <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 {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * and at least one of the threads invokes the {@link java.util.function.LongConsumer#accept(long) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
- * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * <p>However, it is safe to use {@link java.util.function.LongConsumer#accept(long) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} 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()}
@@ -43,44 +43,42 @@ package org.apache.commons.statistics.descriptive;
  * safe and efficient parallel execution.
  *
  * @since 1.1
- * @see Math#min(double, double)
+ * @see Math#min(long, long)
  */
-public final class Min implements DoubleStatistic, DoubleStatisticAccumulator<Min> {
+public final class LongMin implements LongStatistic, StatisticAccumulator<LongMin> {
 
     /** Current minimum. */
-    private double minimum = Double.POSITIVE_INFINITY;
+    private long minimum = Long.MAX_VALUE;
 
     /**
      * Create an instance.
      */
-    private Min() {
+    private LongMin() {
         // No-op
     }
 
     /**
      * Creates an instance.
      *
-     * <p>The initial result is {@link Double#POSITIVE_INFINITY positive infinity}.
+     * <p>The initial result is {@link Long#MAX_VALUE}.
      *
      * @return {@code Min} instance.
      */
-    public static Min create() {
-        return new Min();
+    public static LongMin create() {
+        return new LongMin();
     }
 
     /**
      * Returns an instance populated using the input {@code values}.
      *
-     * <p>The result is {@code NaN} if any of the values is {@code NaN}.
-     *
      * <p>When the input is an empty array, the result is
-     * {@link Double#POSITIVE_INFINITY positive infinity}.
+     * {@link Long#MAX_VALUE}.
      *
      * @param values Values.
      * @return {@code Min} instance.
      */
-    public static Min of(double... values) {
-        return Statistics.add(new Min(), values);
+    public static LongMin of(long... values) {
+        return Statistics.add(new LongMin(), values);
     }
 
     /**
@@ -89,7 +87,7 @@ public final class Min implements DoubleStatistic, DoubleStatisticAccumulator<Mi
      * @param value Value.
      */
     @Override
-    public void accept(double value) {
+    public void accept(long value) {
         minimum = Math.min(minimum, value);
     }
 
@@ -97,18 +95,42 @@ public final class Min implements DoubleStatistic, DoubleStatisticAccumulator<Mi
      * Gets the minimum of all input values.
      *
      * <p>When no values have been added, the result is
-     * {@link Double#POSITIVE_INFINITY positive infinity}.
+     * {@link Long#MAX_VALUE}.
+     *
+     * @return minimum of all values.
+     */
+    @Override
+    public long getAsLong() {
+        return minimum;
+    }
+
+    /**
+     * Gets the minimum of all input values.
+     *
+     * <p>This method will throw an {@link ArithmeticException} if the {@code long}
+     * minimum overflows an {@code int}; or no values have been added.
      *
      * @return minimum of all values.
+     * @see Math#toIntExact(long)
      */
+    @Override
+    public int getAsInt() {
+        return Math.toIntExact(minimum);
+    }
+
     @Override
     public double getAsDouble() {
         return minimum;
     }
 
     @Override
-    public Min combine(Min other) {
-        accept(other.getAsDouble());
+    public BigInteger getAsBigInteger() {
+        return BigInteger.valueOf(minimum);
+    }
+
+    @Override
+    public LongMin combine(LongMin other) {
+        accept(other.getAsLong());
         return this;
     }
 }
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongStatistic.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongStatistic.java
new file mode 100644
index 0000000..95d0f06
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongStatistic.java
@@ -0,0 +1,28 @@
+/*
+ * 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.function.LongConsumer;
+
+/**
+ * Represents a state object for computing a statistic over {@code long} valued input(s).
+ *
+ * @since 1.1
+ */
+public interface LongStatistic extends LongConsumer, StatisticResult {
+    // Composite interface
+}
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
index 8ac52c6..16709da 100644
--- 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
@@ -17,7 +17,7 @@
 package org.apache.commons.statistics.descriptive;
 
 /**
- * Returns the maximum of the available values. Uses {@link Math#max Math.max} as an
+ * Returns the maximum of the available values. Uses {@link Math#max(double, double) Math.max} as an
  * underlying function to compute the {@code maximum}.
  *
  * <ul>
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java
index a7f923f..19f301f 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java
@@ -17,7 +17,7 @@
 package org.apache.commons.statistics.descriptive;
 
 /**
- * Returns the minimum of the available values. Uses {@link Math#min Math.min} as an
+ * Returns the minimum of the available values. Uses {@link Math#min(double, double) Math.min} as an
  * underlying function to compute the {@code minimum}.
  *
  * <ul>
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StatisticAccumulator.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StatisticAccumulator.java
new file mode 100644
index 0000000..693396d
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StatisticAccumulator.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * A mutable result container that accumulates a {@link StatisticResult}.
+ *
+ * @param <T> {@link StatisticResult} being accumulated.
+ *
+ * @since 1.1
+ */
+public interface StatisticAccumulator<T extends StatisticResult> {
+
+    /**
+     * Combines the state of the {@code other} statistic into this one.
+     *
+     * @param other Another statistic to be combined.
+     * @return {@code this} instance after combining {@code other}.
+     */
+    T combine(T other);
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java
index 34d5df8..73c8a40 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Statistics.java
@@ -17,6 +17,8 @@
 package org.apache.commons.statistics.descriptive;
 
 import java.util.function.DoubleConsumer;
+import java.util.function.IntConsumer;
+import java.util.function.LongConsumer;
 
 /**
  * Utility methods for statistics.
@@ -43,6 +45,36 @@ final class Statistics {
         return statistic;
     }
 
+    /**
+     * Add all the {@code values} to the {@code statistic}.
+     *
+     * @param <T> Type of the statistic
+     * @param statistic Statistic.
+     * @param values Values.
+     * @return the statistic
+     */
+    static <T extends IntConsumer> T add(T statistic, int[] values) {
+        for (final int x : values) {
+            statistic.accept(x);
+        }
+        return statistic;
+    }
+
+    /**
+     * Add all the {@code values} to the {@code statistic}.
+     *
+     * @param <T> Type of the statistic
+     * @param statistic Statistic.
+     * @param values Values.
+     * @return the statistic
+     */
+    static <T extends LongConsumer> T add(T statistic, long[] values) {
+        for (final long x : values) {
+            statistic.accept(x);
+        }
+        return statistic;
+    }
+
     /**
      * Returns {@code true} if the second central moment {@code m2} is effectively
      * zero given the magnitude of the first raw moment {@code m1}.
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
index 6e32115..bdda305 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
@@ -57,7 +57,7 @@ import org.junit.jupiter.params.provider.MethodSource;
  *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
  * </ol>
  *
- * <p>An test implementation has to provide these method implementations:
+ * <p>A test implementation has to provide these method implementations:
  *
  * <ul>
  *  <li>{@link #create()}: Create an empty statistic.
@@ -208,8 +208,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
         private final Double expected;
 
         /**
-         * @param expected Expected statistic value.
          * @param values Sample values.
+         * @param expected Expected statistic value.
          */
         TestData(double[] values, double expected) {
             this.values = values;
@@ -425,7 +425,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * Map the {@code value} to the valid domain of the statistic. This method is called
      * with the example data before {@link #getExpectedValue(double[])} and
      * {@link #getExpectedNonFiniteValue(double[])}. In the later case the method is only
-     * called with finite value. It can be used by implementing classes to adjust the data
+     * called with finite values. It can be used by implementing classes to adjust the data
      * to a valid domain, for example if computing the sum-of-logs all values can be
      * updated to be strictly positive.
      *
@@ -1373,7 +1373,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param seed Seed.
      * @param repeat Repeat of the total random permutations.
      */
-    private void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
+    private static void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
         throw new AssertionError(String.format("%s; Seed=%s; Repeat=%d/%d",
             e.getMessage(), Arrays.toString(seed), repeat, RANDOM_PERMUTATIONS), e);
     }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseIntStatisticTest.java
similarity index 60%
copy from commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
copy to commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseIntStatisticTest.java
index 6e32115..7b05937 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseIntStatisticTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.statistics.descriptive;
 
+import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -26,7 +27,6 @@ import java.util.stream.Stream;
 import java.util.stream.Stream.Builder;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.statistics.distribution.DoubleTolerance;
-import org.apache.commons.statistics.distribution.DoubleTolerances;
 import org.apache.commons.statistics.distribution.TestUtils;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
@@ -39,36 +39,43 @@ import org.junit.jupiter.params.provider.MethodSource;
 
 /**
  * Abstract base class for statistic tests.
- * This supports testing statistics that implement {@link DoubleStatistic} and
- * {@link DoubleStatisticAccumulator}.
+ * This supports testing statistics that implement {@link IntStatistic} and
+ * {@link StatisticAccumulator}.
  *
  * <p>This class uses parameterized tests that are repeated for instances of a
- * statistic. The statistic is tested using standard finite and non-finite data.
+ * statistic. The statistic is tested using standard data.
  * Additional data may be provided by implementations.
  *
  * <p>There are 4 main tests for a statistic populated with values using:
  *
  * <ol>
- *  <li>{@link java.util.function.DoubleConsumer#accept accept}
- *  <li>{@link java.util.function.DoubleConsumer#accept accept} and
- *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
- *  <li>{@link #create(double...)}
- *  <li>{@link #create(double...)} and
- *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ *  <li>{@link java.util.function.IntConsumer#accept accept}
+ *  <li>{@link java.util.function.IntConsumer#accept accept} and
+ *      {@link StatisticAccumulator#combine(StatisticResult) combine}
+ *  <li>{@link #create(int...)}
+ *  <li>{@link #create(int...)} and
+ *      {@link StatisticAccumulator#combine(StatisticResult) combine}
  * </ol>
  *
- * <p>An test implementation has to provide these method implementations:
+ * <p>Note: Integer statistics may return the expected result as a {@code double}, {@code int},
+ * {@code long}, or {@code BigInteger}. The default is {@code double}.
+ *
+ * <p>A test implementation has to provide these method implementations:
  *
  * <ul>
+ *  <li>{@link #getResultType()}: Get the native result type of the statistic.
+ *      This is used to verify that test cases are added using the correct type and
+ *      test tolerances are configured for {@code double} result types.
  *  <li>{@link #create()}: Create an empty statistic.
- *  <li>{@link #create(double...)}: Create a statistic from a set of values.
+ *  <li>{@link #create(int...)}: Create a statistic from a set of values.
  *  <li>{@link #getEmptyValue()}: The expected value of a statistic when not enough values have
  *      been observed. The minimum number of values can be provided in {@link #getEmptySize()}.
- *  <li>{@link #getExpectedValue(double[])}: A method to compute an expected value for the
+ *  <li>{@link #getExpectedValue(int[])}: A method to compute an expected value for the
  *      statistic. This is used to create the expected value on standard test data. It should
  *      be an implementation of the canonical definition of the statistic. It can use extended
  *      precision to provided an accurate expected result.
- *  <li>{@link #getTolerance()}: The test tolerance for all tests. This method is not used
+ *  <li>{@link #getTolerance()}: The test tolerance for all tests when the expected value is
+ *      a {@code double}. Tolerances are not used for integer results. This method is not used
  *      directly by the base class for test cases. It provides the default implementation for
  *      the 4 methods to provide the tolerance for each of the 4 main test cases.
  *      <em>Note that the default implementation throws an exception</em>. This forces the
@@ -77,12 +84,6 @@ import org.junit.jupiter.params.provider.MethodSource;
  *      precision for creation of the statistic from a stream of values or an array then
  *      it can override {@link #getToleranceAccept()}, {@link #getToleranceArray()} and
  *      their combine equivalents.
- *  <li>{@link #getExpectedNonFiniteValue(double[])}: A method to compute an expected value
- *      for the statistic when a non-finite value is encountered. This is only used with at
- *      least one non-finite value. Typically this should return {@code NaN} or an infinity.
- *      Data tested with non-finite values will use {@link #getToleranceNonFinite()} which
- *      defaults to an exact binary equality. This may be overridden for example to return
- *      a tolerance that matches any non-finite value.
  *  <li>{@link #streamTestData()}: Provide custom data for the test. It is
  *      <em>strongly recommended</em> to provide reference data from independent implementations.
  *      Edge case data for the statistic can also be provided. See the method javadoc for
@@ -96,7 +97,7 @@ import org.junit.jupiter.params.provider.MethodSource;
  *      The default uses the class name without the "Test" suffix.
  *  <li>{@link #isCombineSymmetric()}: Specify whether to test {@code a.combine(b)} is exactly
  *      equal to {@code b.combine(a)}. The default is {@code true}.
- *  <li>{@link #mapValue(double)}: A method to update the sample data to the valid domain
+ *  <li>{@link #mapValue(int)}: A method to update the sample data to the valid domain
  *      for the statistic. This can be used to alter the default test data, for example
  *      by mapping any negative values to positive.
  * </ul>
@@ -107,66 +108,36 @@ import org.junit.jupiter.params.provider.MethodSource;
  * @param <S> Statistic type
  */
 @TestInstance(Lifecycle.PER_CLASS)
-abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatisticAccumulator<S>> {
-    /** An empty {@code double[]}. */
-    private static final double[] EMPTY = {};
-    /** The number of random permutations to perform. */
-    private static final int RANDOM_PERMUTATIONS = 5;
-    /** The maximum number of values to output in assertion messages. This is used to provide
-     * the data for the failed test which is useful to identify the failed test case. */
-    private static final int MAX_FORMAT_VALUES = 100;
+abstract class BaseIntStatisticTest<S extends IntStatistic & StatisticAccumulator<S>>
+        extends BaseStatisticTest {
+    /** An empty {@code int[]}. */
+    private static final int[] EMPTY = {};
 
     // Standard test data samples using SciPy version 1.11:
     // import scipy.stats
 
     /** Uniform samples: scipy.stats.randint.rvs(-15, 25, size=100).
      * https://en.wikipedia.org/wiki/Discrete_uniform_distribution */
-    private static final double[] V1 = {20, 8, 1, 2, 24, -13, -1, 19, 0, 23, 14, 2, -9, 0, 7, -15, -2, -9, -1, 12, -12,
+    private static final int[] V1 = {20, 8, 1, 2, 24, -13, -1, 19, 0, 23, 14, 2, -9, 0, 7, -15, -2, -9, -1, 12, -12,
         9, 2, 12, 0, 5, 18, 15, -10, 6, 14, 6, 24, 12, -2, 1, -9, 17, 10, 7, 4, -8, 7, 7, -9, 13, -13, 0, -13, 6, 10,
         -6, 23, 2, 6, -10, 12, 6, -6, 4, -5, -9, 5, 16, -14, -4, 22, -4, 20, 12, -13, 6, -13, 12, -3, 13, 1, -6, 13,
         -14, -8, 4, 24, 10, 22, 8, -3, -5, 6, 9, 2, 16, 20, -8, -5, 23, 19, 20, -10, 21};
     /** Poisson samples: scipy.stats.poisson.rvs(1.5, size=100).
      * https://en.wikipedia.org/wiki/Poisson_distribution */
-    private static final double[] V2 = {3, 0, 3, 4, 0, 1, 1, 3, 2, 1, 3, 1, 5, 0, 0, 0, 3, 3, 2, 0, 3, 1, 2, 1, 1, 0, 0,
+    private static final int[] V2 = {3, 0, 3, 4, 0, 1, 1, 3, 2, 1, 3, 1, 5, 0, 0, 0, 3, 3, 2, 0, 3, 1, 2, 1, 1, 0, 0,
         1, 0, 1, 3, 0, 0, 2, 2, 0, 2, 1, 2, 1, 3, 2, 1, 2, 0, 2, 2, 1, 2, 0, 3, 2, 0, 1, 2, 2, 1, 2, 1, 0, 2, 0, 2, 2,
         3, 2, 1, 1, 0, 1, 2, 1, 0, 0, 6, 2, 0, 1, 1, 1, 1, 2, 2, 2, 2, 0, 2, 1, 2, 4, 2, 4, 2, 0, 0, 1, 1, 2, 2, 3};
     /** Poisson samples: scipy.stats.poisson.rvs(4.5, size=100). */
-    private static final double[] V3 = {1, 5, 5, 2, 1, 7, 5, 5, 3, 5, 5, 2, 4, 4, 6, 2, 7, 5, 3, 6, 7, 3, 5, 3, 7, 6, 4,
+    private static final int[] V3 = {1, 5, 5, 2, 1, 7, 5, 5, 3, 5, 5, 2, 4, 4, 6, 2, 7, 5, 3, 6, 7, 3, 5, 3, 7, 6, 4,
         3, 3, 3, 3, 3, 5, 3, 2, 8, 2, 4, 3, 1, 2, 5, 4, 3, 5, 4, 4, 8, 6, 2, 4, 3, 5, 5, 6, 3, 1, 6, 8, 6, 3, 6, 10, 4,
         5, 2, 4, 2, 2, 8, 2, 2, 4, 1, 4, 2, 5, 3, 2, 4, 4, 6, 9, 4, 5, 9, 9, 6, 3, 5, 3, 3, 5, 7, 5, 2, 3, 7, 4, 5};
     /** Poisson samples: scipy.stats.poisson.rvs(45, size=100). */
-    private static final double[] V4 = {42, 51, 38, 38, 49, 48, 42, 47, 51, 46, 45, 35, 39, 42, 49, 55, 53, 46, 49, 56,
+    private static final int[] V4 = {42, 51, 38, 38, 49, 48, 42, 47, 51, 46, 45, 35, 39, 42, 49, 55, 53, 46, 49, 56,
         42, 46, 42, 53, 43, 55, 49, 52, 51, 45, 40, 49, 39, 40, 46, 43, 46, 48, 36, 44, 40, 49, 49, 43, 45, 44, 41, 55,
         52, 45, 57, 41, 43, 44, 38, 52, 44, 45, 43, 42, 38, 37, 47, 42, 47, 45, 70, 45, 50, 47, 46, 50, 47, 35, 43, 52,
         51, 41, 45, 42, 45, 53, 46, 48, 51, 43, 63, 48, 49, 41, 58, 51, 59, 43, 39, 32, 35, 46, 50, 50};
-    /** Normal samples: scipy.stats.norm.rvs(loc=3.4, scale=2.25, size=100). */
-    private static final double[] V5 = {1.06356579, -1.52552007, 7.09739891, -0.41516549, 0.17131653, 0.77923148,
-        2.90491862, 4.12648256, 5.04920689, 4.20053484, 5.83485097, 4.33138009, 4.18795702, 3.269289, 2.2399589,
-        4.16551591, -1.67192439, 1.44919254, 3.52270229, -1.49186865, -0.30794835, 5.82394621, 4.84755567, 4.79622486,
-        5.12461983, 2.62561931, 5.12457788, 8.24460895, 4.91249002, 3.75550863, 4.35440479, 4.17587334, -0.34934393,
-        2.98071452, -1.35620308, 1.93956508, 7.57171999, 5.41976186, 2.8427556, 3.04101193, 2.20374721, 4.65406057,
-        5.76961878, 3.14412957, 7.60322297, 1.598286, 2.51552974, 0.67767289, 0.76514432, 3.65663671, 0.53116457,
-        2.79439061, 7.58564809, 4.16735822, 2.95210392, 6.37867376, 6.57010411, 0.11837698, 9.16270054, 3.80097588,
-        5.48811672, 3.83378268, 2.03669252, 5.34865676, 3.11338528, 4.70088345, 6.00069684, 0.16144587, 4.22654482,
-        2.2722623, 5.39142224, 0.811471, 2.74523433, 6.32457234, 0.73033045, 9.54402353, 0.4800466, 2.00806359,
-        6.06115109, 2.3072464, 5.40974674, 2.05533169, 0.97160161, 8.06915145, 4.40792026, 4.53139251, 3.32350119,
-        1.53645238, 3.49059212, 3.57904997, 0.58634639, 5.87567911, 3.49424866, 5.72228178, 4.41403447, 1.27815121,
-        7.13861948, 4.68209093, 6.4598438, 0.66270586};
-    /** Gamma samples: scipy.stats.gamma.rvs(9, scale=0.5, size=100). */
-    private static final double[] V6 = {3.46479451, 8.80950045, 3.91437318, 4.23327834, 2.6910161, 4.51122052,
-        5.81939474, 3.9142699, 7.75537607, 6.06693317, 3.29388792, 3.90689471, 3.26357137, 3.6398822, 5.60048428,
-        3.68248997, 5.09297897, 4.6302593, 7.01654777, 4.2244833, 2.75326355, 5.36988549, 2.88392811, 3.50131464,
-        4.81183009, 4.92155284, 4.37061644, 3.8064197, 3.31941113, 5.01257676, 3.48037207, 2.62777255, 6.2447332,
-        6.18425783, 3.06915179, 6.42851381, 3.8969583, 3.48723372, 3.49516941, 2.90404439, 2.25920041, 3.68515649,
-        5.09607663, 3.18984299, 2.49261713, 3.9345895, 6.01480539, 8.8065787, 4.3464082, 5.03522483, 4.05315513,
-        6.07365399, 4.34804323, 5.16061656, 3.24706079, 2.89888437, 6.24575902, 3.10893227, 3.74196045, 3.94099137,
-        3.33951846, 4.9264514, 5.21935748, 5.06904776, 2.77543623, 3.72451685, 6.35546017, 4.42425655, 5.99568005,
-        4.2602446, 3.75834066, 5.17730802, 3.60682583, 4.09703419, 5.59942582, 3.49191032, 3.02164323, 8.85183547,
-        5.58631958, 3.24891648, 4.03267796, 4.30984912, 3.78187375, 5.98969913, 1.68855026, 8.43117397, 3.808258,
-        4.82043745, 2.91020117, 4.12921107, 3.54350667, 4.60545934, 5.63180941, 5.07331453, 5.04419517, 3.78796082,
-        4.25143811, 6.3242129, 4.25630677, 4.59158821};
-
-    /** Sizes to cut the sample data into chunks. The {@link #cut(double[], int...)} method is robust
+
+    /** Sizes to cut the sample data into chunks. The {@link #cut(int[], int...)} method is robust
      * to the sizes summing to more than the sample length. If they sum below then the expected
      * value may be incorrect as all data is not used. These all sum to 100 which imposes a maximum
      * length on sample data. */
@@ -180,40 +151,43 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
         {10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
     };
 
-    /** Sizes to cut the non-finite sample data into chunks.
+    /** Sizes to cut the extreme value sample data into chunks.
      * These all sum to 10 which imposes a maximum length on sample data. */
-    private static final int[][] SIZES_NON_FINITE = {
+    private static final int[][] SIZES_EXTREME = {
         {5, 5},
         {3, 3, 4},
         {2, 2, 2, 2, 2},
     };
 
+    /** The statistic name. */
+    private String statisticName;
+
     /** The test data and expected values. */
     private final List<TestData> data = new ArrayList<>();
-    /** The non-finite test data and expected values. */
-    private final List<TestData> dataNonFinite = new ArrayList<>();
+    /** The extreme value test data and expected values. */
+    private final List<TestData> dataExtremeValue = new ArrayList<>();
     /** Cache of the custom test data. */
     private final List<StatisticTestData> dataCustom = new ArrayList<>();
 
-    /** The statistic name. */
-    private String statisticName;
-
     /**
      * Container for test data.
+     *
+     * <p>The statistics is expected to be either a {@code double}, {@code int}, {@code long}
+     * of {@code BigInteger} value.
      */
     private static class TestData {
         /** The sample values. */
-        private final double[] values;
+        private final int[] values;
         /** The expected statistic value. */
-        private final Double expected;
+        private final StatisticResult expected;
 
         /**
-         * @param expected Expected statistic value.
          * @param values Sample values.
+         * @param expected Expected statistic value.
          */
-        TestData(double[] values, double expected) {
+        TestData(int[] values, StatisticResult expected) {
             this.values = values;
-            this.expected = Double.valueOf(expected);
+            this.expected = expected;
         }
 
         /**
@@ -221,7 +195,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
          *
          * @return the values
          */
-        double[] getValues() {
+        int[] getValues() {
             return values.clone();
         }
 
@@ -230,13 +204,14 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
          *
          * @return the expected
          */
-        Double getExpected() {
+        StatisticResult getExpected() {
             return expected;
         }
     }
 
     /**
-     * Container for test data with tolerances for the tests.
+     * Container for test data with tolerances for the tests. Tolerances are used only
+     * for expected values as a {@code double}.
      */
     static class StatisticTestData extends TestData {
         /** The tolerance for the accept test. */
@@ -248,12 +223,24 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
         /** The tolerance for the array+combine test. */
         private final DoubleTolerance tolArrayCombine;
 
+        /**
+         * @param values Sample values.
+         * @param expected Expected statistic value.
+         */
+        StatisticTestData(int[] values, StatisticResult expected) {
+            super(values, expected);
+            this.tolAccept = null;
+            this.tolArray = null;
+            this.tolAcceptCombine = null;
+            this.tolArrayCombine = null;
+        }
+
         /**
          * @param values Sample values.
          * @param expected Expected statistic value.
          * @param tol Tolerance for the tests.
          */
-        StatisticTestData(double[] values, double expected, DoubleTolerance tol) {
+        StatisticTestData(int[] values, StatisticResult expected, DoubleTolerance tol) {
             this(values, expected, tol, tol, tol, tol);
         }
 
@@ -265,7 +252,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
          * @param tolAcceptCombine Tolerance for the test of the statistic using the accept and combine methods.
          * @param tolArrayCombine Tolerance for the test of the statistic using the array create and combine methods.
          */
-        StatisticTestData(double[] values, double expected,
+        StatisticTestData(int[] values, StatisticResult expected,
                 DoubleTolerance tolAccept, DoubleTolerance tolArray,
                 DoubleTolerance tolAcceptCombine, DoubleTolerance tolArrayCombine) {
             super(values, expected);
@@ -277,7 +264,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
         /**
          * Gets the tolerance for the
-         * {@link BaseDoubleStatisticTest#testAccept(double[], double, DoubleTolerance)}
+         * {@link BaseIntStatisticTest#testAccept(int[], StatisticResult, DoubleTolerance)}
          * method.
          *
          * @return the tolerance
@@ -288,7 +275,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
         /**
          * Gets the tolerance for the
-         * {@link BaseDoubleStatisticTest#testArray(double[], double, DoubleTolerance)
+         * {@link BaseIntStatisticTest#testArray(int[], StatisticResult, DoubleTolerance)
          * )} method.
          *
          * @return the tolerance
@@ -299,7 +286,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
         /**
          * Gets the tolerance for the
-         * {@link BaseDoubleStatisticTest#testAcceptAndCombine(double[][], double, DoubleTolerance)}
+         * {@link BaseIntStatisticTest#testAcceptAndCombine(int[][], StatisticResult, DoubleTolerance)}
          * method.
          *
          * @return the tolerance
@@ -310,7 +297,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
         /**
          * Gets the tolerance for the
-         * {@link BaseDoubleStatisticTest#testArrayAndCombine(double[][], double, DoubleTolerance)}
+         * {@link BaseIntStatisticTest#testArrayAndCombine(int[][], StatisticResult, DoubleTolerance)}
          * method.
          *
          * @return the tolerance
@@ -330,60 +317,39 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     void setup() {
         statisticName = getStatisticName();
         // Compute the expected value for the standard test data
-        Stream.of(V1, V2, V3, V4, V5, V6).forEach(values -> {
-            final double[] sample = new double[values.length];
+        Stream.of(V1, V2, V3, V4).forEach(values -> {
+            final int[] sample = new int[values.length];
             for (int i = 0; i < sample.length; i++) {
                 sample[i] = mapValue(values[i]);
             }
-            final double expected = getExpectedValue(sample);
-            data.add(new TestData(sample, expected));
+            data.add(new TestData(values, getExpectedValue(values)));
         });
-        // Data with non-finite values.
+        // Data with extreme values.
         // Note: The order does not matter as this is shuffled
         // in the random order tests.
-        final double a = Double.NaN;
-        final double b = Double.POSITIVE_INFINITY;
-        final double c = Double.NEGATIVE_INFINITY;
-        final double[][] nonFinite = {
+        final int a = Integer.MAX_VALUE;
+        final int b = Integer.MIN_VALUE;
+        final int[][] extreme = {
             {a, 1, 1, 1, 1, 1, 1, 1, 1, 1},
             {b, 1, 1, 1, 1, 1, 1, 1, 1, 1},
-            {c, 1, 1, 1, 1, 1, 1, 1, 1, 1},
             {a, a, 1, 1, 1, 1, 1, 1, 1, 1},
             {b, b, 1, 1, 1, 1, 1, 1, 1, 1},
-            {c, c, 1, 1, 1, 1, 1, 1, 1, 1},
             {a, b, 1, 1, 1, 1, 1, 1, 1, 1},
-            {a, c, 1, 1, 1, 1, 1, 1, 1, 1},
-            {b, c, 1, 1, 1, 1, 1, 1, 1, 1},
-            {a, b, c, 1, 1, 1, 1, 1, 1, 1},
         };
-        Stream.of(nonFinite).forEach(values -> {
-            final double[] sample = values.clone();
+        Stream.of(extreme).forEach(values -> {
+            final int[] sample = values.clone();
             for (int i = sample.length; i-- > 0;) {
-                if (!Double.isFinite(values[i])) {
+                if (values[i] == 1) {
                     break;
                 }
                 sample[i] = mapValue(sample[i]);
             }
-            final double expected = getExpectedNonFiniteValue(sample);
-            dataNonFinite.add(new TestData(sample, expected));
+            dataExtremeValue.add(new TestData(values, getExpectedValue(values)));
         });
         // Cache the custom test data so any computed expected values are calculated only once
         streamTestData().forEach(dataCustom::add);
     }
 
-    /**
-     * Gets the statistic name.
-     *
-     * <p>The default implementation removes the text {@code "Test"} from the
-     * simple class name.
-     *
-     * @return the distribution name
-     * @see Class#getSimpleName()
-     */
-    protected String getStatisticName() {
-        return getClass().getSimpleName().replace("Test", "");
-    }
-
     /**
      * Creates the statistic.
      *
@@ -397,44 +363,20 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param values Values.
      * @return the statistic
      */
-    protected abstract S create(double... values);
-
-    /**
-     * Get the maximum number of values that can be added where the statistic is
-     * considered empty.
-     *
-     * <p>The default is zero. Override this method if the statistic requires multiple
-     * values to compute.
-     *
-     * @return the empty size
-     * @see #getEmptyValue()
-     */
-    protected int getEmptySize() {
-        return 0;
-    }
-
-    /**
-     * Gets the value of the statistics when it is considered empty.
-     *
-     * @return the empty value
-     * @see #getEmptySize()
-     */
-    protected abstract double getEmptyValue();
+    protected abstract S create(int... values);
 
     /**
      * Map the {@code value} to the valid domain of the statistic. This method is called
-     * with the example data before {@link #getExpectedValue(double[])} and
-     * {@link #getExpectedNonFiniteValue(double[])}. In the later case the method is only
-     * called with finite value. It can be used by implementing classes to adjust the data
-     * to a valid domain, for example if computing the sum-of-logs all values can be
-     * updated to be strictly positive.
+     * with the example data before {@link #getExpectedValue(int[])}. It can be used by
+     * implementing classes to adjust the data to a valid domain, for example if computing
+     * the sum-of-logs all values can be updated to be strictly positive.
      *
      * <p>The default implementation does nothing.
      *
      * @param value Value
      * @return the mapped value
      */
-    protected double mapValue(double value) {
+    protected int mapValue(int value) {
         return value;
     }
 
@@ -444,166 +386,71 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * <p>This method should be implemented using the canoncial definition of the statistic,
      * ideally in high-precision (e.g. using BigDecimal for statistics involving arithmetic).
      *
-     * <p>This method will not be called with non-finite values.
-     *
      * @param values Values.
      * @return the expected value
      */
-    protected abstract double getExpectedValue(double[] values);
+    protected abstract StatisticResult getExpectedValue(int[] values);
 
     /**
-     * Gets the expected value of the statistic from the {@code values}, at least one
-     * of which will be non-finite. This method will be called with arrays that contain
-     * combinations of {@code NaN} and {@code +/-infinity}.
+     * Creates the equivalent {@link DoubleStatistic} from the {@code values}.
+     * This is used to cross-validate the {@link IntStatistic} result.
      *
      * @param values Values.
-     * @return the expected value
-     */
-    protected abstract double getExpectedNonFiniteValue(double[] values);
-
-    /**
-     * Gets the tolerance for equality of the statistic and the expected value.
-     * This method is not used by the base implementation except to provide the default
-     * implementation for the individual test tolerance methods.
-     *
-     * <p><strong>Note</strong>
-     *
-     * <p>The default implementation throws an exception. This forces the
-     * implementation to either provide a single tolerance for all tests, or the tests
-     * individually by overriding each tolerance method.
-     *
-     * @return the tolerance
-     */
-    protected DoubleTolerance getTolerance() {
-        throw new IllegalStateException("Not all test tolerances have been configured");
-    }
-
-    /**
-     * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testAccept(double[], double, DoubleTolerance)} test.
-     *
-     * <p>The default implementation uses {@link #getTolerance()}.
-     *
-     * @return the tolerance
-     */
-    protected DoubleTolerance getToleranceAccept() {
-        return getTolerance();
-    }
-
-    /**
-     * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testArray(double[], double, DoubleTolerance)} test.
-     *
-     * <p>The default implementation uses {@link #getTolerance()}.
-     *
-     * @return the tolerance
-     */
-    protected DoubleTolerance getToleranceArray() {
-        return getTolerance();
-    }
-
-    /**
-     * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testAcceptAndCombine(double[][], double, DoubleTolerance)} test.
-     *
-     * <p>The default implementation uses {@link #getTolerance()}.
-     *
-     * @return the tolerance
+     * @return the statistic
      */
-    protected DoubleTolerance getToleranceAcceptAndCombine() {
-        return getTolerance();
-    }
+    protected abstract DoubleStatistic createAsDoubleStatistic(int... values);
 
-    /**
-     * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testArrayAndCombine(double[][], double, DoubleTolerance)} test.
-     *
-     * <p>The default implementation uses {@link #getTolerance()}.
-     *
-     * @return the tolerance
-     */
-    protected DoubleTolerance getToleranceArrayAndCombine() {
-        return getTolerance();
-    }
+    //------------------------ Helper Methods to create test data ---------------------------
 
     /**
-     * Gets the tolerance for equality of the statistic and the expected value
-     * for any test that includes data with non-finite values.
-     *
-     * <p>The default implementation uses binary equality.
-     *
-     * <p>Note: This tolerance has not been separated for different tests that use
-     * the {@code accept}, {@link #create(double...)} and {@code combine} methods.
-     * Computation of a statistic from non-finite values should produce either
-     * {@code NaN} or {@code +/-infinity}. However this is not enforced as the expected
-     * value is computed by implementations.
+     * Creates the test data.
+     * The expected value must be provided.
      *
-     * @return the tolerance
-     * {@link #getExpectedNonFiniteValue(double[])}
-     */
-    protected DoubleTolerance getToleranceNonFinite() {
-        return DoubleTolerances.equals();
-    }
-
-    /**
-     * Returns {@code true} if the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
-     * method is symmetric. If {@code true} then the combine will be tested with
-     * duplicate instances of the statistic, combined in left-to-right and right-to-left;
-     * the result must have the same statistic value.
+     * <p>This method can be used to add data computed with external reference implementations.
      *
-     * <p>The default is {@code true}. Override this if the implementation is not symmetric
-     * due to the implementation details.
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code int}.
      *
-     * @return {@code true} if combine is symmetric
+     * @param expected Expected statistic value.
+     * @param values Values.
+     * @return the statistic test data
      */
-    protected boolean isCombineSymmetric() {
-        return true;
+    final StatisticTestData addReference(int expected, int... values) {
+        Assertions.assertEquals(ResultType.INT, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected));
     }
 
-    //------------------------ Helper Methods to create test data ---------------------------
-
     /**
-     * Creates the tolerance using an absolute error.
-     *
-     * <p>If the absolute tolerance is zero it is ignored and a tolerance of numerical
-     * equality is used.
+     * Creates the test data.
+     * The expected value must be provided.
      *
-     * @param eps Absolute tolerance
-     * @return the tolerance
-     */
-    static final DoubleTolerance createAbsTolerance(double eps) {
-        return eps > 0 ? DoubleTolerances.absolute(eps) : DoubleTolerances.ulps(0);
-    }
-
-    /**
-     * Creates the tolerance using an relative error.
+     * <p>This method can be used to add data computed with external reference implementations.
      *
-     * <p>If the relative tolerance is zero it is ignored and a tolerance of numerical
-     * equality is used.
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code long}.
      *
-     * @param eps Relative tolerance
-     * @return the tolerance
+     * @param expected Expected statistic value.
+     * @param values Values.
+     * @return the statistic test data
      */
-    static final DoubleTolerance createRelTolerance(double eps) {
-        return eps > 0 ? DoubleTolerances.relative(eps) : DoubleTolerances.ulps(0);
+    final StatisticTestData addReference(long expected, int... values) {
+        Assertions.assertEquals(ResultType.LONG, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected));
     }
 
     /**
-     * Creates the tolerance using an {@code Or} combination of absolute and relative error.
+     * Creates the test data.
+     * The expected value must be provided.
      *
-     * <p>If the absolute tolerance is zero it is ignored and a tolerance of numerical equality
-     * is used.
+     * <p>This method can be used to add data computed with external reference implementations.
      *
-     * <p>If the relative tolerance is zero it is ignored.
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code BigInteger}.
      *
-     * @param absTolerance Absolute tolerance
-     * @param relTolerance Relative tolerance
-     * @return the tolerance
+     * @param expected Expected statistic value.
+     * @param values Values.
+     * @return the statistic test data
      */
-    static final DoubleTolerance createAbsOrRelTolerance(double absTolerance, double relTolerance) {
-        final DoubleTolerance tol = createAbsTolerance(absTolerance);
-        return relTolerance > 0 ? tol.or(DoubleTolerances.relative(relTolerance)) : tol;
+    final StatisticTestData addReference(BigInteger expected, int... values) {
+        Assertions.assertEquals(ResultType.BIG_INTEGER, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected));
     }
 
     /**
@@ -613,6 +460,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      *
      * <p>This method can be used to add data computed with external reference implementations.
      *
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code double}.
+     *
      * @param expected Expected statistic value.
      * @param values Values.
      * @return the statistic test data
@@ -621,8 +470,9 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @see #getToleranceAcceptAndCombine()
      * @see #getToleranceArrayAndCombine()
      */
-    final StatisticTestData addReference(double expected, double... values) {
-        return new StatisticTestData(values, expected,
+    StatisticTestData addReference(double expected, int... values) {
+        Assertions.assertEquals(ResultType.DOUBLE, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected),
             getToleranceAccept(), getToleranceArray(),
             getToleranceAcceptAndCombine(), getToleranceArrayAndCombine());
     }
@@ -633,13 +483,16 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      *
      * <p>This method can be used to add data computed with external reference implementations.
      *
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code double}.
+     *
      * @param expected Expected statistic value.
      * @param tol Test tolerance.
      * @param values Values.
      * @return the statistic test data
      */
-    static final StatisticTestData addReference(double expected, DoubleTolerance tol, double... values) {
-        return new StatisticTestData(values, expected, tol);
+    final StatisticTestData addReference(double expected, DoubleTolerance tol, int... values) {
+        Assertions.assertEquals(ResultType.DOUBLE, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected), tol);
     }
 
     /**
@@ -648,6 +501,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      *
      * <p>This method can be used to add data computed with external reference implementations.
      *
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code double}.
+     *
      * @param expected Expected statistic value.
      * @param tolAccept Tolerance for the test of the statistic using the accept method.
      * @param tolArray Tolerance for the test of the statistic using the array create method.
@@ -656,11 +511,12 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param values Values.
      * @return the statistic test data
      */
-    static final StatisticTestData addReference(double expected,
+    final StatisticTestData addReference(double expected,
             DoubleTolerance tolAccept, DoubleTolerance tolArray,
             DoubleTolerance tolAcceptCombine, DoubleTolerance tolArrayCombine,
-            double... values) {
-        return new StatisticTestData(values, expected,
+            int... values) {
+        Assertions.assertEquals(ResultType.DOUBLE, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected),
             tolAccept, tolArray, tolAcceptCombine, tolArrayCombine);
     }
 
@@ -673,38 +529,46 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      *
      * @param values Values.
      * @return the statistic test data
-     * @see #getExpectedValue(double[])
+     * @see #getExpectedValue(int[])
      * @see #getToleranceAccept()
      * @see #getToleranceArray()
      * @see #getToleranceAcceptAndCombine()
      * @see #getToleranceArrayAndCombine()
      */
-    final StatisticTestData addCase(double... values) {
-        return new StatisticTestData(values, getExpectedValue(values),
-            getToleranceAccept(), getToleranceArray(),
-            getToleranceAcceptAndCombine(), getToleranceArrayAndCombine());
+    final StatisticTestData addCase(int... values) {
+        if (getResultType() == ResultType.DOUBLE) {
+            return new StatisticTestData(values, getExpectedValue(values),
+                getToleranceAccept(), getToleranceArray(),
+                getToleranceAcceptAndCombine(), getToleranceArrayAndCombine());
+        }
+        return new StatisticTestData(values, getExpectedValue(values));
     }
 
     /**
      * Creates the edge-case/example test data. The same tolerance is used for all tests.
-     * The expected value will be computed using {@link #getExpectedValue(double[])}.
+     * The expected value will be computed using {@link #getExpectedValue(int[])}.
      *
      * <p>This method can be used to add data to target specific edge cases.
      *
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code double}.
+     *
      * @param tol Test tolerance.
      * @param values Values.
      * @return the statistic test data
      */
-    final StatisticTestData addCase(DoubleTolerance tol, double... values) {
+    final StatisticTestData addCase(DoubleTolerance tol, int... values) {
+        Assertions.assertEquals(ResultType.DOUBLE, getResultType());
         return new StatisticTestData(values, getExpectedValue(values), tol);
     }
 
     /**
      * Creates the edge-case/example test data.
-     * The expected value will be computed using {@link #getExpectedValue(double[])}.
+     * The expected value will be computed using {@link #getExpectedValue(int[])}.
      *
      * <p>This method can be used to add data to target specific edge cases.
      *
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code double}.
+     *
      * @param tolAccept Tolerance for the test of the statistic using the accept method.
      * @param tolArray Tolerance for the test of the statistic using the array create method.
      * @param tolAcceptCombine Tolerance for the test of the statistic using the accept and combine methods.
@@ -715,7 +579,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     final StatisticTestData addCase(
             DoubleTolerance tolAccept, DoubleTolerance tolArray,
             DoubleTolerance tolAcceptCombine, DoubleTolerance tolArrayCombine,
-            double... values) {
+            int... values) {
+        Assertions.assertEquals(ResultType.DOUBLE, getResultType());
         return new StatisticTestData(values, getExpectedValue(values),
             tolAccept, tolArray, tolAcceptCombine, tolArrayCombine);
     }
@@ -738,12 +603,12 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * will be used for all 4 main tests of the statistic:
      *
      * <ol>
-     *  <li>{@link java.util.function.DoubleConsumer#accept accept}
-     *  <li>{@link java.util.function.DoubleConsumer#accept accept} and
-     *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
-     *  <li>{@link #create(double...)}
-     *  <li>{@link #create(double...)} and
-     *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     *  <li>{@link java.util.function.IntConsumer#accept accept}
+     *  <li>{@link java.util.function.IntConsumer#accept accept} and
+     *      {@link StatisticAccumulator#combine(StatisticResult) combine}
+     *  <li>{@link #create(int...)}
+     *  <li>{@link #create(int...)} and
+     *      {@link StatisticAccumulator#combine(StatisticResult) combine}
      * </ol>
      *
      * <p>To test the {@code combine} method the data is split into approximately even groups
@@ -759,7 +624,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      *   add(3.5, createRelTolerance(1e-15), 2, 3, 4, 5),
      *   add(11.5, createRelTolerance(1e-15), 2, 3, 4, 5, 13, 42),
      *   // Edge case for mean
-     *   add(createRelTolerance(1e-15), Double.MAX_VALUE, -Double.MAX_VALUE)
+     *   add(createRelTolerance(1e-15), Int.MAX_VALUE, -Int.MAX_VALUE)
      * )}
      * </pre>
      *
@@ -769,20 +634,11 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      */
     protected abstract Stream<StatisticTestData> streamTestData();
 
-    /**
-     * Gets the group sizes used to test the combine method with the custom test data.
-     *
-     * @return the group sizes
-     */
-    protected int[] getCombineGroupSizes() {
-        return new int[] {2, 3};
-    }
-
     //------------------------ Methods to stream the test data -----------------------------
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept accept} method. The expected value
+     * {@link java.util.function.IntConsumer#accept accept} method. The expected value
      * and tolerance are supplied by the implementing class.
      *
      * @return the stream
@@ -793,7 +649,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link #create(double...)} method. The expected value and tolerance are supplied
+     * {@link #create(int...)} method. The expected value and tolerance are supplied
      * by the implementing class.
      *
      * @return the stream
@@ -804,7 +660,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Stream the arguments to test the computation of the statistic using a single
-     * {@code double[]} array.
+     * {@code int[]} array.
      *
      * @param tol Test tolerance.
      * @param tolCustom Tolerance for any custom test data.
@@ -814,15 +670,15 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
             Function<StatisticTestData, DoubleTolerance> tolCustom) {
         final Builder<Arguments> b = Stream.builder();
         data.forEach(d -> b.accept(Arguments.of(d.getValues(), d.getExpected(), tol)));
-        dataNonFinite.forEach(d -> b.accept(Arguments.of(d.getValues(), d.getExpected(), getToleranceNonFinite())));
+        dataExtremeValue.forEach(d -> b.accept(Arguments.of(d.getValues(), d.getExpected(), tol)));
         dataCustom.forEach(d -> b.accept(Arguments.of(d.getValues(), d.getExpected(), tolCustom.apply(d))));
         return b.build();
     }
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * {@link java.util.function.IntConsumer#accept(int) accept} method for each
+     * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method. The expected value and tolerance are supplied by the implementing class.
      *
      * @return the stream
@@ -833,8 +689,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link #create(double...)} method for each array, then the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method. The
+     * {@link #create(int...)} method for each array, then the
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method. The
      * expected value and tolerance are supplied by the implementing class.
      *
      * @return the stream
@@ -845,9 +701,9 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
+     * {@link java.util.function.IntConsumer#accept(int) accept} method for each
      * element of a parallel stream, then the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      * The expected value and tolerance are supplied by the implementing class.
      *
      * <p>Note that this method uses the tolerance from {@link #getToleranceAcceptAndCombine()}.
@@ -860,8 +716,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Stream the arguments to test the computation of the statistic using the multiple
-     * {@code double[]} arrays and the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@code int[]} arrays and the
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      *
      * @param tol Test tolerance.
      * @param tolCustom Tolerance for any custom test data.
@@ -876,15 +732,15 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
                   .map(x -> cut(d.getValues(), x))
                   .forEach(e -> b.accept(Arguments.of(e, d.getExpected(), tol)));
         });
-        dataNonFinite.forEach(d -> {
+        dataExtremeValue.forEach(d -> {
             // Cut the standard data into pieces
-            Arrays.stream(SIZES_NON_FINITE)
+            Arrays.stream(SIZES_EXTREME)
                   .map(x -> cut(d.getValues(), x))
-                  .forEach(e -> b.accept(Arguments.of(e, d.getExpected(), getToleranceNonFinite())));
+                  .forEach(e -> b.accept(Arguments.of(e, d.getExpected(), tol)));
         });
         dataCustom.forEach(d -> {
             // Only split lengths above the number of groups
-            final double[] values = d.getValues();
+            final int[] values = d.getValues();
             final int n = values.length;
             for (final int groups : getCombineGroupSizes()) {
                 if (groups <= n) {
@@ -905,8 +761,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param sizes Sizes.
      * @return the set of arrays
      */
-    private static double[][] cut(double[] data, int... sizes) {
-        ArrayList<double[]> set = new ArrayList<>();
+    private static int[][] cut(int[] data, int... sizes) {
+        ArrayList<int[]> set = new ArrayList<>();
         int from = 0;
         for (final int size : sizes) {
             final int to = Math.min(data.length, from + size);
@@ -922,7 +778,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
             Assertions.fail(
                 () -> "Sizes " + Arrays.toString(sizes) + " do not cover length " + data.length);
         }
-        return set.toArray(new double[0][]);
+        return set.toArray(new int[0][]);
     }
 
     /**
@@ -933,14 +789,14 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param groups Number of groups.
      * @return the set of arrays
      */
-    private static double[][] split(double[] data, int groups) {
+    private static int[][] split(int[] data, int groups) {
         // Note: round the size of the group.
         // The final group accounts for length difference:
         // 4/3 => 1, 1, 2
         // 5/3 => 2, 2, 1
         // 7/3 => 2, 2, 3
         final int size = (int) Math.round((double) data.length / groups);
-        final double[][] values = new double[groups][];
+        final int[][] values = new int[groups][];
         for (int i = 0; i < groups; i++) {
             final int from = i * size;
             // Final group may be smaller or larger
@@ -975,106 +831,122 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param tol Non-empty tolerance.
      */
     private void assertEmpty(S stat, DoubleTolerance tol) {
-        final double v = getEmptyValue();
+        final StatisticResult v = getEmptyValue();
         final int size = getEmptySize();
-        final double[] values = new double[size + 1];
+        final int[] values = new int[size + 1];
         // Fill beyond empty
         for (int i = 0; i <= size; i++) {
-            Assertions.assertEquals(v, stat.getAsDouble(), () -> statisticName + " should be empty");
+            TestHelper.assertEquals(v, stat, tol, () -> statisticName + " should be empty");
             stat.accept(i + 1);
             values[i] = i + 1;
         }
-        final double expected = getExpectedValue(values);
-        TestUtils.assertEquals(expected, stat.getAsDouble(), tol,
+        final StatisticResult expected = getExpectedValue(values);
+        TestHelper.assertEquals(expected, stat, tol,
             () -> statisticName + " should not be empty");
     }
 
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method. The
+     * {@link java.util.function.IntConsumer#accept(int) accept} method. The
      * statistic is created using both the {@link #create()} and the
-     * {@link #create(double...)} methods; the two instances must compute the same result.
+     * {@link #create(int...)} methods; the two instances must compute the same result.
      */
     @ParameterizedTest
     @MethodSource
-    final void testAccept(double[] values, double expected, DoubleTolerance tol) {
-        final double actual = assertStatistic(v -> Statistics.add(create(), v), values, expected, tol);
-        // Repeat after creation with an empty array
-        assertStatistic(v -> Statistics.add(create(EMPTY), v), values, actual, tol);
+    final void testAccept(int[] values, StatisticResult expected, DoubleTolerance tol) {
+        final S actual = assertStatistic(v -> Statistics.add(create(), v), values, expected, tol);
+        // Repeat after creation with an empty array. No tolerance for exact equality.
+        assertStatistic(v -> Statistics.add(create(EMPTY), v), values, actual, null);
     }
 
     /**
-     * Test the computation of the statistic using the {@link #create(double...)} method.
+     * Test the computation of the statistic using the {@link #create(int...)} method.
      */
     @ParameterizedTest
     @MethodSource
-    final void testArray(double[] values, double expected, DoubleTolerance tol) {
+    final void testArray(int[] values, StatisticResult expected, DoubleTolerance tol) {
         assertStatistic(this::create, values, expected, tol);
     }
 
+    /**
+     * Test the computation of the statistic against the equivalent {@link DoubleStatistic}.
+     * The result is tested as a {@code double} using the configured {@link #getToleranceAsDouble()}.
+     * Integer values are not asserted as they may not be an exact match.
+     */
+    @ParameterizedTest
+    @MethodSource(value = {"testAccept"})
+    final void testVsDoubleStatistic(int[] values) {
+        final double expected = createAsDoubleStatistic(values).getAsDouble();
+        final DoubleTolerance tol = getToleranceAsDouble();
+        TestUtils.assertEquals(expected, Statistics.add(create(), values).getAsDouble(), tol,
+            () -> statisticName + " accept: " + format(values));
+        TestUtils.assertEquals(expected, create(values).getAsDouble(), tol,
+            () -> statisticName + " array: " + format(values));
+    }
+
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * {@link java.util.function.IntConsumer#accept(int) accept} method for each
+     * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      */
     @ParameterizedTest
     @MethodSource
-    final void testAcceptAndCombine(double[][] values, double expected, DoubleTolerance tol) {
+    final void testAcceptAndCombine(int[][] values, StatisticResult expected, DoubleTolerance tol) {
         assertCombine(v -> Statistics.add(create(), v), values, expected, tol);
     }
 
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * {@link java.util.function.IntConsumer#accept(int) accept} method for each
+     * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      */
     @ParameterizedTest
     @MethodSource
-    final void testArrayAndCombine(double[][] values, double expected, DoubleTolerance tol) {
+    final void testArrayAndCombine(int[][] values, StatisticResult expected, DoubleTolerance tol) {
         assertCombine(this::create, values, expected, tol);
     }
 
     /**
-     * Test the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method
+     * Test the {@link StatisticAccumulator#combine(StatisticResult) combine} method
      * with an empty instance combined with a non-empty instance.
      */
     @ParameterizedTest
     @MethodSource(value = "testAccept")
-    final void testEmptyCombine(double[] values) {
+    final void testEmptyCombine(int[] values) {
         final S empty = create();
         final S nonEmpty = Statistics.add(create(), values);
-        final double expected = nonEmpty.getAsDouble();
+        final StatisticResult expected = Statistics.add(create(), values);
         final S result = assertCombine(empty, nonEmpty);
-        Assertions.assertEquals(expected, result.getAsDouble(),
+        TestHelper.assertEquals(expected, result, null,
             () -> statisticName + " empty.combine(nonEmpty)");
     }
 
     /**
-     * Test the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method
+     * Test the {@link StatisticAccumulator#combine(StatisticResult) combine} method
      * with a non-empty instance combined with an empty instance.
      */
     @ParameterizedTest
     @MethodSource(value = "testAccept")
-    final void testCombineEmpty(double[] values) {
+    final void testCombineEmpty(int[] values) {
         final S empty = create();
         final S nonEmpty = Statistics.add(create(), values);
-        final double expected = nonEmpty.getAsDouble();
+        final StatisticResult expected = Statistics.add(create(), values);
         final S result = assertCombine(nonEmpty, empty);
-        Assertions.assertEquals(expected, result.getAsDouble(),
+        TestHelper.assertEquals(expected, result, null,
             () -> statisticName + " nonEmpty.combine(empty)");
     }
 
     /**
      * Test the computation of the statistic using a parallel stream of {@code double}
      * values. The accumulator is the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method; the
-     * combiner is the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * {@link java.util.function.IntConsumer#accept(int) accept} method; the
+     * combiner is the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      *
      * <p>Note: This method is similar to the
-     * {@link #testAcceptAndCombine(double[][], double, DoubleTolerance)} method and uses
+     * {@link #testAcceptAndCombine(int[][], StatisticResult, DoubleTolerance)} method and uses
      * the same data, concatenated to a single array, and tolerance. The difference is the
      * former method uses explicit subsets of the data and its own fixed algorithm to
      * combine instances. This method leaves the entire process of dividing the data and
@@ -1083,22 +955,21 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      */
     @ParameterizedTest
     @MethodSource
-    final void testAcceptParallelStream(double[] values, double expected, DoubleTolerance tol) {
-        final double actual = Arrays.stream(values)
+    final void testAcceptParallelStream(int[] values, StatisticResult expected, DoubleTolerance tol) {
+        final S actual = Arrays.stream(values)
             .parallel()
-            .collect(this::create, DoubleStatistic::accept, DoubleStatisticAccumulator::combine)
-            .getAsDouble();
-        TestUtils.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
+            .collect(this::create, IntStatistic::accept, StatisticAccumulator::combine);
+        TestHelper.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
     }
 
     /**
-     * Test the computation of the statistic using a parallel stream of {@code double[]}
-     * arrays. The arrays are mapped to a statistic using the {@link #create(double...)}
+     * Test the computation of the statistic using a parallel stream of {@code int[]}
+     * arrays. The arrays are mapped to a statistic using the {@link #create(int...)}
      * method, and the stream reduced using the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      *
      * <p>Note: This method is similar to the
-     * {@link #testArrayAndCombine(double[][], double, DoubleTolerance)} method and uses
+     * {@link #testArrayAndCombine(int[][], StatisticResult, DoubleTolerance)} method and uses
      * the same data and tolerance. The difference is the former method uses its own fixed
      * algorithm to combine instances. This method leaves the process of combining to the
      * JDK parallel stream; results may vary across platforms as it is dependent on the
@@ -1106,24 +977,23 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      */
     @ParameterizedTest
     @MethodSource(value = "testArrayAndCombine")
-    final void testArrayParallelStream(double[][] values, double expected, DoubleTolerance tol) {
-        final double actual = Arrays.stream(values)
+    final void testArrayParallelStream(int[][] values, StatisticResult expected, DoubleTolerance tol) {
+        final S actual = Arrays.stream(values)
             .parallel()
             .map(this::create)
-            .reduce(DoubleStatisticAccumulator::combine)
+            .reduce(StatisticAccumulator::combine)
             // Return an empty instance if there is no data
-            .orElseGet(this::create)
-            .getAsDouble();
-        TestUtils.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
+            .orElseGet(this::create);
+        TestHelper.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
     }
 
     /**
-     * Invokes the {@link #testAccept(double[], double, DoubleTolerance)} method using
+     * Invokes the {@link #testAccept(int[], StatisticResult, DoubleTolerance)} method using
      * random permutations of the data.
      */
     @ParameterizedTest
     @MethodSource(value = "testAccept")
-    final void testAcceptRandom(double[] values, double expected, DoubleTolerance tol) {
+    final void testAcceptRandom(int[] values, StatisticResult expected, DoubleTolerance tol) {
         // Obtain a seed so that it can be logged to allow repeats
         final long[] seed = TestHelper.createRNGSeed();
         final UniformRandomProvider rng = TestHelper.createRNG(seed);
@@ -1138,12 +1008,12 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     }
 
     /**
-     * Invokes the {@link #testArray(double[], double, DoubleTolerance)} method using
+     * Invokes the {@link #testArray(int[], StatisticResult, DoubleTolerance)} method using
      * random permutations of the data.
      */
     @ParameterizedTest
     @MethodSource(value = "testArray")
-    final void testArrayRandom(double[] values, double expected, DoubleTolerance tol) {
+    final void testArrayRandom(int[] values, StatisticResult expected, DoubleTolerance tol) {
         // Obtain a seed so that it can be logged to allow repeats
         final long[] seed = TestHelper.createRNGSeed();
         final UniformRandomProvider rng = TestHelper.createRNG(seed);
@@ -1158,16 +1028,16 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     }
 
     /**
-     * Invokes the {@link #testAcceptAndCombine(double[][], double, DoubleTolerance)} method using
+     * Invokes the {@link #testAcceptAndCombine(int[][], StatisticResult, DoubleTolerance)} method using
      * random permutations of the data.
      */
     @ParameterizedTest
     @MethodSource(value = "testAcceptAndCombine")
-    final void testAcceptAndCombineRandom(double[][] values, double expected, DoubleTolerance tol) {
+    final void testAcceptAndCombineRandom(int[][] values, StatisticResult expected, DoubleTolerance tol) {
         // Obtain a seed so that it can be logged to allow repeats
         final long[] seed = TestHelper.createRNGSeed();
         final UniformRandomProvider rng = TestHelper.createRNG(seed);
-        final double[] allValues = TestHelper.concatenate(values);
+        final int[] allValues = TestHelper.concatenate(values);
         int repeat = 0;
         try {
             while (repeat++ < RANDOM_PERMUTATIONS) {
@@ -1181,16 +1051,16 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     }
 
     /**
-     * Invokes the {@link #testArrayAndCombine(double[][], double, DoubleTolerance)} method using
+     * Invokes the {@link #testArrayAndCombine(int[][], StatisticResult, DoubleTolerance)} method using
      * random permutations of the data.
      */
     @ParameterizedTest
     @MethodSource(value = "testArrayAndCombine")
-    final void testArrayAndCombineRandom(double[][] values, double expected, DoubleTolerance tol) {
+    final void testArrayAndCombineRandom(int[][] values, StatisticResult expected, DoubleTolerance tol) {
         // Obtain a seed so that it can be logged to allow repeats
         final long[] seed = TestHelper.createRNGSeed();
         final UniformRandomProvider rng = TestHelper.createRNG(seed);
-        final double[] allValues = TestHelper.concatenate(values);
+        final int[] allValues = TestHelper.concatenate(values);
         int repeat = 0;
         try {
             while (repeat++ < RANDOM_PERMUTATIONS) {
@@ -1205,14 +1075,14 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Test the computation of the statistic using an empty instance and the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method with
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method with
      * instances containing single values. Adding statistic instances containing 1 value
      * should be the same precision as adding a {@code double} value, although the two
      * results are not required to be exactly equal.
      */
     @ParameterizedTest
     @MethodSource(value = "testAccept")
-    final void testCombineAsAccept(double[] values, double expected, DoubleTolerance tol) {
+    final void testCombineAsAccept(int[] values, StatisticResult expected, DoubleTolerance tol) {
         final S empty = create();
         assertStatistic(v ->
             Arrays.stream(values)
@@ -1230,17 +1100,16 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param tol Test tolerance.
      * @return the computed statistic
      */
-    final double assertStatistic(Function<double[], S> constructor,
-                                 double[] values, double expected, DoubleTolerance tol) {
+    final S assertStatistic(Function<int[], S> constructor,
+                            int[] values, StatisticResult expected, DoubleTolerance tol) {
         final S stat = constructor.apply(values);
-        final double actual = stat.getAsDouble();
-        TestUtils.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
-        return actual;
+        TestHelper.assertEquals(expected, stat, tol, () -> statisticName + ": " + format(values));
+        return stat;
     }
 
     /**
      * Assert the computation of the statistic using the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      *
      * @param constructor Function to create the statistic.
      * @param values Values.
@@ -1248,8 +1117,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param tol Test tolerance.
      * @return the computed statistic
      */
-    final double assertCombine(Function<double[], S> constructor,
-                               double[][] values, double expected, DoubleTolerance tol) {
+    final S assertCombine(Function<int[], S> constructor,
+                          int[][] values, StatisticResult expected, DoubleTolerance tol) {
         int n = values.length;
         Assertions.assertNotEquals(0, n, "No data");
         final List<S> stats = Arrays.stream(values)
@@ -1288,9 +1157,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
             n >>= 1;
         }
         final S stat = stats.get(0);
-        final double actual = stat.getAsDouble();
-        TestUtils.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
-        return actual;
+        TestHelper.assertEquals(expected, stat, tol, () -> statisticName + ": " + format(values));
+        return stat;
     }
 
     /**
@@ -1311,43 +1179,23 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
         stats.set(target, s1);
         if (isCombineSymmetric()) {
             final S s2 = assertCombine(stats2.get(rhs), stats2.get(lhs));
-            Assertions.assertEquals(s1.getAsDouble(), s2.getAsDouble(),
+            TestHelper.assertEquals(s1, s2, null,
                 () -> statisticName + ": Non-symmetric combine");
             stats2.set(target, s2);
         }
     }
 
-    /**
-     * Combine the two statistics. This method asserts the contract of the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
-     * The left-hand side (LHS) argument must be returned. The right-hand side (RHS) must
-     * be unchanged by the operation.
-     *
-     * <p>Use this method whenever combining two instances of a statistic.
-     *
-     * @param a LHS.
-     * @param b RHS.
-     * @return the result
-     */
-    final S assertCombine(S a, S b) {
-        final double before = b.getAsDouble();
-        final S c = a.combine(b);
-        Assertions.assertEquals(before, b.getAsDouble(), () -> statisticName + ": Combine altered the RHS");
-        Assertions.assertSame(a, c, () -> statisticName + ": Combined did not return the LHS");
-        return c;
-    }
-
     /**
      * Format the values as a string. The maximum length is limited.
      *
      * @param values Values.
      * @return the string
      */
-    static String format(double[] values) {
+    static String format(int[] values) {
         if (values.length > MAX_FORMAT_VALUES) {
             return Arrays.stream(values)
                          .limit(MAX_FORMAT_VALUES)
-                         .mapToObj(Double::toString)
+                         .mapToObj(Integer::toString)
                          .collect(Collectors.joining(", ", "[", ", ...]"));
         }
         return Arrays.toString(values);
@@ -1359,9 +1207,9 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param values Values.
      * @return the string
      */
-    private static String format(double[][] values) {
+    private static String format(int[][] values) {
         return Arrays.stream(values)
-            .map(BaseDoubleStatisticTest::format)
+            .map(BaseIntStatisticTest::format)
             .collect(Collectors.joining(", "));
     }
 
@@ -1373,7 +1221,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param seed Seed.
      * @param repeat Repeat of the total random permutations.
      */
-    private void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
+    private static void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
         throw new AssertionError(String.format("%s; Seed=%s; Repeat=%d/%d",
             e.getMessage(), Arrays.toString(seed), repeat, RANDOM_PERMUTATIONS), e);
     }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseLongStatisticTest.java
similarity index 60%
copy from commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
copy to commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseLongStatisticTest.java
index 6e32115..c3a567d 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseLongStatisticTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.statistics.descriptive;
 
+import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -26,7 +27,6 @@ import java.util.stream.Stream;
 import java.util.stream.Stream.Builder;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.statistics.distribution.DoubleTolerance;
-import org.apache.commons.statistics.distribution.DoubleTolerances;
 import org.apache.commons.statistics.distribution.TestUtils;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
@@ -39,36 +39,43 @@ import org.junit.jupiter.params.provider.MethodSource;
 
 /**
  * Abstract base class for statistic tests.
- * This supports testing statistics that implement {@link DoubleStatistic} and
- * {@link DoubleStatisticAccumulator}.
+ * This supports testing statistics that implement {@link LongStatistic} and
+ * {@link StatisticAccumulator}.
  *
  * <p>This class uses parameterized tests that are repeated for instances of a
- * statistic. The statistic is tested using standard finite and non-finite data.
+ * statistic. The statistic is tested using standard data.
  * Additional data may be provided by implementations.
  *
  * <p>There are 4 main tests for a statistic populated with values using:
  *
  * <ol>
- *  <li>{@link java.util.function.DoubleConsumer#accept accept}
- *  <li>{@link java.util.function.DoubleConsumer#accept accept} and
- *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
- *  <li>{@link #create(double...)}
- *  <li>{@link #create(double...)} and
- *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ *  <li>{@link java.util.function.LongConsumer#accept accept}
+ *  <li>{@link java.util.function.LongConsumer#accept accept} and
+ *      {@link StatisticAccumulator#combine(StatisticResult) combine}
+ *  <li>{@link #create(long...)}
+ *  <li>{@link #create(long...)} and
+ *      {@link StatisticAccumulator#combine(StatisticResult) combine}
  * </ol>
  *
- * <p>An test implementation has to provide these method implementations:
+ * <p>Note: Integer statistics may return the expected result as a {@code double}, {@code int},
+ * {@code long}, or {@code BigInteger}. The default is {@code double}.
+ *
+ * <p>A test implementation has to provide these method implementations:
  *
  * <ul>
+ *  <li>{@link #getResultType()}: Get the native result type of the statistic.
+ *      This is used to verify that test cases are added using the correct type and
+ *      test tolerances are configured for {@code double} result types.
  *  <li>{@link #create()}: Create an empty statistic.
- *  <li>{@link #create(double...)}: Create a statistic from a set of values.
+ *  <li>{@link #create(int...)}: Create a statistic from a set of values.
  *  <li>{@link #getEmptyValue()}: The expected value of a statistic when not enough values have
  *      been observed. The minimum number of values can be provided in {@link #getEmptySize()}.
- *  <li>{@link #getExpectedValue(double[])}: A method to compute an expected value for the
+ *  <li>{@link #getExpectedValue(long[])}: A method to compute an expected value for the
  *      statistic. This is used to create the expected value on standard test data. It should
  *      be an implementation of the canonical definition of the statistic. It can use extended
  *      precision to provided an accurate expected result.
- *  <li>{@link #getTolerance()}: The test tolerance for all tests. This method is not used
+ *  <li>{@link #getTolerance()}: The test tolerance for all tests when the expected value is
+ *      a {@code double}. Tolerances are not used for integer results. This method is not used
  *      directly by the base class for test cases. It provides the default implementation for
  *      the 4 methods to provide the tolerance for each of the 4 main test cases.
  *      <em>Note that the default implementation throws an exception</em>. This forces the
@@ -77,12 +84,6 @@ import org.junit.jupiter.params.provider.MethodSource;
  *      precision for creation of the statistic from a stream of values or an array then
  *      it can override {@link #getToleranceAccept()}, {@link #getToleranceArray()} and
  *      their combine equivalents.
- *  <li>{@link #getExpectedNonFiniteValue(double[])}: A method to compute an expected value
- *      for the statistic when a non-finite value is encountered. This is only used with at
- *      least one non-finite value. Typically this should return {@code NaN} or an infinity.
- *      Data tested with non-finite values will use {@link #getToleranceNonFinite()} which
- *      defaults to an exact binary equality. This may be overridden for example to return
- *      a tolerance that matches any non-finite value.
  *  <li>{@link #streamTestData()}: Provide custom data for the test. It is
  *      <em>strongly recommended</em> to provide reference data from independent implementations.
  *      Edge case data for the statistic can also be provided. See the method javadoc for
@@ -96,7 +97,7 @@ import org.junit.jupiter.params.provider.MethodSource;
  *      The default uses the class name without the "Test" suffix.
  *  <li>{@link #isCombineSymmetric()}: Specify whether to test {@code a.combine(b)} is exactly
  *      equal to {@code b.combine(a)}. The default is {@code true}.
- *  <li>{@link #mapValue(double)}: A method to update the sample data to the valid domain
+ *  <li>{@link #mapValue(int)}: A method to update the sample data to the valid domain
  *      for the statistic. This can be used to alter the default test data, for example
  *      by mapping any negative values to positive.
  * </ul>
@@ -107,66 +108,36 @@ import org.junit.jupiter.params.provider.MethodSource;
  * @param <S> Statistic type
  */
 @TestInstance(Lifecycle.PER_CLASS)
-abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatisticAccumulator<S>> {
-    /** An empty {@code double[]}. */
-    private static final double[] EMPTY = {};
-    /** The number of random permutations to perform. */
-    private static final int RANDOM_PERMUTATIONS = 5;
-    /** The maximum number of values to output in assertion messages. This is used to provide
-     * the data for the failed test which is useful to identify the failed test case. */
-    private static final int MAX_FORMAT_VALUES = 100;
+abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumulator<S>>
+        extends BaseStatisticTest {
+    /** An empty {@code long[]}. */
+    private static final long[] EMPTY = {};
 
     // Standard test data samples using SciPy version 1.11:
     // import scipy.stats
 
     /** Uniform samples: scipy.stats.randint.rvs(-15, 25, size=100).
      * https://en.wikipedia.org/wiki/Discrete_uniform_distribution */
-    private static final double[] V1 = {20, 8, 1, 2, 24, -13, -1, 19, 0, 23, 14, 2, -9, 0, 7, -15, -2, -9, -1, 12, -12,
+    private static final long[] V1 = {20, 8, 1, 2, 24, -13, -1, 19, 0, 23, 14, 2, -9, 0, 7, -15, -2, -9, -1, 12, -12,
         9, 2, 12, 0, 5, 18, 15, -10, 6, 14, 6, 24, 12, -2, 1, -9, 17, 10, 7, 4, -8, 7, 7, -9, 13, -13, 0, -13, 6, 10,
         -6, 23, 2, 6, -10, 12, 6, -6, 4, -5, -9, 5, 16, -14, -4, 22, -4, 20, 12, -13, 6, -13, 12, -3, 13, 1, -6, 13,
         -14, -8, 4, 24, 10, 22, 8, -3, -5, 6, 9, 2, 16, 20, -8, -5, 23, 19, 20, -10, 21};
     /** Poisson samples: scipy.stats.poisson.rvs(1.5, size=100).
      * https://en.wikipedia.org/wiki/Poisson_distribution */
-    private static final double[] V2 = {3, 0, 3, 4, 0, 1, 1, 3, 2, 1, 3, 1, 5, 0, 0, 0, 3, 3, 2, 0, 3, 1, 2, 1, 1, 0, 0,
+    private static final long[] V2 = {3, 0, 3, 4, 0, 1, 1, 3, 2, 1, 3, 1, 5, 0, 0, 0, 3, 3, 2, 0, 3, 1, 2, 1, 1, 0, 0,
         1, 0, 1, 3, 0, 0, 2, 2, 0, 2, 1, 2, 1, 3, 2, 1, 2, 0, 2, 2, 1, 2, 0, 3, 2, 0, 1, 2, 2, 1, 2, 1, 0, 2, 0, 2, 2,
         3, 2, 1, 1, 0, 1, 2, 1, 0, 0, 6, 2, 0, 1, 1, 1, 1, 2, 2, 2, 2, 0, 2, 1, 2, 4, 2, 4, 2, 0, 0, 1, 1, 2, 2, 3};
     /** Poisson samples: scipy.stats.poisson.rvs(4.5, size=100). */
-    private static final double[] V3 = {1, 5, 5, 2, 1, 7, 5, 5, 3, 5, 5, 2, 4, 4, 6, 2, 7, 5, 3, 6, 7, 3, 5, 3, 7, 6, 4,
+    private static final long[] V3 = {1, 5, 5, 2, 1, 7, 5, 5, 3, 5, 5, 2, 4, 4, 6, 2, 7, 5, 3, 6, 7, 3, 5, 3, 7, 6, 4,
         3, 3, 3, 3, 3, 5, 3, 2, 8, 2, 4, 3, 1, 2, 5, 4, 3, 5, 4, 4, 8, 6, 2, 4, 3, 5, 5, 6, 3, 1, 6, 8, 6, 3, 6, 10, 4,
         5, 2, 4, 2, 2, 8, 2, 2, 4, 1, 4, 2, 5, 3, 2, 4, 4, 6, 9, 4, 5, 9, 9, 6, 3, 5, 3, 3, 5, 7, 5, 2, 3, 7, 4, 5};
     /** Poisson samples: scipy.stats.poisson.rvs(45, size=100). */
-    private static final double[] V4 = {42, 51, 38, 38, 49, 48, 42, 47, 51, 46, 45, 35, 39, 42, 49, 55, 53, 46, 49, 56,
+    private static final long[] V4 = {42, 51, 38, 38, 49, 48, 42, 47, 51, 46, 45, 35, 39, 42, 49, 55, 53, 46, 49, 56,
         42, 46, 42, 53, 43, 55, 49, 52, 51, 45, 40, 49, 39, 40, 46, 43, 46, 48, 36, 44, 40, 49, 49, 43, 45, 44, 41, 55,
         52, 45, 57, 41, 43, 44, 38, 52, 44, 45, 43, 42, 38, 37, 47, 42, 47, 45, 70, 45, 50, 47, 46, 50, 47, 35, 43, 52,
         51, 41, 45, 42, 45, 53, 46, 48, 51, 43, 63, 48, 49, 41, 58, 51, 59, 43, 39, 32, 35, 46, 50, 50};
-    /** Normal samples: scipy.stats.norm.rvs(loc=3.4, scale=2.25, size=100). */
-    private static final double[] V5 = {1.06356579, -1.52552007, 7.09739891, -0.41516549, 0.17131653, 0.77923148,
-        2.90491862, 4.12648256, 5.04920689, 4.20053484, 5.83485097, 4.33138009, 4.18795702, 3.269289, 2.2399589,
-        4.16551591, -1.67192439, 1.44919254, 3.52270229, -1.49186865, -0.30794835, 5.82394621, 4.84755567, 4.79622486,
-        5.12461983, 2.62561931, 5.12457788, 8.24460895, 4.91249002, 3.75550863, 4.35440479, 4.17587334, -0.34934393,
-        2.98071452, -1.35620308, 1.93956508, 7.57171999, 5.41976186, 2.8427556, 3.04101193, 2.20374721, 4.65406057,
-        5.76961878, 3.14412957, 7.60322297, 1.598286, 2.51552974, 0.67767289, 0.76514432, 3.65663671, 0.53116457,
-        2.79439061, 7.58564809, 4.16735822, 2.95210392, 6.37867376, 6.57010411, 0.11837698, 9.16270054, 3.80097588,
-        5.48811672, 3.83378268, 2.03669252, 5.34865676, 3.11338528, 4.70088345, 6.00069684, 0.16144587, 4.22654482,
-        2.2722623, 5.39142224, 0.811471, 2.74523433, 6.32457234, 0.73033045, 9.54402353, 0.4800466, 2.00806359,
-        6.06115109, 2.3072464, 5.40974674, 2.05533169, 0.97160161, 8.06915145, 4.40792026, 4.53139251, 3.32350119,
-        1.53645238, 3.49059212, 3.57904997, 0.58634639, 5.87567911, 3.49424866, 5.72228178, 4.41403447, 1.27815121,
-        7.13861948, 4.68209093, 6.4598438, 0.66270586};
-    /** Gamma samples: scipy.stats.gamma.rvs(9, scale=0.5, size=100). */
-    private static final double[] V6 = {3.46479451, 8.80950045, 3.91437318, 4.23327834, 2.6910161, 4.51122052,
-        5.81939474, 3.9142699, 7.75537607, 6.06693317, 3.29388792, 3.90689471, 3.26357137, 3.6398822, 5.60048428,
-        3.68248997, 5.09297897, 4.6302593, 7.01654777, 4.2244833, 2.75326355, 5.36988549, 2.88392811, 3.50131464,
-        4.81183009, 4.92155284, 4.37061644, 3.8064197, 3.31941113, 5.01257676, 3.48037207, 2.62777255, 6.2447332,
-        6.18425783, 3.06915179, 6.42851381, 3.8969583, 3.48723372, 3.49516941, 2.90404439, 2.25920041, 3.68515649,
-        5.09607663, 3.18984299, 2.49261713, 3.9345895, 6.01480539, 8.8065787, 4.3464082, 5.03522483, 4.05315513,
-        6.07365399, 4.34804323, 5.16061656, 3.24706079, 2.89888437, 6.24575902, 3.10893227, 3.74196045, 3.94099137,
-        3.33951846, 4.9264514, 5.21935748, 5.06904776, 2.77543623, 3.72451685, 6.35546017, 4.42425655, 5.99568005,
-        4.2602446, 3.75834066, 5.17730802, 3.60682583, 4.09703419, 5.59942582, 3.49191032, 3.02164323, 8.85183547,
-        5.58631958, 3.24891648, 4.03267796, 4.30984912, 3.78187375, 5.98969913, 1.68855026, 8.43117397, 3.808258,
-        4.82043745, 2.91020117, 4.12921107, 3.54350667, 4.60545934, 5.63180941, 5.07331453, 5.04419517, 3.78796082,
-        4.25143811, 6.3242129, 4.25630677, 4.59158821};
-
-    /** Sizes to cut the sample data into chunks. The {@link #cut(double[], int...)} method is robust
+
+    /** Sizes to cut the sample data into chunks. The {@link #cut(long[], int...)} method is robust
      * to the sizes summing to more than the sample length. If they sum below then the expected
      * value may be incorrect as all data is not used. These all sum to 100 which imposes a maximum
      * length on sample data. */
@@ -180,40 +151,43 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
         {10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
     };
 
-    /** Sizes to cut the non-finite sample data into chunks.
+    /** Sizes to cut the extreme value sample data into chunks.
      * These all sum to 10 which imposes a maximum length on sample data. */
-    private static final int[][] SIZES_NON_FINITE = {
+    private static final int[][] SIZES_EXTREME = {
         {5, 5},
         {3, 3, 4},
         {2, 2, 2, 2, 2},
     };
 
+    /** The statistic name. */
+    private String statisticName;
+
     /** The test data and expected values. */
     private final List<TestData> data = new ArrayList<>();
-    /** The non-finite test data and expected values. */
-    private final List<TestData> dataNonFinite = new ArrayList<>();
+    /** The extreme value test data and expected values. */
+    private final List<TestData> dataExtremeValue = new ArrayList<>();
     /** Cache of the custom test data. */
     private final List<StatisticTestData> dataCustom = new ArrayList<>();
 
-    /** The statistic name. */
-    private String statisticName;
-
     /**
      * Container for test data.
+     *
+     * <p>The statistics is expected to be either a {@code double}, {@code int}, {@code long}
+     * of {@code BigInteger} value.
      */
     private static class TestData {
         /** The sample values. */
-        private final double[] values;
+        private final long[] values;
         /** The expected statistic value. */
-        private final Double expected;
+        private final StatisticResult expected;
 
         /**
-         * @param expected Expected statistic value.
          * @param values Sample values.
+         * @param expected Expected statistic value.
          */
-        TestData(double[] values, double expected) {
+        TestData(long[] values, StatisticResult expected) {
             this.values = values;
-            this.expected = Double.valueOf(expected);
+            this.expected = expected;
         }
 
         /**
@@ -221,7 +195,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
          *
          * @return the values
          */
-        double[] getValues() {
+        long[] getValues() {
             return values.clone();
         }
 
@@ -230,13 +204,14 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
          *
          * @return the expected
          */
-        Double getExpected() {
+        StatisticResult getExpected() {
             return expected;
         }
     }
 
     /**
-     * Container for test data with tolerances for the tests.
+     * Container for test data with tolerances for the tests. Tolerances are used only
+     * for expected values as a {@code double}.
      */
     static class StatisticTestData extends TestData {
         /** The tolerance for the accept test. */
@@ -248,12 +223,24 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
         /** The tolerance for the array+combine test. */
         private final DoubleTolerance tolArrayCombine;
 
+        /**
+         * @param values Sample values.
+         * @param expected Expected statistic value.
+         */
+        StatisticTestData(long[] values, StatisticResult expected) {
+            super(values, expected);
+            this.tolAccept = null;
+            this.tolArray = null;
+            this.tolAcceptCombine = null;
+            this.tolArrayCombine = null;
+        }
+
         /**
          * @param values Sample values.
          * @param expected Expected statistic value.
          * @param tol Tolerance for the tests.
          */
-        StatisticTestData(double[] values, double expected, DoubleTolerance tol) {
+        StatisticTestData(long[] values, StatisticResult expected, DoubleTolerance tol) {
             this(values, expected, tol, tol, tol, tol);
         }
 
@@ -265,7 +252,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
          * @param tolAcceptCombine Tolerance for the test of the statistic using the accept and combine methods.
          * @param tolArrayCombine Tolerance for the test of the statistic using the array create and combine methods.
          */
-        StatisticTestData(double[] values, double expected,
+        StatisticTestData(long[] values, StatisticResult expected,
                 DoubleTolerance tolAccept, DoubleTolerance tolArray,
                 DoubleTolerance tolAcceptCombine, DoubleTolerance tolArrayCombine) {
             super(values, expected);
@@ -277,7 +264,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
         /**
          * Gets the tolerance for the
-         * {@link BaseDoubleStatisticTest#testAccept(double[], double, DoubleTolerance)}
+         * {@link BaseLongStatisticTest#testAccept(long[], StatisticResult, DoubleTolerance)}
          * method.
          *
          * @return the tolerance
@@ -288,7 +275,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
         /**
          * Gets the tolerance for the
-         * {@link BaseDoubleStatisticTest#testArray(double[], double, DoubleTolerance)
+         * {@link BaseLongStatisticTest#testArray(long[], StatisticResult, DoubleTolerance)
          * )} method.
          *
          * @return the tolerance
@@ -299,7 +286,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
         /**
          * Gets the tolerance for the
-         * {@link BaseDoubleStatisticTest#testAcceptAndCombine(double[][], double, DoubleTolerance)}
+         * {@link BaseLongStatisticTest#testAcceptAndCombine(long[][], StatisticResult, DoubleTolerance)}
          * method.
          *
          * @return the tolerance
@@ -310,7 +297,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
         /**
          * Gets the tolerance for the
-         * {@link BaseDoubleStatisticTest#testArrayAndCombine(double[][], double, DoubleTolerance)}
+         * {@link BaseLongStatisticTest#testArrayAndCombine(long[][], StatisticResult, DoubleTolerance)}
          * method.
          *
          * @return the tolerance
@@ -330,60 +317,39 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     void setup() {
         statisticName = getStatisticName();
         // Compute the expected value for the standard test data
-        Stream.of(V1, V2, V3, V4, V5, V6).forEach(values -> {
-            final double[] sample = new double[values.length];
+        Stream.of(V1, V2, V3, V4).forEach(values -> {
+            final long[] sample = new long[values.length];
             for (int i = 0; i < sample.length; i++) {
                 sample[i] = mapValue(values[i]);
             }
-            final double expected = getExpectedValue(sample);
-            data.add(new TestData(sample, expected));
+            data.add(new TestData(values, getExpectedValue(values)));
         });
-        // Data with non-finite values.
+        // Data with extreme values.
         // Note: The order does not matter as this is shuffled
         // in the random order tests.
-        final double a = Double.NaN;
-        final double b = Double.POSITIVE_INFINITY;
-        final double c = Double.NEGATIVE_INFINITY;
-        final double[][] nonFinite = {
+        final int a = Integer.MAX_VALUE;
+        final int b = Integer.MIN_VALUE;
+        final long[][] extreme = {
             {a, 1, 1, 1, 1, 1, 1, 1, 1, 1},
             {b, 1, 1, 1, 1, 1, 1, 1, 1, 1},
-            {c, 1, 1, 1, 1, 1, 1, 1, 1, 1},
             {a, a, 1, 1, 1, 1, 1, 1, 1, 1},
             {b, b, 1, 1, 1, 1, 1, 1, 1, 1},
-            {c, c, 1, 1, 1, 1, 1, 1, 1, 1},
             {a, b, 1, 1, 1, 1, 1, 1, 1, 1},
-            {a, c, 1, 1, 1, 1, 1, 1, 1, 1},
-            {b, c, 1, 1, 1, 1, 1, 1, 1, 1},
-            {a, b, c, 1, 1, 1, 1, 1, 1, 1},
         };
-        Stream.of(nonFinite).forEach(values -> {
-            final double[] sample = values.clone();
+        Stream.of(extreme).forEach(values -> {
+            final long[] sample = values.clone();
             for (int i = sample.length; i-- > 0;) {
-                if (!Double.isFinite(values[i])) {
+                if (values[i] == 1) {
                     break;
                 }
                 sample[i] = mapValue(sample[i]);
             }
-            final double expected = getExpectedNonFiniteValue(sample);
-            dataNonFinite.add(new TestData(sample, expected));
+            dataExtremeValue.add(new TestData(values, getExpectedValue(values)));
         });
         // Cache the custom test data so any computed expected values are calculated only once
         streamTestData().forEach(dataCustom::add);
     }
 
-    /**
-     * Gets the statistic name.
-     *
-     * <p>The default implementation removes the text {@code "Test"} from the
-     * simple class name.
-     *
-     * @return the distribution name
-     * @see Class#getSimpleName()
-     */
-    protected String getStatisticName() {
-        return getClass().getSimpleName().replace("Test", "");
-    }
-
     /**
      * Creates the statistic.
      *
@@ -397,44 +363,20 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param values Values.
      * @return the statistic
      */
-    protected abstract S create(double... values);
-
-    /**
-     * Get the maximum number of values that can be added where the statistic is
-     * considered empty.
-     *
-     * <p>The default is zero. Override this method if the statistic requires multiple
-     * values to compute.
-     *
-     * @return the empty size
-     * @see #getEmptyValue()
-     */
-    protected int getEmptySize() {
-        return 0;
-    }
-
-    /**
-     * Gets the value of the statistics when it is considered empty.
-     *
-     * @return the empty value
-     * @see #getEmptySize()
-     */
-    protected abstract double getEmptyValue();
+    protected abstract S create(long... values);
 
     /**
      * Map the {@code value} to the valid domain of the statistic. This method is called
-     * with the example data before {@link #getExpectedValue(double[])} and
-     * {@link #getExpectedNonFiniteValue(double[])}. In the later case the method is only
-     * called with finite value. It can be used by implementing classes to adjust the data
-     * to a valid domain, for example if computing the sum-of-logs all values can be
-     * updated to be strictly positive.
+     * with the example data before {@link #getExpectedValue(long[])}. It can be used by
+     * implementing classes to adjust the data to a valid domain, for example if computing
+     * the sum-of-logs all values can be updated to be strictly positive.
      *
      * <p>The default implementation does nothing.
      *
      * @param value Value
      * @return the mapped value
      */
-    protected double mapValue(double value) {
+    protected long mapValue(long value) {
         return value;
     }
 
@@ -444,166 +386,71 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * <p>This method should be implemented using the canoncial definition of the statistic,
      * ideally in high-precision (e.g. using BigDecimal for statistics involving arithmetic).
      *
-     * <p>This method will not be called with non-finite values.
-     *
      * @param values Values.
      * @return the expected value
      */
-    protected abstract double getExpectedValue(double[] values);
+    protected abstract StatisticResult getExpectedValue(long[] values);
 
     /**
-     * Gets the expected value of the statistic from the {@code values}, at least one
-     * of which will be non-finite. This method will be called with arrays that contain
-     * combinations of {@code NaN} and {@code +/-infinity}.
+     * Creates the equivalent {@link DoubleStatistic} from the {@code values}.
+     * This is used to cross-validate the {@link LongStatistic} result.
      *
      * @param values Values.
-     * @return the expected value
-     */
-    protected abstract double getExpectedNonFiniteValue(double[] values);
-
-    /**
-     * Gets the tolerance for equality of the statistic and the expected value.
-     * This method is not used by the base implementation except to provide the default
-     * implementation for the individual test tolerance methods.
-     *
-     * <p><strong>Note</strong>
-     *
-     * <p>The default implementation throws an exception. This forces the
-     * implementation to either provide a single tolerance for all tests, or the tests
-     * individually by overriding each tolerance method.
-     *
-     * @return the tolerance
-     */
-    protected DoubleTolerance getTolerance() {
-        throw new IllegalStateException("Not all test tolerances have been configured");
-    }
-
-    /**
-     * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testAccept(double[], double, DoubleTolerance)} test.
-     *
-     * <p>The default implementation uses {@link #getTolerance()}.
-     *
-     * @return the tolerance
-     */
-    protected DoubleTolerance getToleranceAccept() {
-        return getTolerance();
-    }
-
-    /**
-     * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testArray(double[], double, DoubleTolerance)} test.
-     *
-     * <p>The default implementation uses {@link #getTolerance()}.
-     *
-     * @return the tolerance
-     */
-    protected DoubleTolerance getToleranceArray() {
-        return getTolerance();
-    }
-
-    /**
-     * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testAcceptAndCombine(double[][], double, DoubleTolerance)} test.
-     *
-     * <p>The default implementation uses {@link #getTolerance()}.
-     *
-     * @return the tolerance
+     * @return the statistic
      */
-    protected DoubleTolerance getToleranceAcceptAndCombine() {
-        return getTolerance();
-    }
+    protected abstract DoubleStatistic createAsDoubleStatistic(long... values);
 
-    /**
-     * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testArrayAndCombine(double[][], double, DoubleTolerance)} test.
-     *
-     * <p>The default implementation uses {@link #getTolerance()}.
-     *
-     * @return the tolerance
-     */
-    protected DoubleTolerance getToleranceArrayAndCombine() {
-        return getTolerance();
-    }
+    //------------------------ Helper Methods to create test data ---------------------------
 
     /**
-     * Gets the tolerance for equality of the statistic and the expected value
-     * for any test that includes data with non-finite values.
-     *
-     * <p>The default implementation uses binary equality.
-     *
-     * <p>Note: This tolerance has not been separated for different tests that use
-     * the {@code accept}, {@link #create(double...)} and {@code combine} methods.
-     * Computation of a statistic from non-finite values should produce either
-     * {@code NaN} or {@code +/-infinity}. However this is not enforced as the expected
-     * value is computed by implementations.
+     * Creates the test data.
+     * The expected value must be provided.
      *
-     * @return the tolerance
-     * {@link #getExpectedNonFiniteValue(double[])}
-     */
-    protected DoubleTolerance getToleranceNonFinite() {
-        return DoubleTolerances.equals();
-    }
-
-    /**
-     * Returns {@code true} if the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
-     * method is symmetric. If {@code true} then the combine will be tested with
-     * duplicate instances of the statistic, combined in left-to-right and right-to-left;
-     * the result must have the same statistic value.
+     * <p>This method can be used to add data computed with external reference implementations.
      *
-     * <p>The default is {@code true}. Override this if the implementation is not symmetric
-     * due to the implementation details.
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code int}.
      *
-     * @return {@code true} if combine is symmetric
+     * @param expected Expected statistic value.
+     * @param values Values.
+     * @return the statistic test data
      */
-    protected boolean isCombineSymmetric() {
-        return true;
+    final StatisticTestData addReference(int expected, long... values) {
+        Assertions.assertEquals(ResultType.INT, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected));
     }
 
-    //------------------------ Helper Methods to create test data ---------------------------
-
     /**
-     * Creates the tolerance using an absolute error.
-     *
-     * <p>If the absolute tolerance is zero it is ignored and a tolerance of numerical
-     * equality is used.
+     * Creates the test data.
+     * The expected value must be provided.
      *
-     * @param eps Absolute tolerance
-     * @return the tolerance
-     */
-    static final DoubleTolerance createAbsTolerance(double eps) {
-        return eps > 0 ? DoubleTolerances.absolute(eps) : DoubleTolerances.ulps(0);
-    }
-
-    /**
-     * Creates the tolerance using an relative error.
+     * <p>This method can be used to add data computed with external reference implementations.
      *
-     * <p>If the relative tolerance is zero it is ignored and a tolerance of numerical
-     * equality is used.
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code long}.
      *
-     * @param eps Relative tolerance
-     * @return the tolerance
+     * @param expected Expected statistic value.
+     * @param values Values.
+     * @return the statistic test data
      */
-    static final DoubleTolerance createRelTolerance(double eps) {
-        return eps > 0 ? DoubleTolerances.relative(eps) : DoubleTolerances.ulps(0);
+    final StatisticTestData addReference(long expected, long... values) {
+        Assertions.assertEquals(ResultType.LONG, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected));
     }
 
     /**
-     * Creates the tolerance using an {@code Or} combination of absolute and relative error.
+     * Creates the test data.
+     * The expected value must be provided.
      *
-     * <p>If the absolute tolerance is zero it is ignored and a tolerance of numerical equality
-     * is used.
+     * <p>This method can be used to add data computed with external reference implementations.
      *
-     * <p>If the relative tolerance is zero it is ignored.
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code BigInteger}.
      *
-     * @param absTolerance Absolute tolerance
-     * @param relTolerance Relative tolerance
-     * @return the tolerance
+     * @param expected Expected statistic value.
+     * @param values Values.
+     * @return the statistic test data
      */
-    static final DoubleTolerance createAbsOrRelTolerance(double absTolerance, double relTolerance) {
-        final DoubleTolerance tol = createAbsTolerance(absTolerance);
-        return relTolerance > 0 ? tol.or(DoubleTolerances.relative(relTolerance)) : tol;
+    final StatisticTestData addReference(BigInteger expected, long... values) {
+        Assertions.assertEquals(ResultType.BIG_INTEGER, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected));
     }
 
     /**
@@ -613,6 +460,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      *
      * <p>This method can be used to add data computed with external reference implementations.
      *
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code double}.
+     *
      * @param expected Expected statistic value.
      * @param values Values.
      * @return the statistic test data
@@ -621,8 +470,9 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @see #getToleranceAcceptAndCombine()
      * @see #getToleranceArrayAndCombine()
      */
-    final StatisticTestData addReference(double expected, double... values) {
-        return new StatisticTestData(values, expected,
+    StatisticTestData addReference(double expected, long... values) {
+        Assertions.assertEquals(ResultType.DOUBLE, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected),
             getToleranceAccept(), getToleranceArray(),
             getToleranceAcceptAndCombine(), getToleranceArrayAndCombine());
     }
@@ -633,13 +483,16 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      *
      * <p>This method can be used to add data computed with external reference implementations.
      *
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code double}.
+     *
      * @param expected Expected statistic value.
      * @param tol Test tolerance.
      * @param values Values.
      * @return the statistic test data
      */
-    static final StatisticTestData addReference(double expected, DoubleTolerance tol, double... values) {
-        return new StatisticTestData(values, expected, tol);
+    final StatisticTestData addReference(double expected, DoubleTolerance tol, long... values) {
+        Assertions.assertEquals(ResultType.DOUBLE, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected), tol);
     }
 
     /**
@@ -648,6 +501,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      *
      * <p>This method can be used to add data computed with external reference implementations.
      *
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code double}.
+     *
      * @param expected Expected statistic value.
      * @param tolAccept Tolerance for the test of the statistic using the accept method.
      * @param tolArray Tolerance for the test of the statistic using the array create method.
@@ -656,11 +511,12 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param values Values.
      * @return the statistic test data
      */
-    static final StatisticTestData addReference(double expected,
+    final StatisticTestData addReference(double expected,
             DoubleTolerance tolAccept, DoubleTolerance tolArray,
             DoubleTolerance tolAcceptCombine, DoubleTolerance tolArrayCombine,
-            double... values) {
-        return new StatisticTestData(values, expected,
+            long... values) {
+        Assertions.assertEquals(ResultType.DOUBLE, getResultType());
+        return new StatisticTestData(values, createStatisticResult(expected),
             tolAccept, tolArray, tolAcceptCombine, tolArrayCombine);
     }
 
@@ -673,38 +529,46 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      *
      * @param values Values.
      * @return the statistic test data
-     * @see #getExpectedValue(double[])
+     * @see #getExpectedValue(long[])
      * @see #getToleranceAccept()
      * @see #getToleranceArray()
      * @see #getToleranceAcceptAndCombine()
      * @see #getToleranceArrayAndCombine()
      */
-    final StatisticTestData addCase(double... values) {
-        return new StatisticTestData(values, getExpectedValue(values),
-            getToleranceAccept(), getToleranceArray(),
-            getToleranceAcceptAndCombine(), getToleranceArrayAndCombine());
+    final StatisticTestData addCase(long... values) {
+        if (getResultType() == ResultType.DOUBLE) {
+            return new StatisticTestData(values, getExpectedValue(values),
+                getToleranceAccept(), getToleranceArray(),
+                getToleranceAcceptAndCombine(), getToleranceArrayAndCombine());
+        }
+        return new StatisticTestData(values, getExpectedValue(values));
     }
 
     /**
      * Creates the edge-case/example test data. The same tolerance is used for all tests.
-     * The expected value will be computed using {@link #getExpectedValue(double[])}.
+     * The expected value will be computed using {@link #getExpectedValue(long[])}.
      *
      * <p>This method can be used to add data to target specific edge cases.
      *
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code double}.
+     *
      * @param tol Test tolerance.
      * @param values Values.
      * @return the statistic test data
      */
-    final StatisticTestData addCase(DoubleTolerance tol, double... values) {
+    final StatisticTestData addCase(DoubleTolerance tol, long... values) {
+        Assertions.assertEquals(ResultType.DOUBLE, getResultType());
         return new StatisticTestData(values, getExpectedValue(values), tol);
     }
 
     /**
      * Creates the edge-case/example test data.
-     * The expected value will be computed using {@link #getExpectedValue(double[])}.
+     * The expected value will be computed using {@link #getExpectedValue(long[])}.
      *
      * <p>This method can be used to add data to target specific edge cases.
      *
+     * <p>Note: Throws an exception if {@link #getResultType()} is not {@code double}.
+     *
      * @param tolAccept Tolerance for the test of the statistic using the accept method.
      * @param tolArray Tolerance for the test of the statistic using the array create method.
      * @param tolAcceptCombine Tolerance for the test of the statistic using the accept and combine methods.
@@ -715,7 +579,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     final StatisticTestData addCase(
             DoubleTolerance tolAccept, DoubleTolerance tolArray,
             DoubleTolerance tolAcceptCombine, DoubleTolerance tolArrayCombine,
-            double... values) {
+            long... values) {
+        Assertions.assertEquals(ResultType.DOUBLE, getResultType());
         return new StatisticTestData(values, getExpectedValue(values),
             tolAccept, tolArray, tolAcceptCombine, tolArrayCombine);
     }
@@ -738,12 +603,12 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * will be used for all 4 main tests of the statistic:
      *
      * <ol>
-     *  <li>{@link java.util.function.DoubleConsumer#accept accept}
-     *  <li>{@link java.util.function.DoubleConsumer#accept accept} and
-     *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
-     *  <li>{@link #create(double...)}
-     *  <li>{@link #create(double...)} and
-     *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     *  <li>{@link java.util.function.LongConsumer#accept accept}
+     *  <li>{@link java.util.function.LongConsumer#accept accept} and
+     *      {@link StatisticAccumulator#combine(StatisticResult) combine}
+     *  <li>{@link #create(int...)}
+     *  <li>{@link #create(int...)} and
+     *      {@link StatisticAccumulator#combine(StatisticResult) combine}
      * </ol>
      *
      * <p>To test the {@code combine} method the data is split into approximately even groups
@@ -759,7 +624,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      *   add(3.5, createRelTolerance(1e-15), 2, 3, 4, 5),
      *   add(11.5, createRelTolerance(1e-15), 2, 3, 4, 5, 13, 42),
      *   // Edge case for mean
-     *   add(createRelTolerance(1e-15), Double.MAX_VALUE, -Double.MAX_VALUE)
+     *   add(createRelTolerance(1e-15), Int.MAX_VALUE, -Int.MAX_VALUE)
      * )}
      * </pre>
      *
@@ -769,20 +634,11 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      */
     protected abstract Stream<StatisticTestData> streamTestData();
 
-    /**
-     * Gets the group sizes used to test the combine method with the custom test data.
-     *
-     * @return the group sizes
-     */
-    protected int[] getCombineGroupSizes() {
-        return new int[] {2, 3};
-    }
-
     //------------------------ Methods to stream the test data -----------------------------
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept accept} method. The expected value
+     * {@link java.util.function.LongConsumer#accept accept} method. The expected value
      * and tolerance are supplied by the implementing class.
      *
      * @return the stream
@@ -793,7 +649,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link #create(double...)} method. The expected value and tolerance are supplied
+     * {@link #create(int...)} method. The expected value and tolerance are supplied
      * by the implementing class.
      *
      * @return the stream
@@ -802,9 +658,25 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
         return streamSingleArrayData(getToleranceArray(), StatisticTestData::getTolArray);
     }
 
+    /**
+     * Test the computation of the statistic against the equivalent {@link DoubleStatistic}.
+     * The result is tested as a {@code double} using the configured {@link #getToleranceAsDouble()}.
+     * Integer values are not asserted as they may not be an exact match.
+     */
+    @ParameterizedTest
+    @MethodSource(value = {"testAccept"})
+    final void testVsDoubleStatistic(long[] values) {
+        final double expected = createAsDoubleStatistic(values).getAsDouble();
+        final DoubleTolerance tol = getToleranceAsDouble();
+        TestUtils.assertEquals(expected, Statistics.add(create(), values).getAsDouble(), tol,
+            () -> statisticName + " accept: " + format(values));
+        TestUtils.assertEquals(expected, create(values).getAsDouble(), tol,
+            () -> statisticName + " array: " + format(values));
+    }
+
     /**
      * Stream the arguments to test the computation of the statistic using a single
-     * {@code double[]} array.
+     * {@code long[]} array.
      *
      * @param tol Test tolerance.
      * @param tolCustom Tolerance for any custom test data.
@@ -814,15 +686,15 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
             Function<StatisticTestData, DoubleTolerance> tolCustom) {
         final Builder<Arguments> b = Stream.builder();
         data.forEach(d -> b.accept(Arguments.of(d.getValues(), d.getExpected(), tol)));
-        dataNonFinite.forEach(d -> b.accept(Arguments.of(d.getValues(), d.getExpected(), getToleranceNonFinite())));
+        dataExtremeValue.forEach(d -> b.accept(Arguments.of(d.getValues(), d.getExpected(), tol)));
         dataCustom.forEach(d -> b.accept(Arguments.of(d.getValues(), d.getExpected(), tolCustom.apply(d))));
         return b.build();
     }
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
+     * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method. The expected value and tolerance are supplied by the implementing class.
      *
      * @return the stream
@@ -833,8 +705,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link #create(double...)} method for each array, then the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method. The
+     * {@link #create(int...)} method for each array, then the
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method. The
      * expected value and tolerance are supplied by the implementing class.
      *
      * @return the stream
@@ -845,9 +717,9 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
+     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
      * element of a parallel stream, then the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      * The expected value and tolerance are supplied by the implementing class.
      *
      * <p>Note that this method uses the tolerance from {@link #getToleranceAcceptAndCombine()}.
@@ -860,8 +732,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Stream the arguments to test the computation of the statistic using the multiple
-     * {@code double[]} arrays and the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@code long[]} arrays and the
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      *
      * @param tol Test tolerance.
      * @param tolCustom Tolerance for any custom test data.
@@ -876,15 +748,15 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
                   .map(x -> cut(d.getValues(), x))
                   .forEach(e -> b.accept(Arguments.of(e, d.getExpected(), tol)));
         });
-        dataNonFinite.forEach(d -> {
+        dataExtremeValue.forEach(d -> {
             // Cut the standard data into pieces
-            Arrays.stream(SIZES_NON_FINITE)
+            Arrays.stream(SIZES_EXTREME)
                   .map(x -> cut(d.getValues(), x))
-                  .forEach(e -> b.accept(Arguments.of(e, d.getExpected(), getToleranceNonFinite())));
+                  .forEach(e -> b.accept(Arguments.of(e, d.getExpected(), tol)));
         });
         dataCustom.forEach(d -> {
             // Only split lengths above the number of groups
-            final double[] values = d.getValues();
+            final long[] values = d.getValues();
             final int n = values.length;
             for (final int groups : getCombineGroupSizes()) {
                 if (groups <= n) {
@@ -905,8 +777,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param sizes Sizes.
      * @return the set of arrays
      */
-    private static double[][] cut(double[] data, int... sizes) {
-        ArrayList<double[]> set = new ArrayList<>();
+    private static long[][] cut(long[] data, int... sizes) {
+        ArrayList<long[]> set = new ArrayList<>();
         int from = 0;
         for (final int size : sizes) {
             final int to = Math.min(data.length, from + size);
@@ -922,7 +794,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
             Assertions.fail(
                 () -> "Sizes " + Arrays.toString(sizes) + " do not cover length " + data.length);
         }
-        return set.toArray(new double[0][]);
+        return set.toArray(new long[0][]);
     }
 
     /**
@@ -933,14 +805,14 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param groups Number of groups.
      * @return the set of arrays
      */
-    private static double[][] split(double[] data, int groups) {
+    private static long[][] split(long[] data, int groups) {
         // Note: round the size of the group.
         // The final group accounts for length difference:
         // 4/3 => 1, 1, 2
         // 5/3 => 2, 2, 1
         // 7/3 => 2, 2, 3
         final int size = (int) Math.round((double) data.length / groups);
-        final double[][] values = new double[groups][];
+        final long[][] values = new long[groups][];
         for (int i = 0; i < groups; i++) {
             final int from = i * size;
             // Final group may be smaller or larger
@@ -975,106 +847,106 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param tol Non-empty tolerance.
      */
     private void assertEmpty(S stat, DoubleTolerance tol) {
-        final double v = getEmptyValue();
+        final StatisticResult v = getEmptyValue();
         final int size = getEmptySize();
-        final double[] values = new double[size + 1];
+        final long[] values = new long[size + 1];
         // Fill beyond empty
         for (int i = 0; i <= size; i++) {
-            Assertions.assertEquals(v, stat.getAsDouble(), () -> statisticName + " should be empty");
+            TestHelper.assertEquals(v, stat, tol, () -> statisticName + " should be empty");
             stat.accept(i + 1);
             values[i] = i + 1;
         }
-        final double expected = getExpectedValue(values);
-        TestUtils.assertEquals(expected, stat.getAsDouble(), tol,
+        final StatisticResult expected = getExpectedValue(values);
+        TestHelper.assertEquals(expected, stat, tol,
             () -> statisticName + " should not be empty");
     }
 
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method. The
+     * {@link java.util.function.LongConsumer#accept(int) accept} method. The
      * statistic is created using both the {@link #create()} and the
-     * {@link #create(double...)} methods; the two instances must compute the same result.
+     * {@link #create(int...)} methods; the two instances must compute the same result.
      */
     @ParameterizedTest
     @MethodSource
-    final void testAccept(double[] values, double expected, DoubleTolerance tol) {
-        final double actual = assertStatistic(v -> Statistics.add(create(), v), values, expected, tol);
-        // Repeat after creation with an empty array
-        assertStatistic(v -> Statistics.add(create(EMPTY), v), values, actual, tol);
+    final void testAccept(long[] values, StatisticResult expected, DoubleTolerance tol) {
+        final S actual = assertStatistic(v -> Statistics.add(create(), v), values, expected, tol);
+        // Repeat after creation with an empty array. No tolerance for exact equality.
+        assertStatistic(v -> Statistics.add(create(EMPTY), v), values, actual, null);
     }
 
     /**
-     * Test the computation of the statistic using the {@link #create(double...)} method.
+     * Test the computation of the statistic using the {@link #create(int...)} method.
      */
     @ParameterizedTest
     @MethodSource
-    final void testArray(double[] values, double expected, DoubleTolerance tol) {
+    final void testArray(long[] values, StatisticResult expected, DoubleTolerance tol) {
         assertStatistic(this::create, values, expected, tol);
     }
 
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
+     * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      */
     @ParameterizedTest
     @MethodSource
-    final void testAcceptAndCombine(double[][] values, double expected, DoubleTolerance tol) {
+    final void testAcceptAndCombine(long[][] values, StatisticResult expected, DoubleTolerance tol) {
         assertCombine(v -> Statistics.add(create(), v), values, expected, tol);
     }
 
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
+     * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      */
     @ParameterizedTest
     @MethodSource
-    final void testArrayAndCombine(double[][] values, double expected, DoubleTolerance tol) {
+    final void testArrayAndCombine(long[][] values, StatisticResult expected, DoubleTolerance tol) {
         assertCombine(this::create, values, expected, tol);
     }
 
     /**
-     * Test the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method
+     * Test the {@link StatisticAccumulator#combine(StatisticResult) combine} method
      * with an empty instance combined with a non-empty instance.
      */
     @ParameterizedTest
     @MethodSource(value = "testAccept")
-    final void testEmptyCombine(double[] values) {
+    final void testEmptyCombine(long[] values) {
         final S empty = create();
         final S nonEmpty = Statistics.add(create(), values);
-        final double expected = nonEmpty.getAsDouble();
+        final StatisticResult expected = Statistics.add(create(), values);
         final S result = assertCombine(empty, nonEmpty);
-        Assertions.assertEquals(expected, result.getAsDouble(),
+        TestHelper.assertEquals(expected, result, null,
             () -> statisticName + " empty.combine(nonEmpty)");
     }
 
     /**
-     * Test the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method
+     * Test the {@link StatisticAccumulator#combine(StatisticResult) combine} method
      * with a non-empty instance combined with an empty instance.
      */
     @ParameterizedTest
     @MethodSource(value = "testAccept")
-    final void testCombineEmpty(double[] values) {
+    final void testCombineEmpty(long[] values) {
         final S empty = create();
         final S nonEmpty = Statistics.add(create(), values);
-        final double expected = nonEmpty.getAsDouble();
+        final StatisticResult expected = Statistics.add(create(), values);
         final S result = assertCombine(nonEmpty, empty);
-        Assertions.assertEquals(expected, result.getAsDouble(),
+        TestHelper.assertEquals(expected, result, null,
             () -> statisticName + " nonEmpty.combine(empty)");
     }
 
     /**
      * Test the computation of the statistic using a parallel stream of {@code double}
      * values. The accumulator is the
-     * {@link java.util.function.DoubleConsumer#accept(double) accept} method; the
-     * combiner is the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * {@link java.util.function.LongConsumer#accept(int) accept} method; the
+     * combiner is the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      *
      * <p>Note: This method is similar to the
-     * {@link #testAcceptAndCombine(double[][], double, DoubleTolerance)} method and uses
+     * {@link #testAcceptAndCombine(long[][], StatisticResult, DoubleTolerance)} method and uses
      * the same data, concatenated to a single array, and tolerance. The difference is the
      * former method uses explicit subsets of the data and its own fixed algorithm to
      * combine instances. This method leaves the entire process of dividing the data and
@@ -1083,22 +955,21 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      */
     @ParameterizedTest
     @MethodSource
-    final void testAcceptParallelStream(double[] values, double expected, DoubleTolerance tol) {
-        final double actual = Arrays.stream(values)
+    final void testAcceptParallelStream(long[] values, StatisticResult expected, DoubleTolerance tol) {
+        final S actual = Arrays.stream(values)
             .parallel()
-            .collect(this::create, DoubleStatistic::accept, DoubleStatisticAccumulator::combine)
-            .getAsDouble();
-        TestUtils.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
+            .collect(this::create, LongStatistic::accept, StatisticAccumulator::combine);
+        TestHelper.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
     }
 
     /**
-     * Test the computation of the statistic using a parallel stream of {@code double[]}
-     * arrays. The arrays are mapped to a statistic using the {@link #create(double...)}
+     * Test the computation of the statistic using a parallel stream of {@code long[]}
+     * arrays. The arrays are mapped to a statistic using the {@link #create(int...)}
      * method, and the stream reduced using the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      *
      * <p>Note: This method is similar to the
-     * {@link #testArrayAndCombine(double[][], double, DoubleTolerance)} method and uses
+     * {@link #testArrayAndCombine(long[][], StatisticResult, DoubleTolerance)} method and uses
      * the same data and tolerance. The difference is the former method uses its own fixed
      * algorithm to combine instances. This method leaves the process of combining to the
      * JDK parallel stream; results may vary across platforms as it is dependent on the
@@ -1106,24 +977,23 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      */
     @ParameterizedTest
     @MethodSource(value = "testArrayAndCombine")
-    final void testArrayParallelStream(double[][] values, double expected, DoubleTolerance tol) {
-        final double actual = Arrays.stream(values)
+    final void testArrayParallelStream(long[][] values, StatisticResult expected, DoubleTolerance tol) {
+        final S actual = Arrays.stream(values)
             .parallel()
             .map(this::create)
-            .reduce(DoubleStatisticAccumulator::combine)
+            .reduce(StatisticAccumulator::combine)
             // Return an empty instance if there is no data
-            .orElseGet(this::create)
-            .getAsDouble();
-        TestUtils.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
+            .orElseGet(this::create);
+        TestHelper.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
     }
 
     /**
-     * Invokes the {@link #testAccept(double[], double, DoubleTolerance)} method using
+     * Invokes the {@link #testAccept(long[], StatisticResult, DoubleTolerance)} method using
      * random permutations of the data.
      */
     @ParameterizedTest
     @MethodSource(value = "testAccept")
-    final void testAcceptRandom(double[] values, double expected, DoubleTolerance tol) {
+    final void testAcceptRandom(long[] values, StatisticResult expected, DoubleTolerance tol) {
         // Obtain a seed so that it can be logged to allow repeats
         final long[] seed = TestHelper.createRNGSeed();
         final UniformRandomProvider rng = TestHelper.createRNG(seed);
@@ -1138,12 +1008,12 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     }
 
     /**
-     * Invokes the {@link #testArray(double[], double, DoubleTolerance)} method using
+     * Invokes the {@link #testArray(long[], StatisticResult, DoubleTolerance)} method using
      * random permutations of the data.
      */
     @ParameterizedTest
     @MethodSource(value = "testArray")
-    final void testArrayRandom(double[] values, double expected, DoubleTolerance tol) {
+    final void testArrayRandom(long[] values, StatisticResult expected, DoubleTolerance tol) {
         // Obtain a seed so that it can be logged to allow repeats
         final long[] seed = TestHelper.createRNGSeed();
         final UniformRandomProvider rng = TestHelper.createRNG(seed);
@@ -1158,16 +1028,16 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     }
 
     /**
-     * Invokes the {@link #testAcceptAndCombine(double[][], double, DoubleTolerance)} method using
+     * Invokes the {@link #testAcceptAndCombine(long[][], StatisticResult, DoubleTolerance)} method using
      * random permutations of the data.
      */
     @ParameterizedTest
     @MethodSource(value = "testAcceptAndCombine")
-    final void testAcceptAndCombineRandom(double[][] values, double expected, DoubleTolerance tol) {
+    final void testAcceptAndCombineRandom(long[][] values, StatisticResult expected, DoubleTolerance tol) {
         // Obtain a seed so that it can be logged to allow repeats
         final long[] seed = TestHelper.createRNGSeed();
         final UniformRandomProvider rng = TestHelper.createRNG(seed);
-        final double[] allValues = TestHelper.concatenate(values);
+        final long[] allValues = TestHelper.concatenate(values);
         int repeat = 0;
         try {
             while (repeat++ < RANDOM_PERMUTATIONS) {
@@ -1181,16 +1051,16 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     }
 
     /**
-     * Invokes the {@link #testArrayAndCombine(double[][], double, DoubleTolerance)} method using
+     * Invokes the {@link #testArrayAndCombine(long[][], StatisticResult, DoubleTolerance)} method using
      * random permutations of the data.
      */
     @ParameterizedTest
     @MethodSource(value = "testArrayAndCombine")
-    final void testArrayAndCombineRandom(double[][] values, double expected, DoubleTolerance tol) {
+    final void testArrayAndCombineRandom(long[][] values, StatisticResult expected, DoubleTolerance tol) {
         // Obtain a seed so that it can be logged to allow repeats
         final long[] seed = TestHelper.createRNGSeed();
         final UniformRandomProvider rng = TestHelper.createRNG(seed);
-        final double[] allValues = TestHelper.concatenate(values);
+        final long[] allValues = TestHelper.concatenate(values);
         int repeat = 0;
         try {
             while (repeat++ < RANDOM_PERMUTATIONS) {
@@ -1205,14 +1075,14 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Test the computation of the statistic using an empty instance and the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method with
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method with
      * instances containing single values. Adding statistic instances containing 1 value
      * should be the same precision as adding a {@code double} value, although the two
      * results are not required to be exactly equal.
      */
     @ParameterizedTest
     @MethodSource(value = "testAccept")
-    final void testCombineAsAccept(double[] values, double expected, DoubleTolerance tol) {
+    final void testCombineAsAccept(long[] values, StatisticResult expected, DoubleTolerance tol) {
         final S empty = create();
         assertStatistic(v ->
             Arrays.stream(values)
@@ -1230,17 +1100,16 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param tol Test tolerance.
      * @return the computed statistic
      */
-    final double assertStatistic(Function<double[], S> constructor,
-                                 double[] values, double expected, DoubleTolerance tol) {
+    final S assertStatistic(Function<long[], S> constructor,
+                            long[] values, StatisticResult expected, DoubleTolerance tol) {
         final S stat = constructor.apply(values);
-        final double actual = stat.getAsDouble();
-        TestUtils.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
-        return actual;
+        TestHelper.assertEquals(expected, stat, tol, () -> statisticName + ": " + format(values));
+        return stat;
     }
 
     /**
      * Assert the computation of the statistic using the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      *
      * @param constructor Function to create the statistic.
      * @param values Values.
@@ -1248,8 +1117,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param tol Test tolerance.
      * @return the computed statistic
      */
-    final double assertCombine(Function<double[], S> constructor,
-                               double[][] values, double expected, DoubleTolerance tol) {
+    final S assertCombine(Function<long[], S> constructor,
+                          long[][] values, StatisticResult expected, DoubleTolerance tol) {
         int n = values.length;
         Assertions.assertNotEquals(0, n, "No data");
         final List<S> stats = Arrays.stream(values)
@@ -1288,9 +1157,8 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
             n >>= 1;
         }
         final S stat = stats.get(0);
-        final double actual = stat.getAsDouble();
-        TestUtils.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
-        return actual;
+        TestHelper.assertEquals(expected, stat, tol, () -> statisticName + ": " + format(values));
+        return stat;
     }
 
     /**
@@ -1311,43 +1179,23 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
         stats.set(target, s1);
         if (isCombineSymmetric()) {
             final S s2 = assertCombine(stats2.get(rhs), stats2.get(lhs));
-            Assertions.assertEquals(s1.getAsDouble(), s2.getAsDouble(),
+            TestHelper.assertEquals(s1, s2, null,
                 () -> statisticName + ": Non-symmetric combine");
             stats2.set(target, s2);
         }
     }
 
-    /**
-     * Combine the two statistics. This method asserts the contract of the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
-     * The left-hand side (LHS) argument must be returned. The right-hand side (RHS) must
-     * be unchanged by the operation.
-     *
-     * <p>Use this method whenever combining two instances of a statistic.
-     *
-     * @param a LHS.
-     * @param b RHS.
-     * @return the result
-     */
-    final S assertCombine(S a, S b) {
-        final double before = b.getAsDouble();
-        final S c = a.combine(b);
-        Assertions.assertEquals(before, b.getAsDouble(), () -> statisticName + ": Combine altered the RHS");
-        Assertions.assertSame(a, c, () -> statisticName + ": Combined did not return the LHS");
-        return c;
-    }
-
     /**
      * Format the values as a string. The maximum length is limited.
      *
      * @param values Values.
      * @return the string
      */
-    static String format(double[] values) {
+    static String format(long[] values) {
         if (values.length > MAX_FORMAT_VALUES) {
             return Arrays.stream(values)
                          .limit(MAX_FORMAT_VALUES)
-                         .mapToObj(Double::toString)
+                         .mapToObj(Long::toString)
                          .collect(Collectors.joining(", ", "[", ", ...]"));
         }
         return Arrays.toString(values);
@@ -1359,9 +1207,9 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param values Values.
      * @return the string
      */
-    private static String format(double[][] values) {
+    private static String format(long[][] values) {
         return Arrays.stream(values)
-            .map(BaseDoubleStatisticTest::format)
+            .map(BaseLongStatisticTest::format)
             .collect(Collectors.joining(", "));
     }
 
@@ -1373,7 +1221,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * @param seed Seed.
      * @param repeat Repeat of the total random permutations.
      */
-    private void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
+    private static void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
         throw new AssertionError(String.format("%s; Seed=%s; Repeat=%d/%d",
             e.getMessage(), Arrays.toString(seed), repeat, RANDOM_PERMUTATIONS), e);
     }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java
new file mode 100644
index 0000000..f058c84
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java
@@ -0,0 +1,331 @@
+/*
+ * 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.math.BigInteger;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+import org.junit.jupiter.api.Assertions;
+
+/**
+ * Abstract base class for statistic tests.
+ *
+ * <p>This class contains common methods used by type-specific statistic tests.
+ */
+abstract class BaseStatisticTest {
+    /** The number of random permutations to perform. */
+    static final int RANDOM_PERMUTATIONS = 5;
+    /** The maximum number of values to output in assertion messages. This is used to provide
+     * the data for the failed test which is useful to identify the failed test case. */
+    static final int MAX_FORMAT_VALUES = 100;
+    /** A marker return object to indicate that a method for a result type raised an exception. */
+    private static final Object MARKER_OBJECT = new Object();
+
+    /**
+     * Gets the statistic name.
+     *
+     * <p>The default implementation removes the text {@code "Test"} from the
+     * simple class name.
+     *
+     * @return the distribution name
+     * @see Class#getSimpleName()
+     */
+    protected String getStatisticName() {
+        return getClass().getSimpleName().replace("Test", "");
+    }
+
+    /**
+     * Gets the native result type of the statistic.
+     * The default is {@code double}.
+     *
+     * @return the result type
+     */
+    protected ResultType getResultType() {
+        return ResultType.DOUBLE;
+    }
+
+    /**
+     * Get the maximum number of values that can be added where the statistic is
+     * considered empty.
+     *
+     * <p>The default is zero. Override this method if the statistic requires multiple
+     * values to compute.
+     *
+     * @return the empty size
+     * @see #getEmptyValue()
+     */
+    protected int getEmptySize() {
+        return 0;
+    }
+
+    /**
+     * Gets the value of the statistics when it is considered empty.
+     *
+     * @return the empty value
+     * @see #getEmptySize()
+     */
+    protected abstract StatisticResult getEmptyValue();
+
+    /**
+     * Creates the statistic result using an {@code double} value.
+     *
+     * @param value Value.
+     * @return the statistic result
+     */
+    static StatisticResult createStatisticResult(double value) {
+        // This method is included for completeness.
+        // It is a simple lambda to the default StatisticResult interface.
+        return () -> value;
+    }
+
+    /**
+     * Creates the statistic result using an {@code int} value.
+     *
+     * @param value Value.
+     * @return the statistic result
+     */
+    static StatisticResult createStatisticResult(int value) {
+        return (IntStatisticResult) () -> value;
+    }
+
+    /**
+     * Creates the statistic result using a {@code long} value.
+     *
+     * @param value Value.
+     * @return the statistic result
+     */
+    static StatisticResult createStatisticResult(long value) {
+        return (LongStatisticResult) () -> value;
+    }
+
+    /**
+     * Creates the statistic result using a {@code BigInteger} value.
+     *
+     * @param value Value.
+     * @return the statistic result
+     */
+    static StatisticResult createStatisticResult(BigInteger value) {
+        return (BigIntegerStatisticResult) () -> value;
+    }
+
+    /**
+     * Gets the tolerance for equality of the statistic and the expected value as a {@code double}.
+     * This method is not used by the base implementation except to provide the default
+     * implementation for the individual test tolerance methods.
+     *
+     * <p>Tolerances are not required to be configured if the result type is not a {@code double}.
+     *
+     * <p><strong>Note</strong>
+     *
+     * <p>The default implementation throws an exception. This forces the
+     * implementation to either provide a single tolerance for all tests, or the tests
+     * individually by overriding each tolerance method.
+     *
+     * @return the tolerance
+     * @see #getResultType()
+     */
+    protected DoubleTolerance getTolerance() {
+        if (getResultType() == ResultType.DOUBLE) {
+            throw new IllegalStateException("Not all test tolerances have been configured");
+        }
+        // Not required
+        return null;
+    }
+
+    /**
+     * Gets the tolerance for equality of the statistic and the expected value
+     * for the {@link #testAccept(int[], StatisticResult, DoubleTolerance)} test.
+     *
+     * <p>The default implementation uses {@link #getTolerance()}.
+     *
+     * @return the tolerance
+     */
+    protected DoubleTolerance getToleranceAccept() {
+        return getTolerance();
+    }
+
+    /**
+     * Gets the tolerance for equality of the statistic and the expected value
+     * for the {@link #testArray(int[], StatisticResult, DoubleTolerance)} test.
+     *
+     * <p>The default implementation uses {@link #getTolerance()}.
+     *
+     * @return the tolerance
+     */
+    protected DoubleTolerance getToleranceArray() {
+        return getTolerance();
+    }
+
+    /**
+     * Gets the tolerance for equality of the statistic and the expected value
+     * for the {@link #testAcceptAndCombine(int[][], StatisticResult, DoubleTolerance)} test.
+     *
+     * <p>The default implementation uses {@link #getTolerance()}.
+     *
+     * @return the tolerance
+     */
+    protected DoubleTolerance getToleranceAcceptAndCombine() {
+        return getTolerance();
+    }
+
+    /**
+     * Gets the tolerance for equality of the statistic and the expected value
+     * for the {@link #testArrayAndCombine(int[][], StatisticResult, DoubleTolerance)} test.
+     *
+     * <p>The default implementation uses {@link #getTolerance()}.
+     *
+     * @return the tolerance
+     */
+    protected DoubleTolerance getToleranceArrayAndCombine() {
+        return getTolerance();
+    }
+
+    /**
+     * Gets the tolerance for equality of the statistic and the equivalent {@link DoubleStatistic}.
+     * This method is used to cross-validate the statistic computation against the reference
+     * {@code double} implementation.
+     *
+     * <p>The default implementation return {@link #getTolerance()}.
+     * 
+     * <p>Note: Computation using {@code double} values may not be as accurate as integer
+     * specialisations. This tolerance can be set appropriately to detect errors, for example
+     * using a relative tolerance of 1e-12.
+     *
+     * @return the tolerance
+     */
+    protected DoubleTolerance getToleranceAsDouble() {
+        return getTolerance();
+    }
+
+    /**
+     * Returns {@code true} if the
+     * {@link StatisticAccumulator#combine(StatisticResult) combine}
+     * method is symmetric. If {@code true} then the combine will be tested with
+     * duplicate instances of the statistic, combined in left-to-right and right-to-left;
+     * the result must have the same statistic value.
+     *
+     * <p>The default is {@code true}. Override this if the implementation is not symmetric
+     * due to the implementation details.
+     *
+     * @return {@code true} if combine is symmetric
+     */
+    protected boolean isCombineSymmetric() {
+        return true;
+    }
+
+    //------------------------ Helper Methods to create test data ---------------------------
+
+    /**
+     * Creates the tolerance using an absolute error.
+     *
+     * <p>If the absolute tolerance is zero it is ignored and a tolerance of numerical
+     * equality is used.
+     *
+     * @param eps Absolute tolerance
+     * @return the tolerance
+     */
+    static final DoubleTolerance createAbsTolerance(double eps) {
+        return eps > 0 ? DoubleTolerances.absolute(eps) : DoubleTolerances.ulps(0);
+    }
+
+    /**
+     * Creates the tolerance using an relative error.
+     *
+     * <p>If the relative tolerance is zero it is ignored and a tolerance of numerical
+     * equality is used.
+     *
+     * @param eps Relative tolerance
+     * @return the tolerance
+     */
+    static final DoubleTolerance createRelTolerance(double eps) {
+        return eps > 0 ? DoubleTolerances.relative(eps) : DoubleTolerances.ulps(0);
+    }
+
+    /**
+     * Creates the tolerance using an {@code Or} combination of absolute and relative error.
+     *
+     * <p>If the absolute tolerance is zero it is ignored and a tolerance of numerical equality
+     * is used.
+     *
+     * <p>If the relative tolerance is zero it is ignored.
+     *
+     * @param absTolerance Absolute tolerance
+     * @param relTolerance Relative tolerance
+     * @return the tolerance
+     */
+    static final DoubleTolerance createAbsOrRelTolerance(double absTolerance, double relTolerance) {
+        final DoubleTolerance tol = createAbsTolerance(absTolerance);
+        return relTolerance > 0 ? tol.or(DoubleTolerances.relative(relTolerance)) : tol;
+    }
+
+    /**
+     * Gets the group sizes used to test the combine method with the custom test data.
+     *
+     * @return the group sizes
+     */
+    protected int[] getCombineGroupSizes() {
+        return new int[] {2, 3};
+    }
+
+    /**
+     * Combine the two statistics. This method asserts the contract of the
+     * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
+     * The left-hand side (LHS) argument must be returned. The right-hand side (RHS) must
+     * be unchanged by the operation.
+     *
+     * <p>Use this method whenever combining two instances of a statistic.
+     *
+     * @param <S> Type of statistic
+     * @param a LHS.
+     * @param b RHS.
+     * @return the result
+     */
+    final <S extends StatisticResult & StatisticAccumulator<S>> S assertCombine(S a, S b) {
+        final Object before = getNativeResult(b);
+        final S c = a.combine(b);
+        Assertions.assertEquals(before, getNativeResult(b), () -> getStatisticName() + ": Combine altered the RHS");
+        Assertions.assertSame(a, c, () -> getStatisticName() + ": Combined did not return the LHS");
+        return c;
+    }
+
+    /**
+     * Gets the native result of the statistic.
+     *
+     * @param s Statistic.
+     * @return the native result
+     * @see #getNativeResult(IntStatistic)
+     */
+    private Object getNativeResult(StatisticResult s) {
+        try {
+            switch (getResultType()) {
+            case DOUBLE:
+                return s.getAsDouble();
+            case LONG:
+                return s.getAsLong();
+            case INT:
+                return s.getAsInt();
+            case BIG_INTEGER:
+                return s.getAsBigInteger();
+            default:
+                break;
+            }
+        } catch (Throwable t) {
+            return MARKER_OBJECT;
+        }
+        throw new IllegalStateException("Unrecognised result type: " + getResultType());
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BigIntegerStatisticResult.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BigIntegerStatisticResult.java
new file mode 100644
index 0000000..7a30ace
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BigIntegerStatisticResult.java
@@ -0,0 +1,48 @@
+/*
+ * 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.math.BigInteger;
+
+/**
+ * Represents the {@code BigInteger} result of a statistic computed over a set of values.
+ *
+ * <p>This is a helper interface to map the native type of the expected value to other result types.
+ *
+ * @since 1.1
+ */
+@FunctionalInterface
+interface BigIntegerStatisticResult extends StatisticResult  {
+
+    @Override
+    BigInteger getAsBigInteger();
+
+    @Override
+    default double getAsDouble() {
+        return getAsBigInteger().doubleValue();
+    }
+
+    @Override
+    default int getAsInt() {
+        return getAsBigInteger().intValueExact();
+    }
+
+    @Override
+    default long getAsLong() {
+        return getAsBigInteger().longValueExact();
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMaxTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMaxTest.java
new file mode 100644
index 0000000..c8d4fd3
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMaxTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+
+/**
+ * Test for {@link IntMax}.
+ */
+final class IntMaxTest extends BaseIntStatisticTest<IntMax> {
+
+    @Override
+    protected ResultType getResultType() {
+        return ResultType.INT;
+    }
+
+    @Override
+    protected IntMax create() {
+        return IntMax.create();
+    }
+
+    @Override
+    protected IntMax create(int... values) {
+        return IntMax.of(values);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(int... values) {
+        return Max.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Integer.MIN_VALUE);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(int[] values) {
+        // Use the JDK as a reference implementation
+        final int x = Arrays.stream(values).max().orElse(Integer.MIN_VALUE);
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        return Stream.of(
+            addCase(Integer.MAX_VALUE - 1),
+            addCase(Integer.MIN_VALUE + 1)
+        );
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMinTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMinTest.java
new file mode 100644
index 0000000..1485769
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMinTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+
+/**
+ * Test for {@link IntMin}.
+ */
+final class IntMinTest extends BaseIntStatisticTest<IntMin> {
+
+    @Override
+    protected ResultType getResultType() {
+        return ResultType.INT;
+    }
+
+    @Override
+    protected IntMin create() {
+        return IntMin.create();
+    }
+
+    @Override
+    protected IntMin create(int... values) {
+        return IntMin.of(values);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(int... values) {
+        return Min.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Integer.MAX_VALUE);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(int[] values) {
+        // Use the JDK as a reference implementation
+        final int x = Arrays.stream(values).min().orElse(Integer.MAX_VALUE);
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        return Stream.of(
+            addCase(Integer.MAX_VALUE - 1),
+            addCase(Integer.MIN_VALUE + 1)
+        );
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntStatisticResult.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntStatisticResult.java
new file mode 100644
index 0000000..4b5d2f5
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntStatisticResult.java
@@ -0,0 +1,48 @@
+/*
+ * 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.math.BigInteger;
+
+/**
+ * Represents the {@code int} result of a statistic computed over a set of values.
+ *
+ * <p>This is a helper interface to map the native type of the expected value to other result types.
+ *
+ * @since 1.1
+ */
+@FunctionalInterface
+interface IntStatisticResult extends StatisticResult  {
+
+    @Override
+    int getAsInt();
+
+    @Override
+    default double getAsDouble() {
+        return getAsInt();
+    }
+
+    @Override
+    default long getAsLong() {
+        return getAsInt();
+    }
+
+    @Override
+    default BigInteger getAsBigInteger() {
+        return BigInteger.valueOf(getAsInt());
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMaxTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMaxTest.java
new file mode 100644
index 0000000..f7c239e
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMaxTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+
+/**
+ * Test for {@link LongMax}.
+ */
+final class LongMaxTest extends BaseLongStatisticTest<LongMax> {
+
+    @Override
+    protected ResultType getResultType() {
+        return ResultType.LONG;
+    }
+
+    @Override
+    protected LongMax create() {
+        return LongMax.create();
+    }
+
+    @Override
+    protected LongMax create(long... values) {
+        return LongMax.of(values);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(long... values) {
+        return Max.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Long.MIN_VALUE);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(long[] values) {
+        // Use the JDK as a reference implementation
+        final long x = Arrays.stream(values).max().orElse(Long.MIN_VALUE);
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        return Stream.of(
+            addCase(Long.MAX_VALUE - 1),
+            addCase(Long.MIN_VALUE + 1)
+        );
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMinTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMinTest.java
new file mode 100644
index 0000000..1896e03
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMinTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+
+/**
+ * Test for {@link LongMin}.
+ */
+final class LongMinTest extends BaseLongStatisticTest<LongMin> {
+
+    @Override
+    protected ResultType getResultType() {
+        return ResultType.LONG;
+    }
+
+    @Override
+    protected LongMin create() {
+        return LongMin.create();
+    }
+
+    @Override
+    protected LongMin create(long... values) {
+        return LongMin.of(values);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(long... values) {
+        return Min.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Long.MAX_VALUE);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(long[] values) {
+        // Use the JDK as a reference implementation
+        final long x = Arrays.stream(values).min().orElse(Long.MAX_VALUE);
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        return Stream.of(
+            addCase(Long.MAX_VALUE - 1),
+            addCase(Long.MIN_VALUE + 1)
+        );
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongStatisticResult.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongStatisticResult.java
new file mode 100644
index 0000000..54f8369
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongStatisticResult.java
@@ -0,0 +1,48 @@
+/*
+ * 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.math.BigInteger;
+
+/**
+ * Represents the {@code long} result of a statistic computed over a set of values.
+ *
+ * <p>This is a helper interface to map the native type of the expected value to other result types.
+ *
+ * @since 1.1
+ */
+@FunctionalInterface
+interface LongStatisticResult extends StatisticResult  {
+
+    @Override
+    long getAsLong();
+
+    @Override
+    default double getAsDouble() {
+        return getAsLong();
+    }
+
+    @Override
+    default int getAsInt() {
+        return Math.toIntExact(getAsLong());
+    }
+
+    @Override
+    default BigInteger getAsBigInteger() {
+        return BigInteger.valueOf(getAsLong());
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/ResultType.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/ResultType.java
new file mode 100644
index 0000000..e30045e
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/ResultType.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * The native result type of a {@link StatisticResult}.
+ */
+enum ResultType {
+    /** {@code double}. */
+    DOUBLE,
+    /** {@code int}. */
+    INT,
+    /** {@code long}. */
+    LONG,
+    /** {@code BigInteger}. */
+    BIG_INTEGER
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java
index ffebb45..2eb35e9 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java
@@ -17,12 +17,15 @@
 package org.apache.commons.statistics.descriptive;
 
 import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.math.MathContext;
 import java.util.Arrays;
 import java.util.function.Supplier;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.simple.RandomSource;
 import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.TestUtils;
+import org.junit.jupiter.api.Assertions;
 
 /**
  * Helper class for tests in {@code o.a.c.s.descriptive} module.
@@ -72,6 +75,30 @@ final class TestHelper {
                 .toArray();
     }
 
+    /**
+     * Helper function to concatenate arrays.
+     *
+     * @param arrays Arrays to be concatenated.
+     * @return A new array containing elements from all input arrays in the order they appear.
+     */
+    static int[] concatenate(int[]... arrays) {
+        return Arrays.stream(arrays)
+                .flatMapToInt(Arrays::stream)
+                .toArray();
+    }
+
+    /**
+     * Helper function to concatenate arrays.
+     *
+     * @param arrays Arrays to be concatenated.
+     * @return A new array containing elements from all input arrays in the order they appear.
+     */
+    static long[] concatenate(long[]... arrays) {
+        return Arrays.stream(arrays)
+                .flatMapToLong(Arrays::stream)
+                .toArray();
+    }
+
     /**
      * Helper function to reverse the concatenation of arrays. The array is copied
      * sequentially into the destination arrays.
@@ -90,6 +117,42 @@ final class TestHelper {
         }
     }
 
+    /**
+     * Helper function to reverse the concatenation of arrays. The array is copied
+     * sequentially into the destination arrays.
+     *
+     * <p>Warning: No lengths checks are performed. It is assume the combine length
+     * of the destination arrays is equal to the length of the data.
+     *
+     * @param data Array to be unconcatenated.
+     * @param arrays Destination arrays.
+     */
+    static void unconcatenate(int[] data, int[]... arrays) {
+        int from = 0;
+        for (final int[] a : arrays) {
+            System.arraycopy(data, from, a, 0, a.length);
+            from += a.length;
+        }
+    }
+
+    /**
+     * Helper function to reverse the concatenation of arrays. The array is copied
+     * sequentially into the destination arrays.
+     *
+     * <p>Warning: No lengths checks are performed. It is assume the combine length
+     * of the destination arrays is equal to the length of the data.
+     *
+     * @param data Array to be unconcatenated.
+     * @param arrays Destination arrays.
+     */
+    static void unconcatenate(long[] data, long[]... arrays) {
+        int from = 0;
+        for (final long[] a : arrays) {
+            System.arraycopy(data, from, a, 0, a.length);
+            from += a.length;
+        }
+    }
+
     /**
      * Helper function to compute the expected value of Mean using BigDecimal.
      * @param values Values.
@@ -196,6 +259,46 @@ final class TestHelper {
         return array;
     }
 
+    /**
+     * Shuffles the entries of the given array.
+     *
+     * <p>Uses Fisher-Yates shuffle copied from
+     * <a href="https://github.com/apache/commons-rng/blob/master/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/ArraySampler.java">
+     *     RNG ArraySampler.</a>
+     *
+     * <p>TODO: This can be removed when {@code commons-rng-sampling 1.6} is released.
+     *
+     * @param rng Source of randomness.
+     * @param array Array whose entries will be shuffled (in-place).
+     * @return Shuffled input array.
+     */
+    static int[] shuffle(UniformRandomProvider rng, int[] array) {
+        for (int i = array.length; i > 1; i--) {
+            swap(array, i - 1, rng.nextInt(i));
+        }
+        return array;
+    }
+
+    /**
+     * Shuffles the entries of the given array.
+     *
+     * <p>Uses Fisher-Yates shuffle copied from
+     * <a href="https://github.com/apache/commons-rng/blob/master/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/ArraySampler.java">
+     *     RNG ArraySampler.</a>
+     *
+     * <p>TODO: This can be removed when {@code commons-rng-sampling 1.6} is released.
+     *
+     * @param rng Source of randomness.
+     * @param array Array whose entries will be shuffled (in-place).
+     * @return Shuffled input array.
+     */
+    static long[] shuffle(UniformRandomProvider rng, long[] array) {
+        for (int i = array.length; i > 1; i--) {
+            swap(array, i - 1, rng.nextInt(i));
+        }
+        return array;
+    }
+
     /**
      * Shuffles the entries of the given array.
      *
@@ -229,6 +332,31 @@ final class TestHelper {
         array[j] = tmp;
     }
 
+    /**
+     * Swaps the two specified elements in the array.
+     *
+     * @param array Array.
+     * @param i First index.
+     * @param j Second index.
+     */
+    private static void swap(int[] array, int i, int j) {
+        final int tmp = array[i];
+        array[i] = array[j];
+        array[j] = tmp;
+    }
+
+    /**
+     * Swaps the two specified elements in the array.
+     *
+     * @param array Array.
+     * @param i First index.
+     * @param j Second index.
+     */
+    private static void swap(long[] array, int i, int j) {
+        final long tmp = array[i];
+        array[i] = array[j];
+        array[j] = tmp;
+    }
     /**
      * Swaps the two specified elements in the array.
      *
@@ -241,4 +369,82 @@ final class TestHelper {
         array[i] = array[j];
         array[j] = tmp;
     }
+
+    /**
+     * Assert the results are equal. All result types are compared.
+     *
+     * <p>If the tolerance is null then it is assumed the result is an exact integer type;
+     * the double value is compared using binary equality.
+     *
+     * <p>If the tolerance is not null then it is assumed the result is a {@code double} type;
+     * the BigInteger value is compared using its {@code double} representation if the
+     * actual BigInteger result is not equal.
+     *
+     * <p>If the {@code expected} throws an exception, then the {@code actual} is asserted
+     * to throw an exception of the same class.
+     *
+     * @param expected Expected.
+     * @param actual Actual.
+     * @param tol Tolerance for double equality.
+     * @param msg Message used for failure.
+     */
+    static void assertEquals(StatisticResult expected, StatisticResult actual, DoubleTolerance tol,
+            Supplier<String> msg) {
+        final Supplier<String> intMsg = () -> prefix(msg) + " int value";
+        Integer i = null;
+        try {
+            i = expected.getAsInt();
+        } catch (Throwable t) {
+            Assertions.assertThrowsExactly(t.getClass(), () -> actual.getAsInt(), intMsg);
+        }
+        if (i != null) {
+            Assertions.assertEquals(i.intValue(), actual.getAsInt(), intMsg);
+        }
+
+        final Supplier<String> longMsg = () -> prefix(msg) + " long value";
+        Long l = null;
+        try {
+            l = expected.getAsLong();
+        } catch (Throwable t) {
+            Assertions.assertThrowsExactly(t.getClass(), () -> actual.getAsLong(), longMsg);
+        }
+        if (l != null) {
+            Assertions.assertEquals(l.longValue(), actual.getAsLong(), longMsg);
+        }
+
+        final Supplier<String> doubleMsg = () -> prefix(msg) + " double value";
+        Double d = null;
+        try {
+            d = expected.getAsDouble();
+        } catch (Throwable t) {
+            Assertions.assertThrowsExactly(t.getClass(), () -> actual.getAsDouble(), doubleMsg);
+        }
+        if (d != null) {
+            if (tol == null) {
+                Assertions.assertEquals(d, actual.getAsDouble(), doubleMsg);
+            } else {
+                TestUtils.assertEquals(d, actual.getAsDouble(), tol, doubleMsg);
+            }
+        }
+
+        final Supplier<String> bigMsg = () -> prefix(msg) + " BigInteger value";
+        BigInteger b = null;
+        try {
+            b = expected.getAsBigInteger();
+        } catch (Throwable t) {
+            Assertions.assertThrowsExactly(t.getClass(), () -> actual.getAsBigInteger(), bigMsg);
+        }
+        if (b != null) {
+            if (tol == null) {
+                // Assume exact
+                Assertions.assertEquals(b, actual.getAsBigInteger(), bigMsg);
+            } else {
+                // Double computation may not be exact so check within tolerance
+                final BigInteger bb = actual.getAsBigInteger();
+                if (!b.equals(bb)) {
+                    TestUtils.assertEquals(b.doubleValue(), bb.doubleValue(), tol, bigMsg);
+                }
+            }
+        }
+    }
 }
diff --git a/src/conf/checkstyle/checkstyle-suppressions.xml b/src/conf/checkstyle/checkstyle-suppressions.xml
index 7086dac..8216ef6 100644
--- a/src/conf/checkstyle/checkstyle-suppressions.xml
+++ b/src/conf/checkstyle/checkstyle-suppressions.xml
@@ -39,4 +39,6 @@
   <suppress checks="ParameterNumber" files=".*[/\\]WilcoxonSignedRankTestTest.java" />
   <suppress checks="ParameterNumber" files=".*[/\\]UnconditionedExactTestTest.java" />
   <suppress checks="MethodLength" files=".*[/\\]WilcoxonSignedRankTestTest.java" />
+  <suppress checks="IllegalCatch" files=".*[/\\]TestHelper.java" lines="390-450" />
+  <suppress checks="IllegalCatch" files=".*[/\\]BaseStatisticTest.java" lines="280-400" />
 </suppressions>


(commons-statistics) 05/06: Drop DoubleStatisticAccumulator interface

Posted by ah...@apache.org.
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

commit 8c2cf0b5fe56410d8c63abe2cdd780112c91d617
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Thu Dec 21 22:04:01 2023 +0000

    Drop DoubleStatisticAccumulator interface
    
    All StatisticResult implementations can use the StatisticAccumulator
    interface; the argument type for the statistic computation
    (int/long/double) is not relevant to the accumulator combine method.
---
 .../descriptive/DoubleStatisticAccumulator.java    | 35 -----------------
 .../statistics/descriptive/DoubleStatistics.java   |  2 +-
 .../statistics/descriptive/FirstMoment.java        |  4 +-
 .../statistics/descriptive/GeometricMean.java      |  6 +--
 .../commons/statistics/descriptive/Kurtosis.java   |  6 +--
 .../apache/commons/statistics/descriptive/Max.java |  6 +--
 .../commons/statistics/descriptive/Mean.java       |  6 +--
 .../apache/commons/statistics/descriptive/Min.java |  6 +--
 .../commons/statistics/descriptive/Product.java    |  6 +--
 .../commons/statistics/descriptive/Skewness.java   |  6 +--
 .../statistics/descriptive/StandardDeviation.java  |  6 +--
 .../apache/commons/statistics/descriptive/Sum.java |  6 +--
 .../descriptive/SumOfCubedDeviations.java          |  4 +-
 .../descriptive/SumOfFourthDeviations.java         |  4 +-
 .../commons/statistics/descriptive/SumOfLogs.java  |  6 +--
 .../descriptive/SumOfSquaredDeviations.java        |  4 +-
 .../statistics/descriptive/SumOfSquares.java       |  6 +--
 .../commons/statistics/descriptive/Variance.java   |  6 +--
 .../descriptive/BaseDoubleStatisticTest.java       | 44 +++++++++++-----------
 .../descriptive/DoubleStatisticsTest.java          |  8 ++--
 .../descriptive/SumOfCubedDeviationsWrapper.java   |  2 +-
 .../descriptive/SumOfFourthDeviationsWrapper.java  |  2 +-
 22 files changed, 73 insertions(+), 108 deletions(-)

diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/DoubleStatisticAccumulator.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/DoubleStatisticAccumulator.java
deleted file mode 100644
index d06207c..0000000
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/DoubleStatisticAccumulator.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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;
-
-/**
- * A mutable result container that accumulates a {@link DoubleStatistic}.
- *
- * @param <T> {@link DoubleStatistic} being accumulated.
- *
- * @since 1.1
- */
-public interface DoubleStatisticAccumulator<T extends DoubleStatistic> {
-
-    /**
-     * Combines the state of the {@code other} statistic into this one.
-     *
-     * @param other Another statistic to be combined.
-     * @return {@code this} instance after combining {@code other}.
-     */
-    T combine(T other);
-}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/DoubleStatistics.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/DoubleStatistics.java
index 7ae57c8..299f52a 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/DoubleStatistics.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/DoubleStatistics.java
@@ -681,7 +681,7 @@ public final class DoubleStatistics implements DoubleConsumer {
      * @param a LHS.
      * @param b RHS.
      */
-    private static <T extends DoubleStatistic & DoubleStatisticAccumulator<T>> void combine(T a, T b) {
+    private static <T extends DoubleStatistic & StatisticAccumulator<T>> void combine(T a, T b) {
         if (a != null) {
             a.combine(b);
         }
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/FirstMoment.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/FirstMoment.java
index 18d4ed2..5fe08f5 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/FirstMoment.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/FirstMoment.java
@@ -38,10 +38,10 @@ import java.util.function.DoubleConsumer;
  * <p><strong>Note that this implementation is not synchronized.</strong> If
  * multiple threads access an instance of this class concurrently, and at least
  * one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} 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()}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/GeometricMean.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/GeometricMean.java
index f550f8f..64f0647 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/GeometricMean.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/GeometricMean.java
@@ -44,10 +44,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>This instance is not thread safe.</strong>
  * If multiple threads access an instance of this class concurrently,
  * and at least one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} functions of
  * {@link java.util.stream.Collector Collector} on a parallel stream,
  * because the parallel instance of {@link java.util.stream.Stream#collect Stream.collect()}
@@ -58,7 +58,7 @@ package org.apache.commons.statistics.descriptive;
  * @see SumOfLogs
  * @since 1.1
  */
-public final class GeometricMean implements DoubleStatistic, DoubleStatisticAccumulator<GeometricMean> {
+public final class GeometricMean implements DoubleStatistic, StatisticAccumulator<GeometricMean> {
     /** Count of values that have been added. */
     private long n;
 
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Kurtosis.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Kurtosis.java
index e4c6547..6057a5a 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Kurtosis.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Kurtosis.java
@@ -74,10 +74,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>Note that this instance is not synchronized.</strong> If
  * multiple threads access an instance of this class concurrently, and at least
  * one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} functions of
  * {@link java.util.stream.Collector Collector} on a parallel stream,
  * because the parallel instance of {@link java.util.stream.Stream#collect Stream.collect()}
@@ -87,7 +87,7 @@ package org.apache.commons.statistics.descriptive;
  * @see <a href="https://en.wikipedia.org/wiki/Kurtosis">Kurtosis (Wikipedia)</a>
  * @since 1.1
  */
-public final class Kurtosis implements DoubleStatistic, DoubleStatisticAccumulator<Kurtosis> {
+public final class Kurtosis implements DoubleStatistic, StatisticAccumulator<Kurtosis> {
     /** 2, the length limit where the biased skewness is undefined.
      * This limit effectively imposes the result m4 / m2^2 = 0 / 0 = NaN when 1 value
      * has been added. However note that when more samples are added and the variance
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
index 16709da..5ac6ad8 100644
--- 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
@@ -32,10 +32,10 @@ package org.apache.commons.statistics.descriptive;
  * <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 {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} 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()}
@@ -45,7 +45,7 @@ package org.apache.commons.statistics.descriptive;
  * @since 1.1
  * @see Math#max(double, double)
  */
-public final class Max implements DoubleStatistic, DoubleStatisticAccumulator<Max> {
+public final class Max implements DoubleStatistic, StatisticAccumulator<Max> {
 
     /** Current maximum. */
     private double maximum = Double.NEGATIVE_INFINITY;
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Mean.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Mean.java
index f35c07c..1f0e1b6 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Mean.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Mean.java
@@ -50,10 +50,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>Note that this implementation is not synchronized.</strong> If
  * multiple threads access an instance of this class concurrently, and at least
  * one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} 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()}
@@ -71,7 +71,7 @@ package org.apache.commons.statistics.descriptive;
  * @see <a href="https://en.wikipedia.org/wiki/Mean">Mean (Wikipedia)</a>
  * @since 1.1
  */
-public final class Mean implements DoubleStatistic, DoubleStatisticAccumulator<Mean> {
+public final class Mean implements DoubleStatistic, StatisticAccumulator<Mean> {
 
     /**
      * First moment used to compute the mean.
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java
index 19f301f..7601f6b 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Min.java
@@ -32,10 +32,10 @@ package org.apache.commons.statistics.descriptive;
  * <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 {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} 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()}
@@ -45,7 +45,7 @@ package org.apache.commons.statistics.descriptive;
  * @since 1.1
  * @see Math#min(double, double)
  */
-public final class Min implements DoubleStatistic, DoubleStatisticAccumulator<Min> {
+public final class Min implements DoubleStatistic, StatisticAccumulator<Min> {
 
     /** Current minimum. */
     private double minimum = Double.POSITIVE_INFINITY;
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Product.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Product.java
index 86cfbff..3e656f7 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Product.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Product.java
@@ -30,10 +30,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>This instance is not thread safe.</strong>
  * If multiple threads access an instance of this class concurrently,
  * and at least one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} functions of
  * {@link java.util.stream.Collector Collector} on a parallel stream,
  * because the parallel instance of {@link java.util.stream.Stream#collect Stream.collect()}
@@ -42,7 +42,7 @@ package org.apache.commons.statistics.descriptive;
  *
  * @since 1.1
  */
-public final class Product implements DoubleStatistic, DoubleStatisticAccumulator<Product> {
+public final class Product implements DoubleStatistic, StatisticAccumulator<Product> {
 
     /** Product of all values. */
     private double productValue = 1;
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Skewness.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Skewness.java
index f948159..383dfe4 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Skewness.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Skewness.java
@@ -76,10 +76,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>Note that this instance is not synchronized.</strong> If
  * multiple threads access an instance of this class concurrently, and at least
  * one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} functions of
  * {@link java.util.stream.Collector Collector} on a parallel stream,
  * because the parallel instance of {@link java.util.stream.Stream#collect Stream.collect()}
@@ -89,7 +89,7 @@ package org.apache.commons.statistics.descriptive;
  * @see <a href="https://en.wikipedia.org/wiki/Skewness">Skewness (Wikipedia)</a>
  * @since 1.1
  */
-public final class Skewness implements DoubleStatistic, DoubleStatisticAccumulator<Skewness> {
+public final class Skewness implements DoubleStatistic, StatisticAccumulator<Skewness> {
     /** 2, the length limit where the biased skewness is undefined.
      * This limit effectively imposes the result m3 / m2^1.5 = 0 / 0 = NaN when 1 value
      * has been added. However note that when more samples are added and the variance
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StandardDeviation.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StandardDeviation.java
index 97f0b77..d293bae 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StandardDeviation.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/StandardDeviation.java
@@ -57,10 +57,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>Note that this instance is not synchronized.</strong> If
  * multiple threads access an instance of this class concurrently, and at least
  * one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} functions of
  * {@link java.util.stream.Collector Collector} on a parallel stream,
  * because the parallel instance of {@link java.util.stream.Stream#collect Stream.collect()}
@@ -85,7 +85,7 @@ package org.apache.commons.statistics.descriptive;
  * @see Variance
  * @since 1.1
  */
-public final class StandardDeviation implements DoubleStatistic, DoubleStatisticAccumulator<StandardDeviation> {
+public final class StandardDeviation implements DoubleStatistic, StatisticAccumulator<StandardDeviation> {
 
     /**
      * An instance of {@link SumOfSquaredDeviations}, which is used to
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Sum.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Sum.java
index 08ef6c3..4230336 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Sum.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Sum.java
@@ -30,10 +30,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>This instance is not thread safe.</strong>
  * If multiple threads access an instance of this class concurrently,
  * and at least one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} functions of
  * {@link java.util.stream.Collector Collector} on a parallel stream,
  * because the parallel instance of {@link java.util.stream.Stream#collect Stream.collect()}
@@ -43,7 +43,7 @@ package org.apache.commons.statistics.descriptive;
  * @since 1.1
  * @see org.apache.commons.numbers.core.Sum
  */
-public final class Sum implements DoubleStatistic, DoubleStatisticAccumulator<Sum> {
+public final class Sum implements DoubleStatistic, StatisticAccumulator<Sum> {
 
     /** {@link org.apache.commons.numbers.core.Sum Sum} used to compute the sum. */
     private final org.apache.commons.numbers.core.Sum delegate =
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfCubedDeviations.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfCubedDeviations.java
index e58610f..bcc5565 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfCubedDeviations.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfCubedDeviations.java
@@ -44,10 +44,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>Note that this implementation is not synchronized.</strong> If
  * multiple threads access an instance of this class concurrently, and at least
  * one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} 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()}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfFourthDeviations.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfFourthDeviations.java
index b0bc155..8f379c4 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfFourthDeviations.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfFourthDeviations.java
@@ -48,10 +48,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>Note that this implementation is not synchronized.</strong> If
  * multiple threads access an instance of this class concurrently, and at least
  * one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} 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()}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfLogs.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfLogs.java
index b524138..aaf9664 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfLogs.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfLogs.java
@@ -42,10 +42,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>This instance is not thread safe.</strong>
  * If multiple threads access an instance of this class concurrently,
  * and at least one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} functions of
  * {@link java.util.stream.Collector Collector} on a parallel stream,
  * because the parallel instance of {@link java.util.stream.Stream#collect Stream.collect()}
@@ -56,7 +56,7 @@ package org.apache.commons.statistics.descriptive;
  * @see Math#log(double)
  * @since 1.1
  */
-public final class SumOfLogs implements DoubleStatistic, DoubleStatisticAccumulator<SumOfLogs> {
+public final class SumOfLogs implements DoubleStatistic, StatisticAccumulator<SumOfLogs> {
 
     /** {@link org.apache.commons.numbers.core.Sum Sum} used to compute the sum. */
     private final org.apache.commons.numbers.core.Sum delegate =
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquaredDeviations.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquaredDeviations.java
index ed0f979..1a3d7d7 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquaredDeviations.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquaredDeviations.java
@@ -33,10 +33,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>Note that this implementation is not synchronized.</strong> If
  * multiple threads access an instance of this class concurrently, and at least
  * one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} 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()}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquares.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquares.java
index 8364a94..9be57d0 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquares.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/SumOfSquares.java
@@ -36,10 +36,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>This instance is not thread safe.</strong>
  * If multiple threads access an instance of this class concurrently,
  * and at least one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} functions of
  * {@link java.util.stream.Collector Collector} on a parallel stream,
  * because the parallel instance of {@link java.util.stream.Stream#collect Stream.collect()}
@@ -48,7 +48,7 @@ package org.apache.commons.statistics.descriptive;
  *
  * @since 1.1
  */
-public final class SumOfSquares implements DoubleStatistic, DoubleStatisticAccumulator<SumOfSquares> {
+public final class SumOfSquares implements DoubleStatistic, StatisticAccumulator<SumOfSquares> {
 
     /** Sum of squares of all values. */
     private double ss;
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Variance.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Variance.java
index dbb153a..b875a55 100644
--- a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Variance.java
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Variance.java
@@ -54,10 +54,10 @@ package org.apache.commons.statistics.descriptive;
  * <p><strong>Note that this instance is not synchronized.</strong> If
  * multiple threads access an instance of this class concurrently, and at least
  * one of the threads invokes the {@link java.util.function.DoubleConsumer#accept(double) accept} or
- * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method, it must be synchronized externally.
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
  *
  * <p>However, it is safe to use {@link java.util.function.DoubleConsumer#accept(double) accept}
- * and {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
  * as {@code accumulator} and {@code combiner} functions of
  * {@link java.util.stream.Collector Collector} on a parallel stream,
  * because the parallel instance of {@link java.util.stream.Stream#collect Stream.collect()}
@@ -81,7 +81,7 @@ package org.apache.commons.statistics.descriptive;
  * @see StandardDeviation
  * @since 1.1
  */
-public final class Variance implements DoubleStatistic, DoubleStatisticAccumulator<Variance> {
+public final class Variance implements DoubleStatistic, StatisticAccumulator<Variance> {
 
     /**
      * An instance of {@link SumOfSquaredDeviations}, which is used to
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
index bdda305..a349e2f 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseDoubleStatisticTest.java
@@ -40,7 +40,7 @@ import org.junit.jupiter.params.provider.MethodSource;
 /**
  * Abstract base class for statistic tests.
  * This supports testing statistics that implement {@link DoubleStatistic} and
- * {@link DoubleStatisticAccumulator}.
+ * {@link StatisticAccumulator}.
  *
  * <p>This class uses parameterized tests that are repeated for instances of a
  * statistic. The statistic is tested using standard finite and non-finite data.
@@ -51,10 +51,10 @@ import org.junit.jupiter.params.provider.MethodSource;
  * <ol>
  *  <li>{@link java.util.function.DoubleConsumer#accept accept}
  *  <li>{@link java.util.function.DoubleConsumer#accept accept} and
- *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ *      {@link StatisticAccumulator#combine(DoubleStatistic) combine}
  *  <li>{@link #create(double...)}
  *  <li>{@link #create(double...)} and
- *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+ *      {@link StatisticAccumulator#combine(DoubleStatistic) combine}
  * </ol>
  *
  * <p>A test implementation has to provide these method implementations:
@@ -107,7 +107,7 @@ import org.junit.jupiter.params.provider.MethodSource;
  * @param <S> Statistic type
  */
 @TestInstance(Lifecycle.PER_CLASS)
-abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatisticAccumulator<S>> {
+abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & StatisticAccumulator<S>> {
     /** An empty {@code double[]}. */
     private static final double[] EMPTY = {};
     /** The number of random permutations to perform. */
@@ -547,7 +547,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Returns {@code true} if the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * {@link StatisticAccumulator#combine(DoubleStatistic) combine}
      * method is symmetric. If {@code true} then the combine will be tested with
      * duplicate instances of the statistic, combined in left-to-right and right-to-left;
      * the result must have the same statistic value.
@@ -740,10 +740,10 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * <ol>
      *  <li>{@link java.util.function.DoubleConsumer#accept accept}
      *  <li>{@link java.util.function.DoubleConsumer#accept accept} and
-     *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     *      {@link StatisticAccumulator#combine(DoubleStatistic) combine}
      *  <li>{@link #create(double...)}
      *  <li>{@link #create(double...)} and
-     *      {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     *      {@link StatisticAccumulator#combine(DoubleStatistic) combine}
      * </ol>
      *
      * <p>To test the {@code combine} method the data is split into approximately even groups
@@ -822,7 +822,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     /**
      * Stream the arguments to test the computation of the statistic using the
      * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * array, then the {@link StatisticAccumulator#combine(DoubleStatistic) combine}
      * method. The expected value and tolerance are supplied by the implementing class.
      *
      * @return the stream
@@ -834,7 +834,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     /**
      * Stream the arguments to test the computation of the statistic using the
      * {@link #create(double...)} method for each array, then the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method. The
+     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method. The
      * expected value and tolerance are supplied by the implementing class.
      *
      * @return the stream
@@ -847,7 +847,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * Stream the arguments to test the computation of the statistic using the
      * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
      * element of a parallel stream, then the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method.
      * The expected value and tolerance are supplied by the implementing class.
      *
      * <p>Note that this method uses the tolerance from {@link #getToleranceAcceptAndCombine()}.
@@ -861,7 +861,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     /**
      * Stream the arguments to test the computation of the statistic using the multiple
      * {@code double[]} arrays and the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method.
      *
      * @param tol Test tolerance.
      * @param tolCustom Tolerance for any custom test data.
@@ -1015,7 +1015,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     /**
      * Test the computation of the statistic using the
      * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * array, then the {@link StatisticAccumulator#combine(DoubleStatistic) combine}
      * method.
      */
     @ParameterizedTest
@@ -1027,7 +1027,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     /**
      * Test the computation of the statistic using the
      * {@link java.util.function.DoubleConsumer#accept(double) accept} method for each
-     * array, then the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * array, then the {@link StatisticAccumulator#combine(DoubleStatistic) combine}
      * method.
      */
     @ParameterizedTest
@@ -1037,7 +1037,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     }
 
     /**
-     * Test the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method
+     * Test the {@link StatisticAccumulator#combine(DoubleStatistic) combine} method
      * with an empty instance combined with a non-empty instance.
      */
     @ParameterizedTest
@@ -1052,7 +1052,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     }
 
     /**
-     * Test the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method
+     * Test the {@link StatisticAccumulator#combine(DoubleStatistic) combine} method
      * with a non-empty instance combined with an empty instance.
      */
     @ParameterizedTest
@@ -1070,7 +1070,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * Test the computation of the statistic using a parallel stream of {@code double}
      * values. The accumulator is the
      * {@link java.util.function.DoubleConsumer#accept(double) accept} method; the
-     * combiner is the {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine}
+     * combiner is the {@link StatisticAccumulator#combine(DoubleStatistic) combine}
      * method.
      *
      * <p>Note: This method is similar to the
@@ -1086,7 +1086,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
     final void testAcceptParallelStream(double[] values, double expected, DoubleTolerance tol) {
         final double actual = Arrays.stream(values)
             .parallel()
-            .collect(this::create, DoubleStatistic::accept, DoubleStatisticAccumulator::combine)
+            .collect(this::create, DoubleStatistic::accept, StatisticAccumulator::combine)
             .getAsDouble();
         TestUtils.assertEquals(expected, actual, tol, () -> statisticName + ": " + format(values));
     }
@@ -1095,7 +1095,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
      * Test the computation of the statistic using a parallel stream of {@code double[]}
      * arrays. The arrays are mapped to a statistic using the {@link #create(double...)}
      * method, and the stream reduced using the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method.
      *
      * <p>Note: This method is similar to the
      * {@link #testArrayAndCombine(double[][], double, DoubleTolerance)} method and uses
@@ -1110,7 +1110,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
         final double actual = Arrays.stream(values)
             .parallel()
             .map(this::create)
-            .reduce(DoubleStatisticAccumulator::combine)
+            .reduce(StatisticAccumulator::combine)
             // Return an empty instance if there is no data
             .orElseGet(this::create)
             .getAsDouble();
@@ -1205,7 +1205,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Test the computation of the statistic using an empty instance and the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method with
+     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method with
      * instances containing single values. Adding statistic instances containing 1 value
      * should be the same precision as adding a {@code double} value, although the two
      * results are not required to be exactly equal.
@@ -1240,7 +1240,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Assert the computation of the statistic using the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method.
      *
      * @param constructor Function to create the statistic.
      * @param values Values.
@@ -1319,7 +1319,7 @@ abstract class BaseDoubleStatisticTest<S extends DoubleStatistic & DoubleStatist
 
     /**
      * Combine the two statistics. This method asserts the contract of the
-     * {@link DoubleStatisticAccumulator#combine(DoubleStatistic) combine} method.
+     * {@link StatisticAccumulator#combine(DoubleStatistic) combine} method.
      * The left-hand side (LHS) argument must be returned. The right-hand side (RHS) must
      * be unchanged by the operation.
      *
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/DoubleStatisticsTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/DoubleStatisticsTest.java
index 9f64644..576e435 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/DoubleStatisticsTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/DoubleStatisticsTest.java
@@ -187,20 +187,20 @@ class DoubleStatisticsTest {
      * @param constructor Constructor for an empty object.
      * @param arrayConstructor Constructor using an array of values.
      */
-    private static <T extends DoubleStatistic & DoubleStatisticAccumulator<T>> void addExpected(Statistic statistic,
+    private static <T extends DoubleStatistic & StatisticAccumulator<T>> void addExpected(Statistic statistic,
             Supplier<T> constructor, Function<double[], T> arrayConstructor) {
         final List<ExpectedResult> results = new ArrayList<>();
         for (final TestData d : testData) {
             // Stream values
             final double e1 = d.stream()
                 .map(values -> Statistics.add(constructor.get(), values))
-                .reduce(DoubleStatisticAccumulator::combine)
+                .reduce(StatisticAccumulator::combine)
                 .orElseThrow(IllegalStateException::new)
                 .getAsDouble();
             // Create from array
             final double e2 = d.stream()
                 .map(arrayConstructor)
-                .reduce(DoubleStatisticAccumulator::combine)
+                .reduce(StatisticAccumulator::combine)
                 .orElseThrow(IllegalStateException::new)
                 .getAsDouble();
             // Check that there is a finite value to compute during testing
@@ -574,7 +574,7 @@ class DoubleStatisticsTest {
      * @param setter Option setter.
      * @return the number of unique results
      */
-    private static <T extends DoubleStatistic & DoubleStatisticAccumulator<T>> int addBooleanOptionCase(
+    private static <T extends DoubleStatistic & StatisticAccumulator<T>> int addBooleanOptionCase(
             Stream.Builder<Arguments> builder, double[] values, Statistic statistic,
             Function<double[], T> arrayConstructor, BiConsumer<T, Boolean> setter) {
         final T stat = arrayConstructor.apply(values);
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/SumOfCubedDeviationsWrapper.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/SumOfCubedDeviationsWrapper.java
index e60eb8a..47e4706 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/SumOfCubedDeviationsWrapper.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/SumOfCubedDeviationsWrapper.java
@@ -20,7 +20,7 @@ package org.apache.commons.statistics.descriptive;
  * Wrapper for {@link SumOfCubedDeviations} to allow testing as a statistic.
  */
 final class SumOfCubedDeviationsWrapper implements
-        DoubleStatistic, DoubleStatisticAccumulator<SumOfCubedDeviationsWrapper> {
+        DoubleStatistic, StatisticAccumulator<SumOfCubedDeviationsWrapper> {
 
     private final SumOfCubedDeviations delegate;
 
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/SumOfFourthDeviationsWrapper.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/SumOfFourthDeviationsWrapper.java
index b540a2d..4f3dbf7 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/SumOfFourthDeviationsWrapper.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/SumOfFourthDeviationsWrapper.java
@@ -20,7 +20,7 @@ package org.apache.commons.statistics.descriptive;
  * Wrapper for {@link SumOfFourthDeviations} to allow testing as a statistic.
  */
 final class SumOfFourthDeviationsWrapper implements
-        DoubleStatistic, DoubleStatisticAccumulator<SumOfFourthDeviationsWrapper> {
+        DoubleStatistic, StatisticAccumulator<SumOfFourthDeviationsWrapper> {
 
     private final SumOfFourthDeviations delegate;