Усреднение BigDecimals с использованием Streams API Collectors - PullRequest
0 голосов
/ 20 января 2019

Текущий подход основан на двойном типе приза продукта.

public Map<String, BigDecimal> averageProductPriceInCategory() {

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

покупки в основном карта: Map<Client, Map<Product,Integer>>,

  • Внешний ключпредставляет Клиента
  • Внутренний Ключ представляет Продукт.Членами класса продукта являются имя, категория, цена (ранее двойного типа) - хотите преобразовать предоставленный код в код, используя цену в качестве типа BigDecimal
  • значение внутренних карт (целое число)представляет число указанного продукта, принадлежащего конкретному клиенту

Ниже фрагмент может использоваться только для расчета общего приза продуктов, которые относятся к указанной категории.Не уверен, как рассчитать средний приз товаров по категории с помощью 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))));

Ответы [ 4 ]

0 голосов
/ 20 января 2019

Вы можете создать своего собственного коллекционера следующим образом:

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

... а затем используйте его:

Map<String, BigDecimal> totalProductPriceInEachCategory = shopping.values().stream()
      .flatMap(e -> e.keySet().stream())
      .collect(groupingBy(Product::getCategory, mapping(Product::getPrice, avgCollector)));
0 голосов
/ 20 января 2019

Я разбил операции на 2 шага для понимания целей. Вы можете объединить два шага, если хотите.

    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])));

Пояснение:

  • В первой операции, после группировки, поток BigDecimals отображается как поток из двух массивов элементов BigDecimal, где первый элемент - это элемент из исходного потока, а второй - заполнитель со значением один.
  • При уменьшении значение a из (a,b) имеет частичную сумму в первом элементе и частичное число во втором элементе. Первый элемент элемента b содержит каждое из значений BigDecimal, добавляемых к сумме. Второй элемент b не используется.
  • Reduce возвращает необязательный параметр, который будет пустым, если список был пуст или содержал только нулевые значения.
    • Если Optional не пустой, функция Optional.get () вернет двухэлементный массив BigDecimal, где сумма BigDecimals находится в первом элементе, а число BigDecimals - во втором.
    • Если необязательный параметр пуст, генерируется исключение NoSuchElementException.
  • Среднее значение вычисляется путем деления суммы на количество. Это делается для каждой записи в промежуточной карте Map<String, BigDecimal[]> stringMap
0 голосов
/ 20 января 2019

Это основано на исходном коде [Double|Int]Pipeline.average(). Он использует массив для хранения количества элементов (по индексу 0) и суммы (по индексу 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)
          )
      ));
}

У этого есть несколько недостатков:

  1. Не удобно использовать более чем в одном месте.
  2. Не обязательно легко следовать.
  3. Сохраняет счет как BigDecimal, где использование int или long имело бы больше смысла.

Эти проблемы могут быть решены путем извлечения коллектора в пользовательский класс (как ответ Эндрю делает).

0 голосов
/ 20 января 2019

Посмотрите, как реализовано Collectors.averagingDouble или Collectors.averagingInt.

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

В сущности, вам нужен изменяемый тип накопления, который будет содержатьBigDecimal, который представляет собой сумму цен на продукты, и int, который представляет собой количество обработанных продуктов.Имея это, проблема сводится к написанию простого Collector<Product, AccumulationType, BigDecimal>.

Я упростил пример и удалил геттеры / сеттеры и конструктор всех аргументов.Вместо вложенного класса ProductPriceSummary вы можете использовать любой класс изменяемого держателя для 2 элементов.

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

}
...