2

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))));
Tamara Koliada
  • 1,200
  • 2
  • 14
  • 31
Schroedinger
  • 301
  • 6
  • 17
  • Map > - The Key represents the Products Category - the value (BigDecimal) reprents the average products prize in specified Category – Schroedinger Jan 20 '19 at 17:59

4 Answers4

3

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

}
Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
  • 1
    Or use the broader solution of [this answer](https://stackoverflow.com/a/51652539/2711488), `…stream .collect(BigDecimalSummaryStatistics.statistics()) .getAverage(MathContext.DECIMAL128))`… – Holger Jan 21 '19 at 14:17
2

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)));
Donat
  • 4,157
  • 3
  • 11
  • 26
0

I've broken down the operations into 2 steps for understanding purposes. You can combine the two steps if you feel like.

    Map<String, BigDecimal[]> stringMap = shopping.entrySet()
            .stream()
            .flatMap(e -> e.getValue().keySet().stream())
            .collect(Collectors.groupingBy(Product::getCategory,Collectors.collectingAndThen(Collectors.toList(),l -> l.stream().map(Product::getPrize)
                    .map(bd -> new BigDecimal[]{bd, BigDecimal.ONE})
                    .reduce((a, b) -> new BigDecimal[]{a[0].add(b[0]), a[1].add(BigDecimal.ONE)})
                    .get()
            )));

    Map<String, BigDecimal> stringBigDecimalMap = stringMap.entrySet().stream()
            .collect(Collectors.toMap(Map.Entry::getKey,e -> e.getValue()[0].divide(e.getValue()[1])));

Explanation:

  • In the first operation, after grouping, 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. This is done for every entry in the intermediate map Map<String, BigDecimal[]> stringMap
Pankaj Singhal
  • 15,283
  • 9
  • 47
  • 86
0

This is based on the source code of [Double|Int]Pipeline.average(). It uses an array to store the count of items (at index 0) and the sum (at index 1).

public Map<String, BigDecimal> averageProductPriceInCategory() {
  return shopping.entrySet().stream()
      .flatMap(entry -> entry.getValue().keySet().stream())
      .collect(Collectors.groupingBy(
          Product::getCategory,
          Collector.of(
              () -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO},
              (array, product) -> {
                array[0] = array[0].add(BigDecimal.ONE);
                array[1] = array[1].add(product.getPrice());
              },
              (left, right) -> {
                left[0] = left[0].add(right[0]);
                left[1] = left[1].add(right[1]);
                return left;
              },
              array -> array[0].compareTo(BigDecimal.ONE) <= 0 
                       ? array[1] 
                       : array[1].divide(array[0], RoundingMode.HALF_UP)
          )
      ));
}

This has a few downsides:

  1. Not convenient to use in more than one place.
  2. Not necessarily easy to follow.
  3. Stores the count as a BigDecimal where using an int or long would make more sense.

These problems can be solved by extracting the collector into a custom class (as Andrew's answer does).

Slaw
  • 37,820
  • 8
  • 53
  • 80