Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Averaging BigDecimals using Streams API Collectors

Current approach based on double type of product prize.

public Map<String, BigDecimal> averageProductPriceInCategory() {

    return shopping.entrySet()
            .stream()
            .flatMap(e -> e.getValue().keySet().stream())
            .collect(Collectors.groupingBy(Product::getCategory,
                    Collectors.averagingDouble(Product::getPrize)));
}

shopping is basically a map: Map<Client, Map<Product,Integer>>,

  • The outer Key represents the Client
  • The inner Key represents the Product. Product class members are name, category, price (previously of double type) - want to refactor the provided code into one using price as a type of BigDecimal
  • the inner maps value (Integer) reprents the number of specified product which belong to a specific client

Below snippet can be used only for calculating the total prize of products which belong to a specified category. Not sure, how to calculate the average products prize in regards to category with using BigDecimals

Map<String, BigDecimal> totalProductPriceInEachCategory = shopping.entrySet().stream()
                .flatMap(e -> e.getValue().keySet().stream())
                .collect(Collectors.groupingBy(Product::getCategory,
                        Collectors.mapping(Product::getPrize,
                                Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
like image 335
Schroedinger Avatar asked Jul 03 '26 15:07

Schroedinger


2 Answers

Take a look at how Collectors.averagingDouble or Collectors.averagingInt is implemented.

public static <T> Collector<T, ?, Double>
averagingInt(ToIntFunction<? super T> mapper) {
    return new CollectorImpl<>(
            () -> new long[2],
            (a, t) -> { a[0] += mapper.applyAsInt(t); a[1]++; },
            (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; },
            a -> (a[1] == 0) ? 0.0d : (double) a[0] / a[1], CH_NOID);
}

Essentially, you need a mutable accumulation type that would hold a BigDecimal which is a sum of product prices, and an int which is a number of products processed. Having that, the problem boils down to writing a simple Collector<Product, AccumulationType, BigDecimal>.

I simplified an example and removed getters/setters and an all-args constructor. Instead of a nested class ProductPriceSummary, you might use any mutable holder class for 2 elements.

class AverageProductPriceCollector implements Collector<Product, AverageProductPriceCollector.ProductPriceSummary, BigDecimal> {

    static class ProductPriceSummary {

        private BigDecimal sum = BigDecimal.ZERO;
        private int n;

    }

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

    @Override
    public BiConsumer<ProductPriceSummary, Product> accumulator() {
        return (a, p) -> {
            // if getPrize() still returns double
            // a.sum = a.sum.add(BigDecimal.valueOf(p.getPrize()));

            a.sum = a.sum.add(p.getPrize());
            a.n += 1;
        };
    }

    @Override
    public BinaryOperator<ProductPriceSummary> combiner() {
        return (a, b) -> {
            ProductPriceSummary s = new ProductPriceSummary();
            s.sum = a.sum.add(b.sum);
            s.n = a.n + b.n;

            return s;
        };
    }

    @Override
    public Function<ProductPriceSummary, BigDecimal> finisher() {
        return s -> s.n == 0 ?
                   BigDecimal.ZERO :
                   s.sum.divide(BigDecimal.valueOf(s.n), RoundingMode.CEILING);
    }

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

}
like image 179
Andrew Tobilko Avatar answered Jul 06 '26 04:07

Andrew Tobilko


You can make an own collector like this:

Collector<BigDecimal, BigDecimal[], BigDecimal> avgCollector = Collector.of(
      () -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO},
      (pair, val) -> {
        pair[0] = pair[0].add(val);
        pair[1] = pair[1].add(BigDecimal.ONE);
      },
      (pair1, pair2) -> new BigDecimal[]{pair1[0].add(pair2[0]), pair1[1].add(pair2[1])},
      (pair) -> pair[0].divide(pair[1], 2, RoundingMode.HALF_UP)
);

... and then use it:

Map<String, BigDecimal> totalProductPriceInEachCategory = shopping.values().stream()
      .flatMap(e -> e.keySet().stream())
      .collect(groupingBy(Product::getCategory, mapping(Product::getPrice, avgCollector)));
like image 21
Donat Avatar answered Jul 06 '26 04:07

Donat



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!