Statistics.java

/*
 * 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.DoubleConsumer;
import java.util.function.IntConsumer;
import java.util.function.LongConsumer;

/**
 * Utility methods for statistics.
 *
 * @since 1.1
 */
final class Statistics {
    /** A no-operation double consumer. This is exposed for testing. */
    static final DoubleConsumer DOUBLE_NOOP = new DoubleConsumer() {
        @Override
        public void accept(double value) {
            // Do nothing
        }

        @Override
        public DoubleConsumer andThen(DoubleConsumer after) {
            // Delegate to the after consumer
            return after;
        }
    };

    /** A no-operation int consumer. This is exposed for testing. */
    static final IntConsumer INT_NOOP = new IntConsumer() {
        @Override
        public void accept(int value) {
            // Do nothing
        }

        @Override
        public IntConsumer andThen(IntConsumer after) {
            // Delegate to the after consumer
            return after;
        }
    };

    /** A no-operation long consumer. This is exposed for testing. */
    static final LongConsumer LONG_NOOP = new LongConsumer() {
        @Override
        public void accept(long value) {
            // Do nothing
        }

        @Override
        public LongConsumer andThen(LongConsumer after) {
            // Delegate to the after consumer
            return after;
        }
    };

    /** Error message for an incompatible statistics. */
    private static final String INCOMPATIBLE_STATISTICS = "Incompatible statistics";

    /** No instances. */
    private Statistics() {}

    /**
     * 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 DoubleConsumer> T add(T statistic, double[] values) {
        for (final double 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 DoubleConsumer> T add(T statistic, int[] values) {
        for (final double 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 DoubleConsumer> T add(T statistic, long[] values) {
        for (final double 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 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}.
     *
     * <p>This method shares the logic for detecting a zero variance among implementations
     * that divide by the variance (e.g. skewness, kurtosis).
     *
     * @param m1 First raw moment (mean).
     * @param m2 Second central moment (biased variance).
     * @return true if the variance is zero
     */
    static boolean zeroVariance(double m1, double m2) {
        // Note: Commons Math checks the variance is < 1e-19.
        // The absolute threshold does not account for the magnitude of the sample.
        // This checks the average squared deviation from the mean (m2)
        // is smaller than the squared precision of the mean (m1).
        // Precision is set to 15 decimal digits
        // (1e-15 ~ 4.5 eps where eps = 2^-52).
        final double meanPrecision = 1e-15 * m1;
        return m2 <= meanPrecision * meanPrecision;
    }

    /**
     * Chain the {@code consumers} into a single composite consumer. Ignore any {@code null}
     * consumer. Returns {@code null} if all arguments are {@code null}.
     *
     * @param consumers Consumers.
     * @return a composed consumer (or null)
     */
    static DoubleConsumer compose(DoubleConsumer... consumers) {
        DoubleConsumer action = DOUBLE_NOOP;
        for (final DoubleConsumer consumer : consumers) {
            if (consumer != null) {
                action = action.andThen(consumer);
            }
        }
        return action == DOUBLE_NOOP ? null : action;
    }

    /**
     * Chain the {@code consumers} into a single composite consumer. Ignore any {@code null}
     * consumer. Returns {@code null} if all arguments are {@code null}.
     *
     * @param consumers Consumers.
     * @return a composed consumer (or null)
     */
    static IntConsumer compose(IntConsumer... consumers) {
        IntConsumer action = INT_NOOP;
        for (final IntConsumer consumer : consumers) {
            if (consumer != null) {
                action = action.andThen(consumer);
            }
        }
        return action == INT_NOOP ? null : action;
    }

    /**
     * Chain the {@code consumers} into a single composite consumer. Ignore any {@code null}
     * consumer. Returns {@code null} if all arguments are {@code null}.
     *
     * @param consumers Consumers.
     * @return a composed consumer (or null)
     */
    static LongConsumer compose(LongConsumer... consumers) {
        LongConsumer action = LONG_NOOP;
        for (final LongConsumer consumer : consumers) {
            if (consumer != null) {
                action = action.andThen(consumer);
            }
        }
        return action == LONG_NOOP ? null : action;
    }

    /**
     * Gets the statistic result using the {@code int} value.
     * Return {@code null} is the statistic is {@code null}.
     *
     * @param s Statistic.
     * @return the result or null
     */
    static StatisticResult getResultAsIntOrNull(StatisticResult s) {
        if (s != null) {
            return (IntStatisticResult) s::getAsInt;
        }
        return null;
    }

    /**
     * Gets the statistic result using the {@code long} value.
     * Return {@code null} is the statistic is {@code null}.
     *
     * @param s Statistic.
     * @return the result or null
     */
    static StatisticResult getResultAsLongOrNull(StatisticResult s) {
        if (s != null) {
            return (LongStatisticResult) s::getAsLong;
        }
        return null;
    }

    /**
     * Gets the statistic result using the {@code double} value.
     * Return {@code null} is the statistic is {@code null}.
     *
     * @param s Statistic.
     * @return the result or null
     */
    static StatisticResult getResultAsDoubleOrNull(StatisticResult s) {
        if (s != null) {
            return s::getAsDouble;
        }
        return null;
    }

    /**
     * Gets the statistic result using the {@code BigInteger} value.
     * Return {@code null} is the statistic is {@code null}.
     *
     * @param s Statistic.
     * @return the result or null
     */
    static StatisticResult getResultAsBigIntegerOrNull(StatisticResult s) {
        if (s != null) {
            return (BigIntegerStatisticResult) s::getAsBigInteger;
        }
        return null;
    }

    /**
     * Check left-hand side argument {@code a} is {@code null} or else the right-hand side
     * argument {@code b} must also be non-{@code null} so the statistics can be combined.
     *
     * @param <T> {@link StatisticResult} being accumulated.
     * @param a LHS.
     * @param b RHS.
     * @throws IllegalArgumentException if the objects cannot be combined
     */
    static <T extends StatisticResult & StatisticAccumulator<T>> void checkCombineCompatible(T a, T b) {
        if (a != null && b == null) {
            throw new IllegalArgumentException(INCOMPATIBLE_STATISTICS);
        }
    }

    /**
     * Check left-hand side argument {@code a} is {@code null} or else the right-hand side
     * argument {@code b} must be run-time assignable to the same class as {@code a}
     * so the statistics can be combined.
     *
     * @param a LHS.
     * @param b RHS.
     * @throws IllegalArgumentException if the objects cannot be combined
     */
    static void checkCombineAssignable(FirstMoment a, FirstMoment b) {
        if (a != null && (b == null || !a.getClass().isAssignableFrom(b.getClass()))) {
            throw new IllegalArgumentException(INCOMPATIBLE_STATISTICS);
        }
    }

    /**
     * If the left-hand side argument {@code a} is non-{@code null}, combine it with the
     * right-hand side argument {@code b}.
     *
     * @param <T> {@link StatisticResult} being accumulated.
     * @param a LHS.
     * @param b RHS.
     */
    static <T extends StatisticResult & StatisticAccumulator<T>> void combine(T a, T b) {
        if (a != null) {
            a.combine(b);
        }
    }

    /**
     * If the left-hand side argument {@code a} is non-{@code null}, combine it with the
     * right-hand side argument {@code b}. Assumes that the RHS is run-time assignable
     * to the same class as LHS.
     *
     * @param a LHS.
     * @param b RHS.
     * @see #checkCombineAssignable(FirstMoment, FirstMoment)
     */
    static void combineMoment(FirstMoment a, FirstMoment b) {
        // Avoid reflection and use the simpler instanceof
        if (a instanceof SumOfFourthDeviations) {
            ((SumOfFourthDeviations) a).combine((SumOfFourthDeviations) b);
        } else if (a instanceof SumOfCubedDeviations) {
            ((SumOfCubedDeviations) a).combine((SumOfCubedDeviations) b);
        } else if (a instanceof SumOfSquaredDeviations) {
            ((SumOfSquaredDeviations) a).combine((SumOfSquaredDeviations) b);
        } else if (a != null) {
            a.combine(b);
        }
    }
}