4

Given a list of TimeEntry objects I would like to group them into Day and Hour objects.

Where Day have list of Hour objects and the Hour have a list of TimeEntry objects for the hour it represents.

Day and Hour also keep a sum of the TimeEntry durations that's in their respective lists. If possible also get the day name into Day. The list of TimeEntry is sorted on "start".

public class TimeEntry {
  private LocalDateTime start;
  private Duration duration;

  public Integer getDay() { return this.start.get(ChronoField.DAY_OF_MONTH); }
  public Integer getHour() { return this.start.get(ChronoField.HOUR_OF_DAY); }
  public String getDayName() {
        return this.start.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.getDefault());
    }
}

public class Day {
  private Integer dayNr;
  private String dayName;
  private Duration sumOfHours;
  private List<Hour> hours;
}

public class Hour {
  private Integer hourNr;
  private Duration sumOfEntries;
  private List<TimeEntry> entries;
}

How do you do this with Java 8s' streams?

List<Day> days = timeEntries.stream().collect(Collectors.groupingBy(...? 

Sample input:

List<TimeEntry> timeEntries = [{"2016-01-22 10:00", "5 minutes"}, 
{"2016-01-23 08:00", "7 minutes"} , {"2016-01-23 08:43", "3 minutes"}]

Sample output:

[
{dayNr: 22, dayName: Friday, sumofEntries: 5 minutes
  [{hourNr: 10, sumofEntries: 5 minutes}, 
    [{"2016-01-22 10:00", "5 minutes"}]} ]},
{dayNr: 23, dayName: Saturday, sumofEntries: 10 minutes
  [{hourNr: 8, sumofEntries: 10 minutes}, 
    [{"2016-01-23 08:00", "7 minutes"},
     {"2016-01-23 08:43", "3 minutes"} ]}
]
Alexis C.
  • 91,686
  • 21
  • 171
  • 177
NA.
  • 6,451
  • 9
  • 36
  • 36
  • You won't get a `List` unless you write your _own_ collector. The best you can do with the core libraries is a `Map>>` by doing a `groupingBy` on days and then, for each day, a `groupingBy` on hours. – Boris the Spider Jan 24 '16 at 12:36

2 Answers2

2

This is a quite complicated operation. The direct way would be to first create a Map<Integer, Map<Integer, List<TimeEntry>>> by grouping over the days, then over the hours. Once we have that map, we can postprocess it to create the wanted List<Day>.

Creating the temporary map is easy enough. We can use Collectors.groupingBy(classifier, downstream) where the classifier returns the day (through the method reference TimeEntry::getDay) and the downstream collector is another groupingBy collector that classifies over the hours (through the method reference TimeEntry::getHour). After this step, we have a map over each day where the value is a map over each hour mapping to the corresponding time entry.

Next, what we need to do is make a List<Day> out of that map. Basically, each entry of the map (so each day number) must be mapped to the corresponding Day object.

  • The dayNr is simply the key of the entry.
  • The dayName is the day name of one of the time entries for that day and an hour. One time entry must exist since we grouped by those fields earlier: to retrieve it, we get the hour map Map<Integer, List<TimeEntry>> for that day number and just keep the first value.
  • The hours can be retrieved by manipulating the hour map Map<Integer, List<TimeEntry>> for that day number. For each entry of that map:
    • The hourNr is simply the key of the entry
    • The entries field is the value of the entry
    • The sumOfEntries is the addition of all the duration of the time entries for that hour number.
  • Finally, the sumOfHours is the addition of all the sumOfEntries for each hour number for that day number.

A complete implementation is the following, where it is assumed that all of your domain objects have an appropriate constructor and the appropriate getters:

public static void main(String[] args) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
    List<TimeEntry> timeEntries = Arrays.asList(
        new TimeEntry(LocalDateTime.parse("2016-01-22 10:00", formatter), Duration.ofMinutes(5)),
        new TimeEntry(LocalDateTime.parse("2016-01-23 08:00", formatter), Duration.ofMinutes(7)),
        new TimeEntry(LocalDateTime.parse("2016-01-23 08:43", formatter), Duration.ofMinutes(3))
    );

    Map<Integer, Map<Integer, List<TimeEntry>>> map = 
        timeEntries.stream()
                   .collect(groupingBy(TimeEntry::getDay, groupingBy(TimeEntry::getHour)));

    List<Day> output =
        map.entrySet()
           .stream()
           .map(e -> {
              String dayName = e.getValue().values().iterator().next().get(0).getDayName();
              List<Hour> hours =
                 e.getValue().entrySet()
                             .stream()
                             .map(he -> new Hour(he.getKey(), sumDuration(he.getValue(), TimeEntry::getDuration), he.getValue()))
                             .collect(toList());
              return new Day(e.getKey(), dayName, sumDuration(hours, Hour::getSumOfEntries), hours);
           })
           .collect(toList());

    System.out.println(output);
}

private static <T> Duration sumDuration(List<T> list, Function<T, Duration> function) {
    return list.stream().map(function::apply).reduce(Duration.ofMinutes(0), (d1, d2) -> d1.plus(d2));
}

Note that the addition of Duration objects from a list was factored into a helper method sumDuration.

Output of the sample above (assuming a toString() printing all the fields enclosed in square brackets):

[Day [dayNr=22, dayName=vendredi, sumOfHours=PT5M, hours=[Hour [hourNr=10, sumOfEntries=PT5M, entries=[TimeEntry [start=2016-01-22T10:00, duration=PT5M]]]]], Day [dayNr=23, dayName=samedi, sumOfHours=PT10M, hours=[Hour [hourNr=8, sumOfEntries=PT10M, entries=[TimeEntry [start=2016-01-23T08:00, duration=PT7M], TimeEntry [start=2016-01-23T08:43, duration=PT3M]]]]]]
Tunaki
  • 132,869
  • 46
  • 340
  • 423
0

Well, I can't write all the code for you, but this would implement Boris The Spider's idea and hopefully get you started. After instantiating your TimeEntry list like so (See how to initialize static ArrayList in one line):

List<TimeEntry> timeEntries = new ArrayList<TimeEntry>() {
    {
        add(new TimeEntry("2016-01-22T10:00", "PT5M"));
        add(new TimeEntry("2016-01-23T08:00", "PT7M"));
        add(new TimeEntry("2016-01-23T08:43", "PT3M"));
    }
};

then you stream it. You will need two groupingBy collectors. The first will group by hours:

Collector<TimeEntry, ?, Map<Integer, List<TimeEntry>>> groupingByHours
    = Collectors.groupingBy(TimeEntry::getHour);

and the second will group by days.

Collector<TimeEntry, ?, Map<Integer, Map<Integer, List<TimeEntry>>>> groupingByDaysByHour
    = Collectors.groupingBy(TimeEntry::getDay, groupingByHours);

You can then get your TimeEntry list grouped by days then hours like so:

Map<Integer, Map<Integer, List<TimeEntry>>> dayMap = 
    timeEntries.stream()
    .collect( groupingByDaysByHour);

This will give the output of:

{22={10=[2016-01-22T10:00 PT5M]}, 23={8=[2016-01-23T08:00 PT7M, 2016-01-23T08:43 PT3M]}}

For completeness sake, your original TimeEntry class modified to work with this example:

class TimeEntry {
    private LocalDateTime start;
    private Duration duration;

    public TimeEntry(String start, String duration) {
        this.start = LocalDateTime.parse(start, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        this.duration = Duration.parse(duration);
    }

    public int getDay() {
        return start.get(ChronoField.DAY_OF_MONTH);
    }
    public int getHour() {
        return start.get(ChronoField.HOUR_OF_DAY);
    }
    public long getDuration() {
        return duration.toMinutes();
    }
    @Override
    public String toString() {
        return start.toString() + " " + duration.toString();
    }
}
Community
  • 1
  • 1
K.Nicholas
  • 10,956
  • 4
  • 46
  • 66