--- /dev/null
+/*******************************************************************************
+ * Copyright (c) 2017 École Polytechnique de Montréal
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.analysis.timing.core.tests.statistics;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Random;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.analysis.timing.core.statistics.IStatistics;
+import org.eclipse.tracecompass.analysis.timing.core.statistics.Statistics;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Base class to test statistics for different object types. This is done with
+ * two tests.
+ * <ol>
+ * <li>test the values vs some sample points calculated by hand (sanity test)
+ * </li>
+ * <li>2- test exhaustively vs a reference implementation.</li>
+ * </ol>
+ *
+ * Each implementation will need to provide the object type to use for the test
+ * and implement a method to get objects that will return the values to test.
+ * Any additional dataset that should be tested for a specific object should be
+ * implemented in a concrete class for that object type.
+ *
+ * This test class tests statistics with positive and negative values. If for certain objects, negative values are not supported, the following test methods should be overridden, to be ignored:
+ * <ol>
+ * <li>{@link #testLimitDataset2()}</li>
+ * <li>{@link #testLargeDatasetNegative()}<li>
+ * </ol>
+ *
+ * @author Matthew Khouzam
+ * @author Geneviève Bastien
+ * @param <E>
+ * The type of object to calculate statistics on
+ */
+public abstract class AbstractStatisticsTest<@NonNull E> {
+
+ private static final int MEDIUM_AMOUNT_OF_SEGMENTS = 100;
+ private static final int LARGE_AMOUNT_OF_SEGMENTS = 1000000;
+
+ private static final double ERROR = 0.000001;
+ private static final double APPROX_ERROR = 0.0001;
+
+ private final @Nullable Function<@NonNull E, @NonNull Long> fMapper;
+
+ /**
+ * Constructor
+ *
+ * @param mapper
+ * A mapper function that takes an object to computes statistics
+ * for and returns the value to use for the statistics. If the
+ * mapper is <code>null</code>, a default Long identity function
+ * will be used
+ */
+ public AbstractStatisticsTest(Function<E, @NonNull Long> mapper) {
+ fMapper = mapper;
+ }
+
+ /**
+ * Return the default mapper, or if it is null, a default mapper that casts to Long
+ * @return
+ */
+ private @NonNull Function<@NonNull E, @NonNull Long> getMapper() {
+ Function<@NonNull E, @NonNull Long> mapper = fMapper;
+ if (mapper == null) {
+ // Data type should be long, so define a default mapper
+ return e -> (Long) e;
+ }
+ return mapper;
+ }
+
+ private void testOnlineVsOffline(Collection<E> fixture) {
+
+ validate(new OfflineStatisticsCalculator<>(fixture, getMapper()), buildStats(fixture));
+ }
+
+ private Statistics<E> buildStats(Collection<E> fixture) {
+ Statistics<E> sss = createStatistics();
+ for (E seg : fixture) {
+ sss.update(seg);
+ }
+ return sss;
+ }
+
+ private static <@NonNull E> void validate(IStatistics<E> expected, IStatistics<E> toBeTested) {
+ assertEquals("# of elements", expected.getNbElements(), toBeTested.getNbElements());
+ assertEquals("Sum of values", expected.getTotal(), toBeTested.getTotal(), ERROR * expected.getTotal());
+ assertEquals("Mean", expected.getMean(), toBeTested.getMean(), ERROR * expected.getMean());
+ assertEquals("Min", expected.getMin(), toBeTested.getMin());
+ assertEquals("Max", expected.getMax(), toBeTested.getMax());
+ assertEquals("Min Element", expected.getMinObject(), toBeTested.getMinObject());
+ assertEquals("Max Element", expected.getMaxObject(), toBeTested.getMaxObject());
+ assertEquals("Standard Deviation", expected.getStdDev(), toBeTested.getStdDev(), APPROX_ERROR * expected.getStdDev());
+ }
+
+ /**
+ * Create a statistics object by calling the appropriate constructor whether the mapper function is null or not
+ */
+ private @NonNull Statistics<E> createStatistics() {
+ Function<@NonNull E, @NonNull Long> mapper = fMapper;
+ if (mapper == null) {
+ return new Statistics<>();
+ }
+ return new Statistics<>(mapper);
+ }
+
+ /**
+ * Create the fixture of elements of the generic type from the expected
+ * values for a test. For instance, if the test wants to test values {2, 4,
+ * 6} for a Statistics object for class Foo, then this method will return a
+ * Collection of Foo objects whose mapper function will map respectively to
+ * {2, 4, 6}
+ *
+ * @param longFixture
+ * The long values that objects should map to for this test
+ * @return A collection of E elements that map to the long values
+ */
+ protected abstract Collection<E> createElementsWithValues(Collection<@NonNull Long> longFixture);
+
+ /**
+ * Test statistics on empty dataset
+ */
+ @Test
+ public void testEmpty() {
+ // Verify the expected default values
+ Statistics<E> stats = createStatistics();
+ assertEquals("Mean", 0, stats.getMean(), ERROR);
+ assertEquals("Min", Long.MAX_VALUE, stats.getMin());
+ assertEquals("Max", Long.MIN_VALUE, stats.getMax());
+ assertEquals("Standard Deviation", Double.NaN, stats.getStdDev(), ERROR);
+ assertNull(stats.getMinObject());
+ assertNull(stats.getMaxObject());
+ assertEquals("Nb objects", 0, stats.getNbElements());
+ assertEquals("Total", 0, stats.getTotal(), ERROR);
+ }
+
+ /**
+ * Test statistics with values added in ascending order
+ */
+ @Test
+ public void testAscending() {
+ // Create a fixture of long values in ascending order
+ List<@NonNull Long> longFixture = new ArrayList<>(MEDIUM_AMOUNT_OF_SEGMENTS);
+ for (long i = 0; i <= MEDIUM_AMOUNT_OF_SEGMENTS; i++) {
+ longFixture.add(i);
+ }
+
+ // Create the statistics object for the objects that will return those
+ // values
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+ Statistics<E> sss = buildStats(fixture);
+ assertEquals("Mean", 50, sss.getMean(), ERROR);
+ assertEquals("Min", 0, sss.getMin());
+ assertEquals("Max", MEDIUM_AMOUNT_OF_SEGMENTS, sss.getMax());
+ assertEquals("Standard Deviation", 29.3, sss.getStdDev(), 0.02);
+
+ // Compare with an offline algorithm
+ testOnlineVsOffline(fixture);
+ }
+
+ /**
+ * Test statistics with values added in descending order
+ */
+ @Test
+ public void testDescending() {
+ // Create a fixture of long values in descending order.
+ List<@NonNull Long> longFixture = new ArrayList<>(MEDIUM_AMOUNT_OF_SEGMENTS);
+ for (long i = MEDIUM_AMOUNT_OF_SEGMENTS; i >= 0; i--) {
+ longFixture.add(i);
+ }
+
+ // Create the statistics object for the objects that will return those
+ // values
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+ Statistics<E> sss = buildStats(fixture);
+ assertEquals("Mean", 50, sss.getMean(), ERROR);
+ assertEquals("Min", 0, sss.getMin());
+ assertEquals("Max", MEDIUM_AMOUNT_OF_SEGMENTS, sss.getMax());
+ assertEquals("Standard Deviation", 29.3, sss.getStdDev(), 0.02);
+
+ // Compare with an offline algorithm
+ testOnlineVsOffline(fixture);
+
+ }
+
+ /**
+ * Test a data set with a small number of objects
+ */
+ @Test
+ public void testSmallDataset() {
+ // Create fixture with only 1 element
+ List<@NonNull Long> longFixture = new ArrayList<>(1);
+ longFixture.add(1L);
+
+ // Create the statistics object for the objects that will return those
+ // values
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+
+ // Compare with an offline algorithm
+ testOnlineVsOffline(fixture);
+ }
+
+ /**
+ * Test a dataset with positive limit values
+ */
+ @Test
+ public void testLimitDataset() {
+ // Create a fixture with max values
+ List<@NonNull Long> longFixture = new ArrayList<>(1);
+ longFixture.add(Long.MAX_VALUE);
+ longFixture.add(Long.MAX_VALUE);
+
+ // Create the statistics object for the objects that will return those
+ // values
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+ Statistics<E> sss = buildStats(fixture);
+ // Test some values
+ assertEquals("Mean", Long.MAX_VALUE, sss.getMean(), ERROR);
+ assertEquals("Total", (double) 2 * Long.MAX_VALUE, sss.getTotal(), ERROR);
+ assertEquals("Standard deviation", Double.NaN, sss.getStdDev(), ERROR);
+
+ // Compare with an offline algorithm
+ testOnlineVsOffline(fixture);
+ }
+
+ /**
+ * Test a dataset with negative limit values
+ *
+ * NOTE: This test has negative values
+ */
+ @Test
+ public void testLimitDataset2() {
+ // Create a fixture with min values
+ List<@NonNull Long> longFixture = new ArrayList<>(1);
+ longFixture.add(Long.MIN_VALUE);
+ longFixture.add(Long.MIN_VALUE);
+ longFixture.add(Long.MIN_VALUE);
+
+ // Create the statistics object for the objects that will return those
+ // values
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+ Statistics<E> sss = buildStats(fixture);
+ // Test some values
+ assertEquals("Mean", Long.MIN_VALUE, sss.getMean(), ERROR);
+ assertEquals("Total", (double) 3 * Long.MIN_VALUE, sss.getTotal(), ERROR);
+ assertEquals("Standard deviation", 0, sss.getStdDev(), ERROR);
+
+ // Compare with an offline algorithm
+ testOnlineVsOffline(fixture);
+ }
+
+ /**
+ * Test a data set with a large number of objects of random values
+ */
+ @Test
+ public void testLargeDataset() {
+ // Create a fixture of a large number of random values
+ List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+ Random rng = new Random(10);
+ for (int i = 1; i <= LARGE_AMOUNT_OF_SEGMENTS; i++) {
+ longFixture.add(Math.abs(rng.nextLong()));
+ }
+ // Create the statistics object for the objects that will return those
+ // values
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+
+ // Compare with an offline algorithm
+ testOnlineVsOffline(fixture);
+
+ }
+
+ /**
+ * Test a data set with a large number of objects of random values
+ *
+ * NOTE: This test contains negative values
+ */
+ @Test
+ public void testLargeDatasetNegative() {
+ // Create a fixture of a large number of random values
+ List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+ Random rng = new Random(10);
+ for (int i = 1; i <= LARGE_AMOUNT_OF_SEGMENTS; i++) {
+ longFixture.add(rng.nextLong());
+ }
+ // Create the statistics object for the objects that will return those
+ // values
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+
+ // Compare with an offline algorithm
+ testOnlineVsOffline(fixture);
+
+ }
+
+ /**
+ * Test a random dataset where the distribution follows white noise
+ */
+ @Test
+ public void testNoiseDataset() {
+ // Create a fixture of a large number of random values
+ List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+ Random rng = new Random(1234);
+ for (int i = 1; i <= LARGE_AMOUNT_OF_SEGMENTS; i++) {
+ longFixture.add(Long.valueOf(Math.abs(rng.nextInt(1000000))));
+ }
+ // Create the statistics object for the objects that will return those
+ // values
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+
+ // Compare with an offline algorithm
+ testOnlineVsOffline(fixture);
+
+ }
+
+ /**
+ * Test a random dataset where the distribution follows gaussian noise
+ */
+ @Test
+ public void gaussianNoiseTest() {
+ // Create a fixture of a large number of random values
+ List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+ Random rng = new Random(1234);
+ for (int i = 1; i <= LARGE_AMOUNT_OF_SEGMENTS; i++) {
+ longFixture.add(Long.valueOf(Math.abs(rng.nextInt(1000))));
+ }
+ // Create the statistics object for the objects that will return those
+ // values
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+
+ // Compare with an offline algorithm
+ testOnlineVsOffline(fixture);
+
+ }
+
+ /**
+ * Test building a statistics store with streams
+ */
+ @Test
+ public void streamBuildingTest() {
+ Statistics<E> expected = createStatistics();
+ List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+ for (long i = 0; i < LARGE_AMOUNT_OF_SEGMENTS; i++) {
+ longFixture.add(i);
+ }
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+ fixture.forEach(e -> expected.update(e));
+ Statistics<E> actual = fixture.stream()
+ .<org.eclipse.tracecompass.analysis.timing.core.statistics.Statistics<E>> collect(() -> createStatistics(),
+ Statistics<E>::update, Statistics<E>::merge);
+ validate(expected, actual);
+ }
+
+ /**
+ * Test building a statistics store with parallel streams
+ */
+ @Test
+ public void parallelStreamBuildingTest() {
+ Statistics<E> expected = createStatistics();
+ List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+ for (long i = 0; i < LARGE_AMOUNT_OF_SEGMENTS; i++) {
+ longFixture.add(i);
+ }
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+ fixture.forEach(e -> expected.update(e));
+ Statistics<E> actual = fixture.parallelStream()
+ .<org.eclipse.tracecompass.analysis.timing.core.statistics.Statistics<E>> collect(() -> createStatistics(),
+ Statistics<E>::update, Statistics<E>::merge);
+ validate(expected, actual);
+ }
+
+ /**
+ * Test statistics nodes being merged. Two identical blocks.
+ */
+ @Test
+ public void testMergeStatisticsNodes() {
+ // Create a fixture of a few values
+ int nbElements = 10;
+ List<@NonNull Long> longFixture = new ArrayList<>(nbElements);
+ for (long i = 0; i < nbElements; i++) {
+ longFixture.add(i);
+ }
+
+ // Get an object fixture
+ Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+ IStatistics<@NonNull E> expected = createStatistics();
+ IStatistics<@NonNull E> statsA = createStatistics();
+ IStatistics<@NonNull E> statsB = createStatistics();
+
+ Collection<@NonNull E> allElements = new ArrayList<>(2 * nbElements);
+
+ fixture.stream().forEach(obj -> {
+ // Since we will merge the statistics, the object should be added
+ // twice to the expected statistics and the allElements collection
+ expected.update(obj);
+ expected.update(obj);
+ statsA.update(obj);
+ statsB.update(obj);
+ allElements.add(obj);
+ allElements.add(obj);
+ });
+ // Merge the 2 statistics
+ statsA.merge(statsB);
+ assertEquals("Merged size", 2 * nbElements, statsA.getNbElements());
+
+ // Compare the results of the merge with the expected results
+ validate(expected, statsA);
+
+ // Compare with the offline comparator
+ IStatistics<@NonNull E> offline = new OfflineStatisticsCalculator<>(allElements, getMapper());
+ validate(offline, statsA);
+ }
+
+ /**
+ * Test statistics nodes being merged. Two random blocks.
+ */
+ @Test
+ public void testMergeStatisticsRandomNodes() {
+ Random rnd = new Random();
+ rnd.setSeed(1234);
+ // Create 2 fixtures of a random sizes
+ int size = 2 + rnd.nextInt(1000);
+ int size2 = 2 + rnd.nextInt(1000);
+
+ List<@NonNull Long> longFixture1 = new ArrayList<>(size);
+ for (long i = 0; i < size; i++) {
+ longFixture1.add(Long.valueOf(Math.abs(rnd.nextInt(1000))));
+ }
+ Collection<@NonNull E> fixture1 = createElementsWithValues(longFixture1);
+
+ List<@NonNull Long> longFixture2 = new ArrayList<>(size2);
+ for (long i = 0; i < size2; i++) {
+ longFixture2.add(Long.valueOf(Math.abs(rnd.nextInt(1000))));
+ }
+ Collection<@NonNull E> fixture2 = createElementsWithValues(longFixture2);
+
+ // Create the statistics objects to merge
+ IStatistics<@NonNull E> expected = createStatistics();
+ IStatistics<@NonNull E> statsA = createStatistics();
+ IStatistics<@NonNull E> statsB = createStatistics();
+ Collection<@NonNull E> allElements = new ArrayList<>(size + size2);
+
+ fixture1.stream().forEach(obj -> {
+ expected.update(obj);
+ statsA.update(obj);
+ allElements.add(obj);
+ });
+
+ fixture2.stream().forEach(obj -> {
+ expected.update(obj);
+ statsB.update(obj);
+ allElements.add(obj);
+ });
+
+ // Make sure statsA and statsB have the expected size
+ assertEquals("size of statsA", size, statsA.getNbElements());
+ assertEquals("size of statsB", size2, statsB.getNbElements());
+
+ // Merge the 2 statistics
+ statsA.merge(statsB);
+ assertEquals("Merged size", size + size2, statsA.getNbElements());
+
+ // Compare the results of the merge with the expected results
+ validate(expected, statsA);
+
+ // Compare with the offline comparator
+ IStatistics<@NonNull E> offline = new OfflineStatisticsCalculator<>(allElements, getMapper());
+ validate(offline, statsA);
+ }
+
+ /**
+ * Test corner cases when merging statistics nodes
+ */
+ @Test
+ public void mergeStatisticsCornerCaseNodesTest() {
+ // Create a fixtures of one element
+ Collection<@NonNull E> oneFixture = createElementsWithValues(ImmutableList.of(10L));
+ // Create a small fixtures of a few elements
+ Collection<@NonNull E> smallFixture = createElementsWithValues(ImmutableList.of(0L, 10L, 5L, 12L, 7L, 1234L));
+
+ // Control statistics, not to be modified
+ Statistics<E> noElements = createStatistics();
+ Statistics<E> oneElement = createStatistics();
+ oneElement.update(oneFixture.iterator().next());
+ Statistics<E> allElements = createStatistics();
+ oneFixture.stream().forEach(obj -> allElements.update(obj));
+ smallFixture.stream().forEach(obj -> allElements.update(obj));
+
+ // The statistics objects to test
+ Statistics<E> testStats = createStatistics();
+ Statistics<E> testStats2 = createStatistics();
+
+ // Test merging empty stats on a non-empty one
+ testStats.update(oneFixture.iterator().next());
+ testStats.merge(testStats2);
+ validate(oneElement, testStats);
+ validate(noElements, testStats2);
+
+ // Test merging a one element statistics an empty stats
+ testStats2.merge(testStats);
+ validate(oneElement, testStats);
+ validate(oneElement, testStats2);
+
+ // Test merging stats with only 1 segment
+ Statistics<E> testStats3 = createStatistics();
+ smallFixture.stream().forEach(obj -> testStats3.update(obj));
+ testStats3.merge(testStats2);
+ validate(oneElement, testStats2);
+ validate(allElements, testStats3);
+
+ // Test merging on stats with only 1 segment
+ Statistics<E> testStats4 = createStatistics();
+ smallFixture.stream().forEach(obj -> testStats4.update(obj));
+ testStats2.merge(testStats4);
+ validate(allElements, testStats2);
+
+ }
+
+}
--- /dev/null
+/*******************************************************************************
+ * Copyright (c) 2017 École Polytechnique de Montréal
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.analysis.timing.core.statistics;
+
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.common.core.NonNullUtils;
+
+/**
+ * Class that calculates statistics on a certain type of object. If the object
+ * is not a {@link Long}, a mapper function should be passed in the constructor
+ * to retrieve the long value to make statistics on from an object.
+ *
+ * @author Bernd Hufmann
+ * @author Geneviève Bastien
+ *
+ * @param <E>
+ * The type of object to calculate statistics on
+ * @since 1.3
+ */
+public class Statistics<@NonNull E> implements IStatistics<E> {
+
+ private final Function<E, @NonNull Long> fMapper;
+
+ private @Nullable E fMin = null;
+ private @Nullable E fMax = null;
+ private long fNbElements;
+ private double fMean;
+ /**
+ * reminder, this is the variance * nb elem, as per the online algorithm
+ */
+ private double fVariance;
+ private double fTotal;
+
+ /**
+ * Constructor
+ */
+ public Statistics() {
+ this(e -> {
+ if (!(e instanceof Long)) {
+ throw new IllegalStateException("The object " + e + " is not a number"); //$NON-NLS-1$//$NON-NLS-2$
+ }
+ return (Long) e;
+ });
+ }
+
+ /**
+ * Constructor
+ *
+ * @param mapper
+ * A mapper function that takes an object to computes statistics
+ * for and returns the value to use for the statistics
+ */
+ public Statistics(Function<E, Long> mapper) {
+ fNbElements = 0;
+ fMean = 0.0;
+ fVariance = 0.0;
+ fTotal = 0.0;
+ fMapper = mapper;
+ }
+
+ @Override
+ public long getMin() {
+ @Nullable
+ E min = fMin;
+ if (min == null) {
+ return Long.MAX_VALUE;
+ }
+ return NonNullUtils.checkNotNull(fMapper.apply(min));
+ }
+
+ @Override
+ public long getMax() {
+ @Nullable
+ E max = fMax;
+ if (max == null) {
+ return Long.MIN_VALUE;
+ }
+ return NonNullUtils.checkNotNull(fMapper.apply(max));
+ }
+
+ @Override
+ public @Nullable E getMinObject() {
+ return fMin;
+ }
+
+ @Override
+ public @Nullable E getMaxObject() {
+ return fMax;
+ }
+
+ @Override
+ public long getNbElements() {
+ return fNbElements;
+ }
+
+ @Override
+ public double getMean() {
+ return fMean;
+ }
+
+ /**
+ * Gets the standard deviation of the elements. It uses the online algorithm
+ * shown here <a href=
+ * "https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm">
+ * Wikipedia article of dec 3 2015 </a>
+ *
+ * @return the standard deviation of the elements, will return NaN if there
+ * are less than 3 elements
+ */
+ @Override
+ public double getStdDev() {
+ return fNbElements > 2 ? Math.sqrt(fVariance / (fNbElements - 1)) : Double.NaN;
+ }
+
+ @Override
+ public double getTotal() {
+ return fTotal;
+ }
+
+ @Override
+ public void update(E object) {
+ Long value = NonNullUtils.checkNotNull(fMapper.apply(object));
+ /*
+ * Min and max are trivial, as well as number of segments
+ */
+ fMin = value <= getMin() ? object : fMin;
+ fMax = value >= getMax() ? object : fMax;
+
+ fNbElements++;
+ /*
+ * The running mean is not trivial, see proof in javadoc.
+ *
+ * TODO: Check if saturated math would be required here
+ */
+ double delta = value - fMean;
+ fMean += delta / fNbElements;
+ fVariance += delta * (value - fMean);
+ fTotal += value;
+ }
+
+ @Override
+ public void merge(IStatistics<E> o) {
+ if (!(o instanceof Statistics)) {
+ throw new IllegalArgumentException("Can only merge statistics of the same class"); //$NON-NLS-1$
+ }
+ Statistics<E> other = (Statistics<E>) o;
+ if (other.fNbElements == 0) {
+ return;
+ } else if (fNbElements == 0) {
+ copy(other);
+ } else if (other.fNbElements == 1) {
+ update(NonNullUtils.checkNotNull(other.getMaxObject()));
+ } else if (fNbElements == 1) {
+ Statistics<E> copyOther = new Statistics<>(fMapper);
+ copyOther.copy(other);
+ copyOther.update(NonNullUtils.checkNotNull(getMaxObject()));
+ copy(copyOther);
+ } else {
+ internalMerge(other);
+ }
+ }
+
+ private void internalMerge(Statistics<E> other) {
+ /*
+ * TODO: Check if saturated math would be required in this method
+ *
+ * Min and max are trivial, as well as number of segments
+ */
+ long min = getMin();
+ long max = getMax();
+ fMin = other.getMin() <= min ? other.getMinObject() : fMin;
+ fMax = other.getMax() >= max ? other.getMaxObject() : fMax;
+
+ long oldNbSeg = fNbElements;
+ double oldAverage = fMean;
+ long otherSegments = other.getNbElements();
+ double otherAverage = other.getMean();
+ fNbElements += otherSegments;
+ fTotal += other.getTotal();
+
+ /*
+ * Average is a weighted average
+ */
+ fMean = ((oldNbSeg * oldAverage) + (otherAverage * otherSegments)) / fNbElements;
+
+ /*
+ * This one is a bit tricky.
+ *
+ * The variance is the sum of the deltas from a mean squared.
+ *
+ * So if we add the old mean squared back to to variance and remove the
+ * new mean, the standard deviation can be easily calculated.
+ */
+ double avg1Sq = oldAverage * oldAverage;
+ double avg2sq = otherAverage * otherAverage;
+ double avgtSq = fMean * fMean;
+ /*
+ * This is a tricky part, bear in mind that the set is not continuous
+ * but discrete, Therefore, we have for n elements, n-1 intervals
+ * between them. Ergo, n-1 intervals are used for divisions and
+ * multiplications.
+ */
+ double variance1 = fVariance / (oldNbSeg - 1);
+ double variance2 = other.fVariance / (otherSegments - 1);
+ fVariance = ((variance1 + avg1Sq - avgtSq) * (oldNbSeg - 1) + (variance2 + avg2sq - avgtSq) * (otherSegments - 1));
+ }
+
+ private void copy(Statistics<E> copyOther) {
+ fMean = copyOther.fMean;
+ fMax = copyOther.fMax;
+ fMin = copyOther.fMin;
+ fNbElements = copyOther.fNbElements;
+ fTotal = copyOther.fTotal;
+ fVariance = copyOther.fVariance;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass() + ": Avg: " + getMean() + " on " + getNbElements() + " elements"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+}