I have a data in the following format:
ProductName | Date
------------|------
ABC | 1-May
ABC | 1-May
XYZ | 1-May
ABC | 2-May
It is in the form of List, where Product consists of ProductName and Date. Now I wanted to group these data and get the count with sum as follows:
1-May
-> ABC : 2
-> XYZ : 1
-> Total : 3
2-May
-> ABC: 1
-> Total : 1
So far what I've achieved is grouping with counting, but not the total value.
myProductList.stream()
.collect(Collectors.groupingBy(Product::getDate,
Collectors.groupingBy(Product::getProductName, Collectors.counting())));
Not sure how do I get the total value.
You could use Collectors.collectingAndThen
to add the entry with the total to every inner map:
Map<LocalDate, Map<String, Long>> result = myProductList.stream()
.collect(Collectors.groupingBy(
Product::getDate,
TreeMap::new, // orders entries by key, i.e. by date
Collectors.collectingAndThen(
Collectors.groupingBy(
Product::getProductName,
LinkedHashMap::new, // LinkedHashMap is mutable and
Collectors.counting()), // preserves insertion order, i.e.
map -> { // we can insert the total later
map.put("Total", map.values().stream().mapToLong(c -> c).sum());
return map;
})));
The result
map contains:
{2017-05-01={ABC=2, XYZ=1, Total=3}, 2017-05-02={ABC=1, Total=1}}
I've specified suppliers for both the outer map and the inner maps. The outer map is a TreeMap
, which orders its entries by key (in this case by date). For the inner maps I've decided to go for LinkedHashMap
, which is mutable and preserves insertion order, i.e. we will be able to insert the total later, once the inner maps have been filled with data.
So far so good. However, I think we can do it better, since, once each inner map is filled with data, we need to traverse all its values to calculate the total. (This is what map.values().stream().mapToLong(c -> c).sum()
actually does). By doing this, we are not taking advantage of the fact that, when counting, every element of the stream adds 1
not only to the group it belongs, but also to the total. Luckily, we can solve this with a custom collector:
public static <T, K> Collector<T, ?, Map<K, Long>> groupsWithTotal(
Function<? super T, ? extends K> classifier,
K totalKeyName) {
class Acc {
Map<K, Long> map = new LinkedHashMap<>();
long total = 0L;
void accumulate(T elem) {
this.map.merge(classifier.apply(elem), 1L, Long::sum);
this.total++;
}
Acc combine(Acc another) {
another.map.forEach((k, v) -> {
this.map.merge(k, v, Long::sum);
this.total += v;
});
return this;
}
Map<K, Long> finish() {
this.map.put(totalKeyName, total);
return this.map;
}
}
return Collector.of(Acc::new, Acc::accumulate, Acc::combine, Acc::finish);
}
This collector not only counts elements for each group (like Collectors.groupingBy(Product::getProductName, Collectors.counting())
does), but also adds to the total when accumulating and combining. When finishing, it also adds an entry with the total.
To use it, simply invoke the groupsWithTotal
helper method:
Map<LocalDate, Map<String, Long>> result = myProductList.stream()
.collect(Collectors.groupingBy(
Product::getDate,
TreeMap::new,
groupsWithTotal(Product::getProductName, "Total")));
The output is the same:
{2017-05-01={ABC=2, XYZ=1, Total=3}, 2017-05-02={ABC=1, Total=1}}
As a bonus, given LinkedHashMap
supports null
keys, this custom collector can also group by a null
key, i.e. in the rare case that a Product
has a null
productName
, it will create an entry with the null
key instead of throwing a NullPointerException
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With