Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 produce 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 that with Streams API?

like image 474
Nick Melis Avatar asked Dec 13 '22 23:12

Nick Melis


2 Answers

In case you don't want to create a new key class, as suggested by assylias, you can do a 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()))
                   );

... followed by a nested print

map.forEach((k,v)-> v.forEach((a,b)-> System.out.println(k + " " +  a + " " + b)));

This prints

2017-05 EVENT3 1
2017-04 EVENT2 2
2017-04 EVENT1 1
2017-03 EVENT1 2

EDIT: I noticed the order of the dates was the opposite of the OP's expected solution. Using the 3-parameter version of groupingBy you can specify a sorted 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
like image 126
Robin Topper Avatar answered Dec 16 '22 12:12

Robin Topper


You could create a "key" class that contains the year/month and the 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.
}

You can then use that key to group your 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
like image 44
assylias Avatar answered Dec 16 '22 12:12

assylias