6

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?

3
  • you say that you want to aggregate by month, but the result also contains the year. So is it by month and year; or aggregate by month but show the year also? Commented Apr 25, 2017 at 13:53
  • I meant aggregate by year/month (yyyy-mm). I've edited the post :) Commented Apr 25, 2017 at 14:47
  • then any of the answers here qualify :) Commented Apr 25, 2017 at 14:49

5 Answers 5

8

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
Sign up to request clarification or add additional context in comments.

Comments

3

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

1 Comment

I like this solution, it seems the most OO to me. I would add a comment regarding ordering, i.e. implement compareTo in Group and using a TreeMap, or a TreeMap with a custom comparator.
3

If you do not want to define your own key, you could groupBy twice. The result is the same, but in 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 is:

 {2017-05={EVENT3=1}, 2017-04={EVENT2=2, EVENT1=1}, 2017-03={EVENT1=2}}

2 Comments

Grouping by getMonth will not take the year into account, the other answers use YearMonth for that purpose.
@MalteHartwig Yes I know, but the OP said: I need to aggregate by month and event type. It could easily be changed to year also...
0
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)));

No need to use an Arrays.asList() method to get to a 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

2 Comments

The OP gave a List in his example. Why wouldn't you want to use it? Besides that your answer is the same as mine.
Why would you use the Arrays class' static method to create a list and then transform that list to a stream when you can get a stream directly? I didn't look at your answer.
0

We can create method in POJO has that contains list of fields to use for grouping by, 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 would be:

{2017-05:EVENT3=1, 2017-04:EVENT1=1, 2017-04:EVENT2=2, 2017-03:EVENT1=2}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.