[英]Averaging BigDecimals using Streams API Collectors
当前基于双重奖品的方法。
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>>
,
以下代码段仅可用于计算属于指定类别的产品的总奖金。 不确定如何使用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))));
看一下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>
。
我简化了一个示例,并删除了getters / setters和all-args构造函数。 除了使用嵌套类ProductPriceSummary
,您还可以将任何可变的holder类用于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();
}
}
为了理解目的,我将操作分为两个步骤。 如果您愿意,可以将两个步骤结合在一起。
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])));
说明:
a
的(a,b)
的值在所述第一元件和第二元件的部分计数的部分和。 b
元素的第一个元素包含要添加到总和的每个BigDecimal值。 不使用b
的第二个元素。 Map<String, BigDecimal[]> stringMap
每个条目完成的 这基于[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)
)
));
}
这有一些缺点:
BigDecimal
,在此情况下,使用int
或long
会更有意义。 这些问题可以通过将收集器提取到自定义类中来解决(就像Andrew的回答一样)。
您可以这样创建自己的收集器:
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)));
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.