Reputation: 7107
I have a list of Orders I should group by two criteria.
Order_Id| Customer | Date | Amount |
1 | "Sam" | 2019-03-21 | 100 |
2 | "Nick" | 2019-03-21 | 102 |
3 | "Dan" | 2019-03-21 | 300 |
4 | "Sam" | 2019-04-21 | 400 |
5 | "Jenny" | 2019-04-21 | 220 |
6 | "Jenny" | 2019-04-12 | 330 |
Top buyer for each month by summed amount should be found, for current example:
{
MARCH: { customer='Dan', amount=300 },
APRIL: { customer='Jenny', amount=550 }
}
There is a solution I was able to find:
public class Main {
public static void main(String[] args) {
List<Order> orders = List.of(
new Order(1L, "Sam", LocalDate.of(2019, 3, 21), 100L),
new Order(2L, "Nick", LocalDate.of(2019, 3, 21), 102L),
new Order(3L, "Dan", LocalDate.of(2019, 3, 21), 300L),
new Order(4L, "Sam", LocalDate.of(2019, 4, 21), 400L),
new Order(5L, "Jenny", LocalDate.of(2019, 4, 21), 220L),
new Order(6L, "Jenny", LocalDate.of(2019, 4, 12), 330L)
);
solution1(orders);
}
private static void solution1(List<Order> orders) {
final Map<Month, Map<String, Long>> buyersSummed = new HashMap<>();
for (Order order : orders) {
Map<String, Long> customerAmountMap = buyersSummed.computeIfAbsent(order.getOrderMonth(), mapping -> new HashMap<>());
customerAmountMap.putIfAbsent(order.getCustomer(), 0L);
Long customerAmount = customerAmountMap.get(order.getCustomer());
customerAmountMap.put(order.getCustomer(), customerAmount + order.getAmount());
}
final Map<Month, BuyerDetails> topBuyers = buyersSummed.entrySet().stream()
.collect(
toMap(Entry::getKey, customerAmountEntry -> customerAmountEntry.getValue().entrySet().stream()
.map(entry -> new BuyerDetails(entry.getKey(), entry.getValue()))
.max(Comparator.comparingLong(BuyerDetails::getAmount)).orElseThrow())
);
System.out.println(topBuyers);
}
}
The data model I used:
class BuyerDetails {
String customer;
Long amount;
public BuyerDetails(String customer, Long amount) {
this.customer = customer;
this.amount = amount;
}
public String getCustomer() {
return customer;
}
public Long getAmount() {
return amount;
}
}
class Order {
Long id;
String customer;
LocalDate orderDate;
Long amount;
public Order(Long id, String customer, LocalDate orderDate, Long amount) {
this.id = id;
this.customer = customer;
this.orderDate = orderDate;
this.amount = amount;
}
public Long getId() {
return id;
}
public String getCustomer() {
return customer;
}
public LocalDate getOrderDate() {
return orderDate;
}
public Month getOrderMonth() {
return getOrderDate().getMonth();
}
public Long getAmount() {
return amount;
}
}
The question:
Is there any way to solve the task above in one stream?
Upvotes: 8
Views: 1252
Reputation: 11739
It has a nested stream, so it is not one stream and it returns Map<String, Optional<BuyerDetails>>
.
orders.stream()
.collect(
Collectors.groupingBy(Order::getOrderMonth,
Collectors.collectingAndThen(
Collectors.groupingBy(
Order::getCustomer,
Collectors.summarizingLong(Order::getAmount)
),
e -> e.entrySet()
.stream()
.map(entry -> new BuyerDetails(entry.getKey(), entry.getValue().getSum()))
.max(Comparator.comparingLong(BuyerDetails::getAmount))
)
)
)
so there 3 steps:
Collectors.groupingBy(Order::getOrderMonth,
Collectors.groupingBy(Order::getCustomer, Collectors.summarizingLong( Order::getAmount))
max(Comparator.comparingLong(BuyerDetails::getAmount))
output is
{
APRIL = Optional [ BuyerDetails { customer = 'Jenny', amount = 550 } ],
MARCH = Optional [ BuyerDetails { customer = 'Dan', amount = 300 } ]
}
I am curious as well if this can be done without additional stream.
Upvotes: 2
Reputation: 20741
Try using groupingBy
, summingLong
and comparingLong
like as shown below
Map<Month, BuyerDetails> topBuyers = orders.stream()
.collect(Collectors.groupingBy(Order::getOrderMonth,
Collectors.groupingBy(Order::getCustomer,
Collectors.summingLong(Order::getAmount))))
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
order -> order.getValue().entrySet().stream()
.max(Comparator.comparingLong(Map.Entry::getValue))
.map(cust -> new BuyerDetails(cust.getKey(), cust.getValue())).get()));
Output
{
"MARCH": { "customer": "Dan", "amount": 300 },
"APRIL": { "customer": "Jenny", "amount": 550 }
}
Upvotes: 4
Reputation: 2641
Alright, here we go! The following code will get you what you want, with exactly 1 call to stream()
:
Map<Month, BuyerDetails> grouped = orders.stream().collect(
Collectors.groupingBy(Order::getOrderMonth,
Collectors.collectingAndThen(
Collectors.groupingBy(Order::getCustomer,
Collectors.summingLong(Order::getAmount)
),
((Function<Map<String,Long>, Map.Entry<String,Long>>)
map -> Collections.max(
map.entrySet(), Comparator.comparingLong(Map.Entry::getValue)
)
).andThen(
e -> new BuyerDetails(e.getKey(),e.getValue())
)
)
)
);
System.out.println(grouped);
Output:
{MARCH={ customer='Dan', amount=300 }, APRIL={ customer='Jenny', amount=550 }}
Now, this is a bit of a doozy, so let's go through it line by line to see what's happening:
Map<Month, BuyerDetails> grouped = orders.stream().collect(
First, we stream the orders we have,
Collectors.groupingBy(Order::getOrderMonth,
grouping by the month, we find:
Collectors.collectingAndThen(
Collectors.groupingBy(Order::getCustomer,
each Customer and
Collectors.summingLong(Order::getAmount)
),
their total orders within the month.
((Function<Map<String,Long>, Map.Entry<String,Long>>)
(We cast to Function, so we can use methods like andThen
on the lambda function we define)
map -> Collections.max(
map.entrySet(), Comparator.comparingLong(Map.Entry::getValue)
)
For each month, we find the Customer with maximal order sum.
).andThen(
Then, we
e -> new BuyerDetails(e.getKey(),e.getValue())
create a new buyer detail for said customer
)
)
)
);
and collect all Month/BuyerDetail pairs.
System.out.println(grouped);
Finally, we print the created data structure.
Upvotes: 2
Reputation: 7521
This can't be done with single stream since both sum
and max
are terminal operations and they can't be applied to same stream.
Better split this into two operations
Map<Month, Map<String, Long>> sumsByMonth = orders.stream().collect(
Collectors.groupingBy(
Order::getOrderMonth,
Collectors.groupingBy(
Order::getCustomer,
Collectors.mapping(
Order::getAmount,
Collectors.reducing(0L, a -> a, (a1, a2) -> a1 + a2)
)
)
)
);
Map<Month, BuyerDetails> topBuyers = sumsByMonth.entrySet().stream().collect(
Collectors.toMap(
Map.Entry::getKey,
sums -> sums.getValue().entrySet().stream()
.max(Comparator.comparingLong(Map.Entry::getValue))
.map(e -> new BuyerDetails(e.getKey(), e.getValue()))
.get()
)
);
Upvotes: 0
Reputation: 180103
Is there any way to solve the task above in one stream?
It depends on what you mean by "in one stream". You want to perform a reduction operation that is probably best characterized as a composite of a sequence of reductions:
From the perspective of the Stream API, performing any one of those individual reductions on a stream is a terminal operation on that stream. You can process the result with a new stream, even chaining that together syntactically, but although that might take the syntactic form of a single chain of method invocations, it would not constitute all operations happening on a single stream.
You could also create a single Collector
(or the components of one) so that you get the result directly by collecting the stream of your input elements, but internally, that collector would still need to perform the individual reductions, either by internally creating and consuming additional streams, or by performing the same tasks via non-stream APIs. If you count those internal operations then again, no, it would not constitute performing operations on a single stream. (But if you don't consider those internal reductions, then yes, this does it all on one stream.)
Upvotes: 4
Reputation: 115
My approach (3 streams):
private static void solution1(List<Order> orders) {
final Map<Month, BuyerDetails> topBuyers = orders.stream().collect(
Collectors.groupingBy(order -> order.getCustomer() + "$" + order.getOrderDate().getYear() + "." +
order.getOrderMonth(),
Collectors.reducing((ord1, ord2) -> {
ord1.setAmount(ord1.getAmount() + ord2.getAmount());
return ord1;
}))).values().stream()
.collect(Collectors.groupingBy(order -> order.get().getOrderMonth(),
maxBy(Comparator.comparing(order -> order.get().getAmount())))).values().stream()
.collect(
toMap((key) -> key.get().get().getOrderMonth(),
key -> new BuyerDetails(key.get().get().getCustomer(), key.get().get().getAmount())
)
);
}
Upvotes: 1