30

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

2
  • 1
    Can the reduce function accumulator take in a tuple, one which represents the sum and the other a count? Havent researched this sorry
    – smk
    Commented Aug 7, 2015 at 15:43
  • is it more efficient with streams ? Commented May 16, 2018 at 19:37

6 Answers 6

22
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.
21

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);
}
2
  • This is a bit dangerous unless you're (making) sure the list size is not 0. Commented Mar 1, 2019 at 14:17
  • 2
    @TeodorMarinescu One could return Optional<BigDecimal> for this case. I think the thrown ArithmeticException is fine as well.
    – xehpuk
    Commented Mar 1, 2019 at 14:25
7

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.

0
7

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);
}
3

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.

0

I didn't want to count the size of my stream. Then, I developed the following using accumulator and combiner.

Stream<BigDecimal> bigDecimalStream = ...
BigDecimalAverager sum = bigDecimalStream.reduce(new BigDecimalAverager(),
                BigDecimalAverager::accept,
                BigDecimalAverager::combine);
sum.average();

and, here is the code for the identity class;

class BigDecimalAverager {
    private final BigDecimal total;
    private final int count;

    public BigDecimalAverager() {
        this.total = BigDecimal.ZERO;
        this.count = 0;
    }

    public BigDecimalAverager(BigDecimal total, int count) {
        this.total = total;
        this.count = count;
    }

    public BigDecimalAverager accept(BigDecimal bigDecimal) {
        return new BigDecimalAverager(total.add(bigDecimal), count + 1);
    }

    public BigDecimalAverager combine(BigDecimalAverager other) {
        return new BigDecimalAverager(total.add(other.total), count + other.count);
    }

    public BigDecimal average() {
        return count > 0 ? total.divide(new BigDecimal(count), RoundingMode.HALF_UP) : BigDecimal.ZERO;
    }

}

It is up to you how to round the divided value though (I use RoundingMode.HALF_UP for my case).

The above is similar to the way explained in https://stackoverflow.com/a/23661052/1572286

Not the answer you're looking for? Browse other questions tagged or ask your own question.