web-dev-qa-db-fra.com

Comment faire la moyenne de BigDecimals à l'aide de Streams?

Je veux prendre la méthode suivante:

public BigDecimal mean(List<BigDecimal> bigDecimals, RoundingMode roundingMode) {
    BigDecimal sum = BigDecimal.ZERO;
    int count=0;
    for(BigDecimal bigDecimal : bigDecimals) {
        if(null != bigDecimal) {
            sum = sum.add(bigDecimal);
            count++;
        }
    }
    return sum.divide(new BigDecimal(count), roundingMode);
}

et le mettre à jour à l'aide de l'API Streams. Voici ce que j'ai jusqu'à présent:

public BigDecimal average(List<BigDecimal> bigDecimals, RoundingMode roundingMode) {
    BigDecimal sum = bigDecimals.stream()
        .map(Objects::requireNonNull)
        .reduce(BigDecimal.ZERO, BigDecimal::add);
    long count = bigDecimals.stream().filter(Objects::nonNull).count();
    return sum.divide(new BigDecimal(count), roundingMode);
}

Existe-t-il un moyen de le faire sans diffuser deux fois (la deuxième fois pour obtenir le décompte)?

23
Patrick Garner
BigDecimal[] totalWithCount
                = bigDecimals.stream()
                .filter(bd -> bd != null)
                .map(bd -> new BigDecimal[]{bd, BigDecimal.ONE})
                .reduce((a, b) -> new BigDecimal[]{a[0].add(b[0]), a[1].add(BigDecimal.ONE)})
                .get();
BigDecimal mean = totalWithCount[0].divide(totalWithCount[1], roundingMode);

Description textuelle facultative du code pour ceux qui trouvent cela utile (Ignorez si vous trouvez le code suffisamment explicite.):

  • La liste des BigDecimals est convertie en flux.
  • les valeurs nulles sont filtrées hors du flux.
  • 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 le réduire le a de (a,b) value a la somme partielle dans le premier élément et le 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 option qui sera vide si la liste était vide ou ne contenait que des valeurs nulles.
    • Si le Facultatif n'est pas vide, la fonction Facultatif.get () renverra un tableau à deux éléments de BigDecimal où la somme des BigDecimals est dans le premier élément et le nombre des BigDecimals est dans le second.
    • Si le Facultatif est vide, NoSuchElementException sera levée.
  • La moyenne est calculée en divisant la somme par le nombre.
18
WillShackleford

Vous n'avez pas besoin de diffuser deux fois. Appelez simplement List.size() pour le nombre:

public BigDecimal average(List<BigDecimal> bigDecimals, RoundingMode roundingMode) {
    BigDecimal sum = bigDecimals.stream()
        .map(Objects::requireNonNull)
        .reduce(BigDecimal.ZERO, BigDecimal::add);
    return sum.divide(new BigDecimal(bigDecimals.size()), roundingMode);
}
14
xehpuk

Vous pouvez également utiliser cette implémentation Collector:

class BigDecimalAverageCollector implements Collector<BigDecimal, BigDecimalAccumulator, BigDecimal> {

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

    @Override
    public BiConsumer<BigDecimalAccumulator, BigDecimal> accumulator() {
        return BigDecimalAccumulator::add;
    }

    @Override
    public BinaryOperator<BigDecimalAccumulator> combiner() {
        return BigDecimalAccumulator::combine;
    }

    @Override
    public Function<BigDecimalAccumulator, BigDecimal> finisher() {
        return BigDecimalAccumulator::getAverage;
    }

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

    @NoArgsConstructor
    @AllArgsConstructor
    static class BigDecimalAccumulator {
        @Getter private BigDecimal sum = BigDecimal.ZERO;
        @Getter private BigDecimal count = BigDecimal.ZERO;

        BigDecimal getAverage() {
           return BigDecimal.ZERO.compareTo(count) == 0 ?
                  BigDecimal.ZERO :
                  sum.divide(count, 2, BigDecimal.ROUND_HALF_UP);
        }

        BigDecimalAccumulator combine(BigDecimalAccumulator another) {
            return new BigDecimalAccumulator(
                    sum.add(another.getSum()),
                    count.add(another.getCount())
            );
        }

        void add(BigDecimal successRate) {
            count = count.add(BigDecimal.ONE);
            sum = sum.add(successRate);
        }
    }

}

Et utilisez-le comme ça:

BigDecimal mean = bigDecimals.stream().collect(new BigDecimalAverageCollector());

Remarque: l'exemple utilise des annotations Project Lombok pour raccourcir le code de collage.

5
Novoj

Si cela ne vous dérange pas une dépendance tierce, ce qui suit fonctionnera avec Collections EclipseCollectors2.summarizingBigDecimal() en appelant getAverage avec un MathContext, ce qui inclut un RoundingMode.

MutableDoubleList doubles = DoubleLists.mutable.with(1.0, 2.0, 3.0, 4.0);
List<BigDecimal> bigDecimals = doubles.collect(BigDecimal::new);
BigDecimal average =
        bigDecimals.stream()
                .collect(Collectors2.summarizingBigDecimal(e -> e))
                .getAverage(MathContext.DECIMAL32);

Assert.assertEquals(BigDecimal.valueOf(2.5), average);

Une version de getAverage pourrait être ajoutée pour accepter également RoundingMode.

Remarque: je suis un committer pour les collections Eclipse.

2
Donald Raab

Je ne voulais pas compter la taille de mon flux. Ensuite, j'ai développé ce qui suit en utilisant un accumulateur et un combinateur.

Stream<BigDecimal> bigDecimalStream = ...
BigDecimalAverager sum = bigDecimalStream.reduce(new BigDecimalAverager(),
                BigDecimalAverager::accept,
                BigDecimalAverager::combine);
sum.average();

et, voici le code de la classe d'identité;

class BigDecimalAverager {
    private final BigDecimal total;
    private final int count;

    public BigDecimalAverager() {
        this.total = BigDecimal.ZERO;
        this.count = 0;
    }

    public BigDecimalAverager(BigDecimal total, int count) {
        this.total = total;
        this.count = count;
    }

    public BigDecimalAverager accept(BigDecimal bigDecimal) {
        return new BigDecimalAverager(total.add(bigDecimal), count + 1);
    }

    public BigDecimalAverager combine(BigDecimalAverager other) {
        return new BigDecimalAverager(total.add(other.total), count + other.count);
    }

    public BigDecimal average() {
        return count > 0 ? total.divide(new BigDecimal(count), RoundingMode.HALF_UP) : BigDecimal.ZERO;
    }

}

C'est à vous de savoir comment arrondir la valeur divisée (j'utilise RoundingMode.HALF_UP pour mon cas).

Ce qui précède est similaire à la manière expliquée dans https://stackoverflow.com/a/23661052/1572286

0
Youness

J'utilise la méthode ci-dessus afin d'obtenir la moyenne d'une liste d'objets BigDecimal. La liste autorise les valeurs nulles.

public BigDecimal bigDecimalAverage(List<BigDecimal> bigDecimalList, RoundingMode roundingMode) {
    // Filter the list removing null values
    List<BigDecimal> bigDecimals = bigDecimalList.stream().filter(Objects::nonNull).collect(Collectors.toList());

    // Special cases
    if (bigDecimals.isEmpty())
        return null;
    if (bigDecimals.size() == 1)
        return bigDecimals.get(0);

    // Return the average of the BigDecimals in the list
    return bigDecimals.stream().reduce(BigDecimal.ZERO, BigDecimal::add).divide(new BigDecimal(bigDecimals.size()), roundingMode);
}
0