Java Stream: grouping and counting by multiple fields
I have the following object:
class Event {
private LocalDateTime when;
private String what;
public Event(LocalDateTime when, String what) {
super();
this.when = when;
this.what = what;
}
public LocalDateTime getWhen() {
return when;
}
public void setWhen(LocalDateTime when) {
this.when = when;
}
public String getWhat() {
return what;
}
public void setWhat(String what) {
this.what = what;
}
}
I need to aggregate by year / month (yyyy-mm) and event type and then count. For example, the following list
List<Event> events = Arrays.asList(
new Event(LocalDateTime.parse("2017-03-03T09:01:16.111"), "EVENT1"),
new Event(LocalDateTime.parse("2017-03-03T09:02:11.222"), "EVENT1"),
new Event(LocalDateTime.parse("2017-04-03T09:04:11.333"), "EVENT1"),
new Event(LocalDateTime.parse("2017-04-03T09:04:11.333"), "EVENT2"),
new Event(LocalDateTime.parse("2017-04-03T09:06:16.444"), "EVENT2"),
new Event(LocalDateTime.parse("2017-05-03T09:01:26.555"), "EVENT3")
);
should have the following result:
Year/Month Type Count 2017-03 EVENT1 2 2017-04 EVENT1 1 2017-04 EVENT2 2 2017-04 EVENT3 1
Any idea if (and if so, how) I can achieve this using the Streams API?
source to share
If you don't want to define your own key, you can groupBy
double. The result is the same, but in a slightly different format:
System.out.println(events.stream()
.collect(Collectors.groupingBy(e -> YearMonth.from(e.getWhen()),
Collectors.groupingBy(Event::getWhat, Collectors.counting()))));
And the result:
{2017-05={EVENT3=1}, 2017-04={EVENT2=2, EVENT1=1}, 2017-03={EVENT1=2}}
source to share
If you don't want to create a new key class like assylias suggested, you can do double groupingBy
Map<YearMonth,Map<String,Long>> map =
events.stream()
.collect(Collectors.groupingBy(e -> YearMonth.from(e.getWhen()),
Collectors.groupingBy(x -> x.getWhat(), Collectors.counting()))
);
... and then nested print
map.forEach((k,v)-> v.forEach((a,b)-> System.out.println(k + " " + a + " " + b)));
Will print
2017-05 EVENT3 1 2017-04 EVENT2 2 2017-04 EVENT1 1 2017-03 EVENT1 2
EDIT: I noticed that the order of the dates was the opposite of the OP's expected solution. Using the 3 parameter version groupingBy
, you can specify an ordered map implementation
Map<YearMonth,Map<String,Long>> map =
events.stream()
.collect(Collectors.groupingBy(e -> YearMonth.from(e.getWhen()), TreeMap::new,
Collectors.groupingBy(x -> x.getWhat(), Collectors.counting()))
);
The same map.forEach(...)
now prints
2017-03 EVENT1 2 2017-04 EVENT2 2 2017-04 EVENT1 1 2017-05 EVENT3 1
source to share
You can create a "key" class that contains the year / month and event type:
class Group {
private YearMonth ym;
private String type;
public Group(Event e) {
this.ym = YearMonth.from(e.getWhen());
this.type = e.getWhat();
}
//equals, hashCode, toString etc.
}
Then you can use this key to group events:
Map<Group, Long> result = events.stream()
.collect(Collectors.groupingBy(Group::new, Collectors.counting()));
result.forEach((k, v) -> System.out.println(k + "\t" + v));
which outputs:
2017-04 EVENT1 1 2017-03 EVENT1 2 2017-04 EVENT2 2 2017-05 EVENT3 1
source to share
final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM");
Stream.of(
new Event(LocalDateTime.parse("2017-03-03T09:01:16.111"), "EVENT1"),
new Event(LocalDateTime.parse("2017-03-03T09:02:11.222"), "EVENT1"),
new Event(LocalDateTime.parse("2017-04-03T09:04:11.333"), "EVENT1"),
new Event(LocalDateTime.parse("2017-04-03T09:04:11.333"), "EVENT2"),
new Event(LocalDateTime.parse("2017-04-03T09:06:16.444"), "EVENT2"),
new Event(LocalDateTime.parse("2017-05-03T09:01:26.555"), "EVENT3")
).collect(Collectors.groupingBy(event ->
dateTimeFormatter.format(event.getWhen()),
Collectors.groupingBy(Event::getWhat, counting())))
.forEach((whenDate,v) -> v.forEach((whatKey,counter) ->
System.out.println(whenDate+ " "+ whatKey+" "+counter)));
You don't need to use the Arrays.asList () method to navigate to the stream. Use Stream.of () method directly to get a stream.
Output
2017-03 EVENT1 2 2017-04 EVENT2 2 2017-04 EVENT1 1 2017-05 EVENT3 1
source to share
We can create a method in POJO that contains a list of fields to use for grouping, like below
public String getWhenAndWhat() {
return YearMonth.from(when) + ":" + what; //you can use delimiters like ':','-',','
}
And the stream code,
System.out.println(events.stream()
.collect(Collectors.groupingBy(Event::getWhenAndWhat, Collectors.counting())));
Output:
{2017-05: EVENT3 = 1, 2017-04: EVENT1 = 1, 2017-04: EVENT2 = 2, 2017-03: EVENT1 = 2}
source to share