Approche actuelle basée sur le double type de prix du produit.
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))));
shopping est essentiellement une carte: Map
,
L'extrait ci-dessous ne peut être utilisé que pour calculer le prix total des produits appartenant à une catégorie spécifiée. Je ne sais pas comment calculer le prix moyen des produits par rapport à la catégorie en utilisant BigDecimals
public Map<String, BigDecimal> averageProductPriceInCategory() { return shopping.entrySet() .stream() .flatMap(e -> e.getValue().keySet().stream()) .collect(Collectors.groupingBy(Product::getCategory, Collectors.averagingDouble(Product::getPrize))); }
4 Réponses :
Découvrez comment Collectors.averagingDouble
ou Collectors.averagingInt code >
est implémenté.
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(); } }
Pour l'essentiel, vous avez besoin d'un type d'accumulation modifiable qui contiendrait un BigDecimal
qui est la somme des prix des produits et un int qui est un nombre de produits traités. Ayant cela, le problème se résume à l'écriture d'un simple Collector
.
J'ai simplifié un exemple et supprimé les getters / setters et un all-args constructeur. Au lieu d'une classe imbriquée ProductPriceSummary
, vous pouvez utiliser n'importe quelle classe de support mutable pour 2 éléments.
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); }
Ou utilisez la solution plus large de cette réponse , … stream .collect (BigDecimalSummaryStatistics.statistics ()) .getAverage ( MathContext.DECIMAL128))
…
J'ai décomposé les opérations en 2 étapes à des fins de compréhension. Vous pouvez combiner les deux étapes si vous le souhaitez.
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])));
Explication:
a
de (a, b)
a la somme partielle dans le premier élément et le décompte partiel dans le deuxième élément. Le premier élément de l'élément b
contient chacune des valeurs BigDecimal à ajouter à la somme. Le deuxième élément de b
n'est pas utilisé. Map stringMap
Ceci est basé sur le code source de [Double | Int] Pipeline.average ()
. Il utilise un tableau pour stocker le nombre d'éléments (à l'index 0
) et la somme (à l'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) ) )); }
Cela présente quelques inconvénients:
BigDecimal
où l'utilisation d'un int
ou long
aurait plus de sens. Ces problèmes peuvent être résolus en extrayant le collecteur dans une classe personnalisée (comme le fait la réponse d'Andrew ) .
Vous pouvez créer votre propre collecteur comme celui-ci:
Map<String, BigDecimal> totalProductPriceInEachCategory = shopping.values().stream() .flatMap(e -> e.keySet().stream()) .collect(groupingBy(Product::getCategory, mapping(Product::getPrice, avgCollector)));
... puis l'utiliser:
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> - La clé représente la catégorie de produits - la valeur (BigDecimal) représente le prix moyen des produits dans la catégorie spécifiée