How to average BigDecimals using Streams?

Patrick Garner picture Patrick Garner · Aug 7, 2015 · Viewed 16k times · Source

I'm wanting to take the following method:

public BigDecimal mean(List<BigDecimal> bigDecimals, RoundingMode roundingMode) {
    BigDecimal sum = BigDecimal.ZERO;
    int count=0;
    for(BigDecimal bigDecimal : bigDecimals) {
        if(null != bigDecimal) {
            sum = sum.add(bigDecimal);
            count++;
        }
    }
    return sum.divide(new BigDecimal(count), roundingMode);
}

and update it using the Streams api. Here's what I've got thus far:

public BigDecimal average(List<BigDecimal> bigDecimals, RoundingMode roundingMode) {
    BigDecimal sum = bigDecimals.stream()
        .map(Objects::requireNonNull)
        .reduce(BigDecimal.ZERO, BigDecimal::add);
    long count = bigDecimals.stream().filter(Objects::nonNull).count();
    return sum.divide(new BigDecimal(count), roundingMode);
}

Is there a way to do this without streaming twice (the second time to get the count)?

Answer

WillShackleford picture WillShackleford · Aug 7, 2015
BigDecimal[] totalWithCount
                = bigDecimals.stream()
                .filter(bd -> bd != null)
                .map(bd -> new BigDecimal[]{bd, BigDecimal.ONE})
                .reduce((a, b) -> new BigDecimal[]{a[0].add(b[0]), a[1].add(BigDecimal.ONE)})
                .get();
BigDecimal mean = totalWithCount[0].divide(totalWithCount[1], roundingMode);

Optional text description of the code for those that are find that to be helpful (Ignore if you find the code sufficiently self explanatory.):

  • The list of BigDecimals is converted to a stream.
  • null values are filtered out of the stream.
  • The stream of BigDecimals is mapped to as stream of two element arrays of BigDecimal where the first element is the element from the original stream and the second is the place holder with value one.
  • In the reduce the a of (a,b) value has the partial sum in the first element and the partial count in the second element. The first element of the b element contains each of the BigDecimal values to add to the sum. The second element of b is not used.
  • Reduce returns an optional that will be empty if the list was empty or contained only null values.
    • If the Optional is not empty, Optional.get() function will return a two element array of BigDecimal where the sum of the BigDecimals is in the first element and the count of the BigDecimals is in the second.
    • If the Optional is empty, NoSuchElementException will be thrown.
  • The mean is computed by dividing the sum by the count.