Emulate java.util.Int/Long/DoubleSummaryStatistics

Change-Id: I49555fd7f4e60646f36db9185fb56ba9c692b06b
diff --git a/user/super/com/google/gwt/emul/java/util/DoubleSummaryStatistics.java b/user/super/com/google/gwt/emul/java/util/DoubleSummaryStatistics.java
new file mode 100644
index 0000000..5163cdc
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/DoubleSummaryStatistics.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed 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 java.util;
+
+import java.util.function.DoubleConsumer;
+
+/**
+ * See <a
+ * href="https://docs.oracle.com/javase/8/docs/api/java/util/DoubleSummaryStatistics.html">the
+ * official Java API doc</a> for details.
+ */
+public class DoubleSummaryStatistics implements DoubleConsumer {
+
+  private long count;
+  private double min = Double.POSITIVE_INFINITY;
+  private double max = Double.NEGATIVE_INFINITY;
+  private double sum;
+  private double sumError;
+  // Because of Kahan summation compensation a naive summation is required
+  // to keep track of infinity values correctly.
+  private double naiveSum;
+
+  @Override
+  public void accept(double value) {
+    count++;
+    min = Math.min(min, value);
+    max = Math.max(max, value);
+    naiveSum += value;
+    sum(value);
+  }
+
+  public void combine(DoubleSummaryStatistics other) {
+    count += other.count;
+    min = Math.min(min, other.min);
+    max = Math.max(max, other.max);
+    naiveSum += other.naiveSum;
+    sum(other.sum);
+    sum(other.sumError);
+  }
+
+  public double getAverage() {
+    return count > 0 ? getSum() / count : 0d;
+  }
+
+  public long getCount() {
+    return count;
+  }
+
+  public double getMin() {
+    return min;
+  }
+
+  public double getMax() {
+    return max;
+  }
+
+  public double getSum() {
+    // Adding sum and sumError here to get a better result
+    // because Kahan summation always applies error compensation
+    // on the next summation.
+    double compensatedSum = sum + sumError;
+    // sumError can be NaN if infinity values had been accepted.
+    if (Double.isNaN(compensatedSum) && Double.isInfinite(naiveSum)) {
+      return naiveSum;
+    }
+    return compensatedSum;
+  }
+
+  @Override
+  public String toString() {
+    return "DoubleSummaryStatistics[" +
+        "count = " + count +
+        ", avg = " + getAverage() +
+        ", min = " + min +
+        ", max = " + max +
+        ", sum = " + getSum() +
+        "]";
+  }
+
+  /**
+   * Adds a new value to the current sum using Kahan summation
+   * algorithm for improved summation precision.
+   *
+   * https://en.wikipedia.org/wiki/Kahan_summation_algorithm
+   *
+   * @param value the value being added to the sum
+   */
+  private void sum(double value) {
+    double compensatedValue = value - sumError;
+    double newSum = sum + compensatedValue;
+    // Logically 'summationError' always evaluates to 0
+    // but in reality it contains a small summation error
+    // that can occur because of rounding in floating point arithmetic.
+    // For example 1.0 + EPSILON with EPSILON being half machine precision
+    // (basically Math.ulp(1.0)/2) will result in 1.0.
+    // Tests should verify that GWT compiler / Closure compiler do not
+    // remove this line during optimizations.
+    // NOTE: sumError can become NaN for infinity values.
+    sumError = (newSum - sum) - compensatedValue;
+    sum = newSum;
+  }
+}
diff --git a/user/super/com/google/gwt/emul/java/util/IntSummaryStatistics.java b/user/super/com/google/gwt/emul/java/util/IntSummaryStatistics.java
new file mode 100644
index 0000000..ca78c82
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/IntSummaryStatistics.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed 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 java.util;
+
+import java.util.function.IntConsumer;
+
+/**
+ * See <a
+ * href="https://docs.oracle.com/javase/8/docs/api/java/util/IntSummaryStatistics.html">the
+ * official Java API doc</a> for details.
+ */
+public class IntSummaryStatistics implements IntConsumer {
+
+  private long count;
+  private int min = Integer.MAX_VALUE;
+  private int max = Integer.MIN_VALUE;
+  private long sum;
+
+  @Override
+  public void accept(int value) {
+    count++;
+    min = Math.min(min, value);
+    max = Math.max(max, value);
+    sum += value;
+  }
+
+  public void combine(IntSummaryStatistics other) {
+    count += other.count;
+    min = Math.min(min, other.min);
+    max = Math.max(max, other.max);
+    sum += other.sum;
+  }
+
+  public double getAverage() {
+    return count > 0 ? (double) sum / count : 0d;
+  }
+
+  public long getCount() {
+    return count;
+  }
+
+  public int getMin() {
+    return min;
+  }
+
+  public int getMax() {
+    return max;
+  }
+
+  public long getSum() {
+    return sum;
+  }
+
+  @Override
+  public String toString() {
+    return "IntSummaryStatistics[" +
+        "count = " + count +
+        ", avg = " + getAverage() +
+        ", min = " + min +
+        ", max = " + max +
+        ", sum = " + sum +
+        "]";
+  }
+}
diff --git a/user/super/com/google/gwt/emul/java/util/LongSummaryStatistics.java b/user/super/com/google/gwt/emul/java/util/LongSummaryStatistics.java
new file mode 100644
index 0000000..0794c90
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/util/LongSummaryStatistics.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed 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 java.util;
+
+import java.util.function.IntConsumer;
+import java.util.function.LongConsumer;
+
+/**
+ * See <a
+ * href="https://docs.oracle.com/javase/8/docs/api/java/util/LongSummaryStatistics.html">the
+ * official Java API doc</a> for details.
+ */
+public class LongSummaryStatistics implements LongConsumer, IntConsumer {
+
+  private long count;
+  private long min = Long.MAX_VALUE;
+  private long max = Long.MIN_VALUE;
+  private long sum;
+
+  @Override
+  public void accept(int value) {
+    accept((long) value);
+  }
+
+  @Override
+  public void accept(long value) {
+    count++;
+    min = Math.min(min, value);
+    max = Math.max(max, value);
+    sum += value;
+  }
+
+  public void combine(LongSummaryStatistics other) {
+    count += other.count;
+    min = Math.min(min, other.min);
+    max = Math.max(max, other.max);
+    sum += other.sum;
+  }
+
+  public double getAverage() {
+    return count > 0 ? (double) sum / count : 0d;
+  }
+
+  public long getCount() {
+    return count;
+  }
+
+  public long getMin() {
+    return min;
+  }
+
+  public long getMax() {
+    return max;
+  }
+
+  public long getSum() {
+    return sum;
+  }
+
+  @Override
+  public String toString() {
+    return "LongSummaryStatistics[" +
+        "count = " + count +
+        ", avg = " + getAverage() +
+        ", min = " + min +
+        ", max = " + max +
+        ", sum = " + sum +
+        "]";
+  }
+}
diff --git a/user/test/com/google/gwt/emultest/EmulJava8Suite.java b/user/test/com/google/gwt/emultest/EmulJava8Suite.java
index d713785..09394ae 100644
--- a/user/test/com/google/gwt/emultest/EmulJava8Suite.java
+++ b/user/test/com/google/gwt/emultest/EmulJava8Suite.java
@@ -15,6 +15,9 @@
  */
 package com.google.gwt.emultest;
 
+import com.google.gwt.emultest.java8.util.DoubleSummaryStatisticsTest;
+import com.google.gwt.emultest.java8.util.IntSummaryStatisticsTest;
+import com.google.gwt.emultest.java8.util.LongSummaryStatisticsTest;
 import com.google.gwt.emultest.java8.util.OptionalDoubleTest;
 import com.google.gwt.emultest.java8.util.OptionalIntTest;
 import com.google.gwt.emultest.java8.util.OptionalLongTest;
@@ -40,7 +43,9 @@
     suite.addTestSuite(OptionalDoubleTest.class);
     suite.addTestSuite(PrimitiveIteratorTest.class);
     suite.addTestSuite(StringJoinerTest.class);
-
+    suite.addTestSuite(DoubleSummaryStatisticsTest.class);
+    suite.addTestSuite(IntSummaryStatisticsTest.class);
+    suite.addTestSuite(LongSummaryStatisticsTest.class);
     return suite;
   }
 }
diff --git a/user/test/com/google/gwt/emultest/java8/util/DoubleSummaryStatisticsTest.java b/user/test/com/google/gwt/emultest/java8/util/DoubleSummaryStatisticsTest.java
new file mode 100644
index 0000000..bb4b3b0
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java8/util/DoubleSummaryStatisticsTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed 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 com.google.gwt.emultest.java8.util;
+
+import com.google.gwt.emultest.java.util.EmulTestBase;
+
+import static java.lang.Double.MAX_VALUE;
+import static java.lang.Double.MIN_VALUE;
+import static java.lang.Double.NEGATIVE_INFINITY;
+import static java.lang.Double.NaN;
+import static java.lang.Double.POSITIVE_INFINITY;
+
+import java.util.Arrays;
+import java.util.DoubleSummaryStatistics;
+import java.util.List;
+
+/**
+ * Tests {@link DoubleSummaryStatistics}.
+ */
+public class DoubleSummaryStatisticsTest extends EmulTestBase {
+
+  private DoubleSummaryStatistics stats;
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    stats = new DoubleSummaryStatistics();
+  }
+
+  public void testAverageAndSumWithCompensation() throws Exception {
+    assertEquals(0d, stats.getAverage());
+    assertEquals(0d, stats.getSum());
+
+    double initial = 1.0d;
+    long count = 1000000;
+
+    // 'precision' is the hardcoded result of Math.ulp(initial) in JVM,
+    // since GWT does not emulate Math.ulp().
+    // This value represents the distance from 'initial' (1.0d) to the
+    // previous/next double. If we add half or less of that distance/precision
+    // to 'initial' then the result will be truncated to 'initial' again due to
+    // floating point arithmetic rounding.
+    // With Kahan summation such rounding errors are detected and compensated
+    // so the summation result should (nearly) equal the expected sum.
+    double precision = 2.220446049250313E-16;
+    double value = precision / 2;
+    double expectedSum = initial + (count * value);
+    long expectedCount = count + 1;
+    double expectedAverage = expectedSum / expectedCount;
+
+    stats.accept(initial);
+    for (int i = 0; i < count; ++i) {
+      stats.accept(value);
+    }
+
+    // TODO (jnehlmeier): While delta = 0 works we probably want to allow some error?
+    // Or maybe use ulp differences instead of a delta?
+    assertEquals(expectedAverage, stats.getAverage(), 0);
+    assertEquals(expectedSum, stats.getSum(), 0);
+  }
+
+  public void testCombine() throws Exception {
+    stats.accept(1.0d);
+    stats.accept(2.0d);
+
+    DoubleSummaryStatistics otherStats = new DoubleSummaryStatistics();
+    otherStats.accept(3.0d);
+    otherStats.accept(4.0d);
+
+    stats.combine(otherStats);
+
+    assertEquals(2.5d, stats.getAverage());
+    assertEquals(1d, stats.getMin());
+    assertEquals(4d, stats.getMax());
+    assertEquals(10d, stats.getSum());
+    assertEquals(4, stats.getCount());
+  }
+
+  public void testCountMaxMin() {
+    assertEquals(0, stats.getCount());
+    assertEquals(NEGATIVE_INFINITY, stats.getMax());
+    assertEquals(POSITIVE_INFINITY, stats.getMin());
+
+    double[][] testData = {
+        //          aDouble,               max,               min
+        {               1.0,               1.0,               1.0 },
+        {              -1.0,               1.0,              -1.0 },
+        {               2.5,               2.5,              -1.0 },
+        {              -2.5,               2.5,              -2.5 },
+        {         MAX_VALUE,         MAX_VALUE,              -2.5 },
+        {         MIN_VALUE,         MAX_VALUE,              -2.5 },
+        { POSITIVE_INFINITY, POSITIVE_INFINITY,              -2.5 },
+        { NEGATIVE_INFINITY, POSITIVE_INFINITY, NEGATIVE_INFINITY },
+    };
+
+    for (int i = 0; i < testData.length; ++i) {
+      long expectedCount = i + 1;
+      double aDouble = testData[i][0];
+      double expectedMax = testData[i][1];
+      double expectedMin = testData[i][2];
+
+      stats.accept(aDouble);
+
+      assertEquals(expectedCount, stats.getCount());
+      assertEquals(expectedMax, stats.getMax());
+      assertEquals(expectedMin, stats.getMin());
+    }
+  }
+
+  public void testInfinity() {
+    stats.accept(NEGATIVE_INFINITY);
+    stats.accept(NEGATIVE_INFINITY);
+    assertEquals(NEGATIVE_INFINITY, stats.getAverage());
+    assertEquals(NEGATIVE_INFINITY, stats.getMax());
+    assertEquals(NEGATIVE_INFINITY, stats.getMin());
+    assertEquals(NEGATIVE_INFINITY, stats.getSum());
+
+    stats.accept(POSITIVE_INFINITY);
+    assertTrue(Double.isNaN(stats.getAverage()));
+    assertEquals(POSITIVE_INFINITY, stats.getMax());
+    assertEquals(NEGATIVE_INFINITY, stats.getMin());
+    assertTrue(Double.isNaN(stats.getSum()));
+
+    stats = new DoubleSummaryStatistics();
+    stats.accept(POSITIVE_INFINITY);
+    stats.accept(POSITIVE_INFINITY);
+    assertEquals(POSITIVE_INFINITY, stats.getAverage());
+    assertEquals(POSITIVE_INFINITY, stats.getMax());
+    assertEquals(POSITIVE_INFINITY, stats.getMin());
+    assertEquals(POSITIVE_INFINITY, stats.getSum());
+
+    stats.accept(NEGATIVE_INFINITY);
+    assertTrue(Double.isNaN(stats.getAverage()));
+    assertEquals(POSITIVE_INFINITY, stats.getMax());
+    assertEquals(NEGATIVE_INFINITY, stats.getMin());
+    assertTrue(Double.isNaN(stats.getSum()));
+  }
+
+  public void testNaN() {
+    List<Double> testData = Arrays.asList(
+        NaN, -1.5d, 2.5d, MAX_VALUE, MIN_VALUE, NaN,
+        NEGATIVE_INFINITY, POSITIVE_INFINITY);
+
+    stats.accept(5.0d);
+    for (Double aDouble : testData) {
+      stats.accept(aDouble);
+      assertTrue(Double.isNaN(stats.getAverage()));
+      assertTrue(Double.isNaN(stats.getMax()));
+      assertTrue(Double.isNaN(stats.getMin()));
+      assertTrue(Double.isNaN(stats.getSum()));
+    }
+  }
+}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/emultest/java8/util/IntSummaryStatisticsTest.java b/user/test/com/google/gwt/emultest/java8/util/IntSummaryStatisticsTest.java
new file mode 100644
index 0000000..e1a49d2
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java8/util/IntSummaryStatisticsTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed 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 com.google.gwt.emultest.java8.util;
+
+import com.google.gwt.emultest.java.util.EmulTestBase;
+
+import static java.lang.Integer.MAX_VALUE;
+import static java.lang.Integer.MIN_VALUE;
+
+import java.util.IntSummaryStatistics;
+
+/**
+ * Tests {@link IntSummaryStatistics}.
+ */
+public class IntSummaryStatisticsTest extends EmulTestBase {
+
+  private IntSummaryStatistics stats;
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    stats = new IntSummaryStatistics();
+  }
+
+  public void testCombine() throws Exception {
+
+    stats.accept(1);
+    stats.accept(2);
+
+    IntSummaryStatistics otherStats = new IntSummaryStatistics();
+    otherStats.accept(3);
+    otherStats.accept(4);
+
+    stats.combine(otherStats);
+
+    assertEquals(2.5d, stats.getAverage());
+    assertEquals(1, stats.getMin());
+    assertEquals(4, stats.getMax());
+    assertEquals(10L, stats.getSum());
+    assertEquals(4L, stats.getCount());
+  }
+
+  public void testStats() {
+
+    assertEquals(0L, stats.getCount());
+    assertEquals(0d, stats.getAverage());
+    assertEquals(MIN_VALUE, stats.getMax());
+    assertEquals(MAX_VALUE, stats.getMin());
+    assertEquals(0L, stats.getSum());
+
+    int[][] testData = {
+        //    anInt,       max,       min,       sum
+        {         1,         1,         1,         1 },
+        {        -1,         1,        -1,         0 },
+        {         2,         2,        -1,         2 },
+        {        -2,         2,        -2,         0 },
+        { MAX_VALUE, MAX_VALUE,        -2, MAX_VALUE },
+        { MIN_VALUE, MAX_VALUE, MIN_VALUE,        -1 },
+    };
+
+    for (int i = 0; i < testData.length; ++i) {
+      long expectedCount = i + 1;
+      int anInt = testData[i][0];
+      int expectedMax = testData[i][1];
+      int expectedMin = testData[i][2];
+      long expectedSum = testData[i][3];
+      double expectedAverage = expectedSum / (double) expectedCount;
+
+      stats.accept(anInt);
+
+      assertEquals(expectedAverage, stats.getAverage());
+      assertEquals(expectedCount, stats.getCount());
+      assertEquals(expectedMax, stats.getMax());
+      assertEquals(expectedMin, stats.getMin());
+      assertEquals(expectedSum, stats.getSum());
+    }
+  }
+}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/emultest/java8/util/LongSummaryStatisticsTest.java b/user/test/com/google/gwt/emultest/java8/util/LongSummaryStatisticsTest.java
new file mode 100644
index 0000000..518351c
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java8/util/LongSummaryStatisticsTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Licensed 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 com.google.gwt.emultest.java8.util;
+
+import com.google.gwt.emultest.java.util.EmulTestBase;
+
+import static java.lang.Long.MAX_VALUE;
+import static java.lang.Long.MIN_VALUE;
+
+import java.util.LongSummaryStatistics;
+
+/**
+ * Tests {@link LongSummaryStatistics}.
+ */
+public class LongSummaryStatisticsTest extends EmulTestBase {
+
+  private LongSummaryStatistics stats;
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    stats = new LongSummaryStatistics();
+  }
+
+  public void testCombine() throws Exception {
+
+    stats.accept(1);
+    stats.accept(2);
+
+    LongSummaryStatistics otherStats = new LongSummaryStatistics();
+    otherStats.accept(3);
+    otherStats.accept(4);
+
+    stats.combine(otherStats);
+
+    assertEquals(2.5d, stats.getAverage());
+    assertEquals(1, stats.getMin());
+    assertEquals(4, stats.getMax());
+    assertEquals(10L, stats.getSum());
+    assertEquals(4L, stats.getCount());
+  }
+
+  public void testStats() {
+
+    assertEquals(0L, stats.getCount());
+    assertEquals(0d, stats.getAverage());
+    assertEquals(MIN_VALUE, stats.getMax());
+    assertEquals(MAX_VALUE, stats.getMin());
+    assertEquals(0L, stats.getSum());
+
+    long[][] testData = {
+        //    aLong,       max,       min,       sum
+        {         1,         1,         1,         1 },
+        {        -1,         1,        -1,         0 },
+        {         2,         2,        -1,         2 },
+        {        -2,         2,        -2,         0 },
+        { MAX_VALUE, MAX_VALUE,        -2, MAX_VALUE },
+        { MIN_VALUE, MAX_VALUE, MIN_VALUE,        -1 },
+    };
+
+    for (int i = 0; i < testData.length; ++i) {
+      long expectedCount = i + 1;
+      long aLong = testData[i][0];
+      long expectedMax = testData[i][1];
+      long expectedMin = testData[i][2];
+      long expectedSum = testData[i][3];
+      double expectedAverage = expectedSum / (double) expectedCount;
+
+      stats.accept(aLong);
+
+      assertEquals(expectedAverage, stats.getAverage());
+      assertEquals(expectedCount, stats.getCount());
+      assertEquals(expectedMax, stats.getMax());
+      assertEquals(expectedMin, stats.getMin());
+      assertEquals(expectedSum, stats.getSum());
+    }
+  }
+}
\ No newline at end of file