How to average BigDecimals using Streams?
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 theb
element contains each of the BigDecimal values to add to the sum. The second element ofb
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.
Related videos on Youtube
Patrick Garner
Updated on October 16, 2020Comments
-
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 over 8 yearsCan the reduce function accumulator take in a tuple, one which represents the sum and the other a count? Havent researched this sorry
-
Someone Somewhere almost 6 yearsis it more efficient with streams ?
-
-
Teodor Marinescu about 5 yearsThis is a bit dangerous unless you're (making) sure the list size is not 0.
-
xehpuk about 5 years@TeodorMarinescu One could return
Optional<BigDecimal>
for this case. I think the thrownArithmeticException
is fine as well.