How to average BigDecimals using Streams?

20,741

Solution 1

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.

Solution 2

You don't need to stream twice. Simply call List.size() for the count:

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

Solution 3

Alternatively you can use this Collector implementation:

class BigDecimalAverageCollector implements Collector<BigDecimal, BigDecimalAccumulator, BigDecimal> {

    @Override
    public Supplier<BigDecimalAccumulator> supplier() {
        return BigDecimalAccumulator::new;
    }

    @Override
    public BiConsumer<BigDecimalAccumulator, BigDecimal> accumulator() {
        return BigDecimalAccumulator::add;
    }

    @Override
    public BinaryOperator<BigDecimalAccumulator> combiner() {
        return BigDecimalAccumulator::combine;
    }

    @Override
    public Function<BigDecimalAccumulator, BigDecimal> finisher() {
        return BigDecimalAccumulator::getAverage;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }

    @NoArgsConstructor
    @AllArgsConstructor
    static class BigDecimalAccumulator {
        @Getter private BigDecimal sum = BigDecimal.ZERO;
        @Getter private BigDecimal count = BigDecimal.ZERO;

        BigDecimal getAverage() {
           return BigDecimal.ZERO.compareTo(count) == 0 ?
                  BigDecimal.ZERO :
                  sum.divide(count, 2, BigDecimal.ROUND_HALF_UP);
        }

        BigDecimalAccumulator combine(BigDecimalAccumulator another) {
            return new BigDecimalAccumulator(
                    sum.add(another.getSum()),
                    count.add(another.getCount())
            );
        }

        void add(BigDecimal successRate) {
            count = count.add(BigDecimal.ONE);
            sum = sum.add(successRate);
        }
    }

}

And use it like that:

BigDecimal mean = bigDecimals.stream().collect(new BigDecimalAverageCollector());

Note: example uses Project Lombok annotations to shorten the glue code.

Solution 4

I use the above method in order to get the average of a list of BigDecimal objects. The list allows null values.

public BigDecimal bigDecimalAverage(List<BigDecimal> bigDecimalList, RoundingMode roundingMode) {
    // Filter the list removing null values
    List<BigDecimal> bigDecimals = bigDecimalList.stream().filter(Objects::nonNull).collect(Collectors.toList());

    // Special cases
    if (bigDecimals.isEmpty())
        return null;
    if (bigDecimals.size() == 1)
        return bigDecimals.get(0);

    // Return the average of the BigDecimals in the list
    return bigDecimals.stream().reduce(BigDecimal.ZERO, BigDecimal::add).divide(new BigDecimal(bigDecimals.size()), roundingMode);
}

Solution 5

If you don't mind a third party dependency, the following will work with Eclipse Collections Collectors2.summarizingBigDecimal() by calling getAverage with a MathContext, which includes a RoundingMode.

MutableDoubleList doubles = DoubleLists.mutable.with(1.0, 2.0, 3.0, 4.0);
List<BigDecimal> bigDecimals = doubles.collect(BigDecimal::new);
BigDecimal average =
        bigDecimals.stream()
                .collect(Collectors2.summarizingBigDecimal(e -> e))
                .getAverage(MathContext.DECIMAL32);

Assert.assertEquals(BigDecimal.valueOf(2.5), average);

A version of getAverage could be added to accept RoundingMode as well.

Note: I am a committer for Eclipse Collections.

Share:
20,741

Related videos on Youtube

Patrick Garner
Author by

Patrick Garner

Updated on October 16, 2020

Comments

  • Patrick Garner
    Patrick Garner over 3 years

    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)?

    • smk
      smk over 8 years
      Can the reduce function accumulator take in a tuple, one which represents the sum and the other a count? Havent researched this sorry
    • Someone Somewhere
      Someone Somewhere almost 6 years
      is it more efficient with streams ?
  • Teodor Marinescu
    Teodor Marinescu about 5 years
    This is a bit dangerous unless you're (making) sure the list size is not 0.
  • xehpuk
    xehpuk about 5 years
    @TeodorMarinescu One could return Optional<BigDecimal> for this case. I think the thrown ArithmeticException is fine as well.