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:25 UTC

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

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>