1
votes

Moyenne de BigDecimals à l'aide de collecteurs d'API Streams

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 > ,

  • La clé externe représente le client
  • La clé interne représente le produit. Les membres de la classe de produits sont nom, catégorie, prix (auparavant de type double) - veulent refactoriser le code fourni en un seul en utilisant le prix comme type de BigDecimal
  • la valeur de la carte interne (Integer) représente le nombre de produits spécifiés qui appartiennent à un client spécifique

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


1 commentaires

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


4 Réponses :


3
votes

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


1 commentaires

Ou utilisez la solution plus large de cette réponse , … stream .collect (BigDecimalSummaryStatistics.statistics ()) .getAverage ( MathContext.DECIMAL128))



0
votes

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:

  • Dans la première opération, après le regroupement, le flux de BigDecimals est mappé en tant que flux de deux tableaux d'éléments de BigDecimal où le premier élément est l'élément du flux d'origine et le second est l'espace réservé avec la valeur un.
  • Dans la réduction, la valeur 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é.
  • Reduce renvoie une valeur facultative qui sera vide si la liste était vide ou ne contenait que des valeurs nulles.
    • Si Optional n'est pas vide, la fonction Optional.get () retournera un tableau à deux éléments de BigDecimal où la somme des BigDecimals est dans le premier élément et le nombre de BigDecimals est dans le second.
    • Si facultatif est vide, NoSuchElementException sera levée.
  • La moyenne est calculée en divisant la somme par le nombre. Ceci est fait pour chaque entrée de la carte intermédiaire Map stringMap


0 commentaires

0
votes

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:

  1. Pas pratique à utiliser à plusieurs endroits.
  2. Pas forcément facile à suivre.
  3. Stocke le décompte sous forme de 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 ) .


0 commentaires

0
votes

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


0 commentaires