6

I have a list of BigDecimals.

List<BigDecimal> amounts = new ArrayList<>()

How do i get the summary statistics of the above list using Java 8 streams without losing precision of upto 3-4 decimal places of the BigDecimal?

Naman
  • 27,789
  • 26
  • 218
  • 353
J28
  • 1,080
  • 3
  • 15
  • 25

3 Answers3

15

I created a BigDecimal specialization of the generic summary statistics collector of this answer, which allowed extending it to also support summing, hence also calculating an average:

/**
 * Like {@code DoubleSummaryStatistics}, {@code IntSummaryStatistics}, and
 * {@code LongSummaryStatistics}, but for {@link BigDecimal}.
 */
public class BigDecimalSummaryStatistics implements Consumer<BigDecimal> {

    public static Collector<BigDecimal,?,BigDecimalSummaryStatistics> statistics() {
        return Collector.of(BigDecimalSummaryStatistics::new,
            BigDecimalSummaryStatistics::accept, BigDecimalSummaryStatistics::merge);
    }
    private BigDecimal sum = BigDecimal.ZERO, min, max;
    private long count;

    public void accept(BigDecimal t) {
        if(count == 0) {
            Objects.requireNonNull(t);
            count = 1;
            sum = t;
            min = t;
            max = t;
        }
        else {
            sum = sum.add(t);
            if(min.compareTo(t) > 0) min = t;
            if(max.compareTo(t) < 0) max = t;
            count++;
        }
    }
    public BigDecimalSummaryStatistics merge(BigDecimalSummaryStatistics s) {
        if(s.count > 0) {
            if(count == 0) {
                count = s.count;
                sum = s.sum;
                min = s.min;
                max = s.max;
            }
            else {
                sum = sum.add(s.sum);
                if(min.compareTo(s.min) > 0) min = s.min;
                if(max.compareTo(s.max) < 0) max = s.max;
                count += s.count;
            }
        }
        return this;
    }

    public long getCount() {
        return count;
    }

    public BigDecimal getSum()
    {
      return sum;
    }

    public BigDecimal getAverage(MathContext mc)
    {
      return count < 2? sum: sum.divide(BigDecimal.valueOf(count), mc);
    }

    public BigDecimal getMin() {
        return min;
    }

    public BigDecimal getMax() {
        return max;
    }

    @Override
    public String toString() {
        return count == 0? "empty": (count+" elements between "+min+" and "+max+", sum="+sum);
    }
}

It can be used similar to the DoubleSummaryStatistics counterpart, like

BigDecimalSummaryStatistics bds = list.stream().collect(BigDecimalSummaryStatistics.statistics());

As a full example:

List<BigDecimal> list = Arrays.asList(BigDecimal.ZERO, BigDecimal.valueOf(-2), BigDecimal.ONE);
BigDecimalSummaryStatistics bds = list.stream().collect(BigDecimalSummaryStatistics.statistics());
System.out.println(bds);
System.out.println("average: "+bds.getAverage(MathContext.DECIMAL128));
3 elements between -2 and 1, sum=-1
average: -0.3333333333333333333333333333333333
Holger
  • 285,553
  • 42
  • 434
  • 765
  • It seems you could use [`BigDecimal.max()`](https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html#max-java.math.BigDecimal-) and `.min()` instead of the `if(…compareTo(…))…` – Didier L Aug 02 '18 at 14:13
  • 1
    @DidierL indeed, these are appropriate for `BigDecimal`. This code uses the `compareTo` variant as it originated from the linked generic solution and I think, I keep it this way as it makes it easier to adapt to other element types. This makes me think…it would be a nice idea to offer `max` and `min` as `default` methods to all `Comparable`s… – Holger Aug 02 '18 at 14:58
  • I'd argue that it would be better to write custom `min()` / `max()` methods for types that don't have them, but I understand your point (I wonder if they have considered adding them to the `Comparator` interface) – Didier L Aug 02 '18 at 15:23
5

You could use Java Streams with Eclipse Collections which has a BigDecimalSummaryStatistics class:

List<BigDecimal> amounts = 
        Lists.mutable.with(BigDecimal.ONE, BigDecimal.TEN, BigDecimal.ZERO, BigDecimal.ONE);

BigDecimalSummaryStatistics stats =
        amounts.stream().collect(Collectors2.summarizingBigDecimal(each -> each));

Assert.assertEquals(BigDecimal.ZERO, stats.getMin());
Assert.assertEquals(BigDecimal.TEN, stats.getMax());
Assert.assertEquals(BigDecimal.valueOf(12L), stats.getSum());
Assert.assertEquals(BigDecimal.valueOf(3L), stats.getAverage());
Assert.assertEquals(4L, stats.getCount());

Note: I am a committer for Eclipse Collections

Donald Raab
  • 6,458
  • 2
  • 36
  • 44
1

If you're open to using a third party library (that is compatible with Java 8 streams), you could use jOOλ, using which you'd write:

Tuple5<
    Long, 
    Optional<BigDecimal>, 
    Optional<BigDecimal>, 
    Optional<BigDecimal>, 
    Optional<BigDecimal>
> tuple
amounts.stream()
       .collect(Tuple.collectors(
           Agg.sum(),
           Agg.count(),
           Agg.avg(),
           Agg.<BigDecimal>min(),
           Agg.<BigDecimal>max()
       ));

This would result in no loss of precision, but is probably quite slower than aggregating doubles

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509