Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to group values from a list with Java Stream API (groupingBy collector)?

I have list of Entry objects. Entry is a:

class Entry {
   private final Date date;
   private final String value;

   // constructor
   // getters
}

I need to group these entries by day. For example,

2011-03-21 09:00 VALUE1
2011-03-21 09:00 VALUE2
2011-03-22 14:00 VALUE3
2011-03-22 16:00 VALUE4
2011-03-21 16:00 VALUE5

Should be grouped:

2011-03-21
    VALUE1
    VALUE2
    VALUE5

2011-03-22
    VALUE3
    VALUE4

I want to get a Map<Date, List<Entry>>. How can I get this using the Stream API (groupingBy collector)?

My attempt below:

final Map<Date, List<Entry>> entries =
        list.stream().collect(Collectors.groupingBy(request -> {
        final Calendar ogirinal = Calendar.getInstance();
        ogirinal.setTime(request.getDate());

        final Calendar cal = Calendar.getInstance();
        cal.set(Calendar.DAY_OF_MONTH, ogirinal.get(Calendar.DAY_OF_MONTH));
        cal.set(Calendar.MONTH, ogirinal.get(Calendar.MONTH));
        cal.set(Calendar.YEAR, ogirinal.get(Calendar.YEAR));

        return cal.getTime();
    }));

Output:

2011-03-21
    VALUE1
2011-03-21
    VALUE2
2011-03-22
    VALUE3
    VALUE4
2011-03-21
    VALUE5
like image 743
Valeriy Avatar asked Mar 09 '18 14:03

Valeriy


4 Answers

Again don't understand why you are using java.util.Date when you can use LocalDateTime for example. But here goes my attempt:

Map<Date, List<Entry>> entries = list.stream().collect(Collectors.groupingBy(e ->
    // easier way to truncate the date
    Date.from(e.getDate().toInstant().truncatedTo(ChronoUnit.DAYS)))
);

DEMO

like image 53
Juan Carlos Mendoza Avatar answered Oct 18 '22 18:10

Juan Carlos Mendoza


You have to truncate the date values, as they may be different up to the millisecond:

import static java.time.temporal.ChronoUnit.DAYS;

final Map<Instant, List<Entry>> entries =
    list.stream().collect(Collectors.groupingBy(request -> 
        request.getDate().toInstant().truncatedTo(DAYS)));
like image 42
Lars Gendner Avatar answered Oct 18 '22 20:10

Lars Gendner


Your approach is pretty much the right way to go, but your entries end up in different groups because the Calendar objects you return all have slightly different timestamps. This is because you do not set hour/minute/... to 0. Only when two of the calendars have the same time by coincidence (due to timer inaccuracies for example) two entries will end up in the same group.

Use something like this to group instead:

LocalDate.fromDateFields(request.getDate());

LocalDate makes creating date-only timestamps much easier than Calendar. This snippet uses joda time's LocalDate, but Java's own is only slightly longer.

like image 4
Malte Hartwig Avatar answered Oct 18 '22 20:10

Malte Hartwig


If you want to group by date (day, month and year), you can discard the time (hour, minutes, seconds). As you're using Java 8, just convert the java.util.Date to a java.time.LocalDate:

Map<LocalDate, List<Entry>> entries = list.stream()
    .collect(Collectors.groupingBy(request ->
         request.getDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate()));

Just some background: a Date represents a specific point in time, the number of milliseconds since Unix epoch, while a LocalDate represents only a day/month/year date, without any notion of timezone.

To properly convert a Date to a LocalDate, I set it to the JVM default timezone (using toInstant().atZone()) and then get only the local part (toLocalDate()).

You could also make your design simpler, changing the Entry class to have a LocalDate field, if possible. You're using Java 8, and unless you have a good reason to use the old API ("legacy code", "my boss doesn't want", etc), it's better to start using java.time stuff.

like image 3
whadf Avatar answered Oct 18 '22 18:10

whadf