Search code examples
javajava-streamjava-timedurationlocaldate

How to group and reduce a list of objects with duration


I want to create a list of persons sorted by LocalDate and String as well as aggregated by duration.

I did some research regarding the use of duration in streams:

and on using streams with grouping and reducing a list of objects into subgroups:

However, it's impossible for me to solve my problem, class Person looks like this:

class Person { 
    private LocalDate startDate; 
    private String personType;
    private Duration time1;
    private Duration time2;//constructor, getter+setter, other methods
}

An example of the created list looks like this:

List<Person> personList = Arrays.asList(
          new Person("2023-02-02","member1","08:00","4:00"),
        new Person("2023-02-02","member1","50:00","0:45"),  
        new Person("2023-02-02","member2","10:00","0:40"),
        new Person("2023-02-02","member2","01:00","1:20"),
        new Person("2023-02-03","member1","08:00","2:00"),
        new Person("2023-02-03","member1","10:00","0:45"),  
        new Person("2023-02-03","member2","10:00","1:40"),
        new Person("2023-02-03","member2","02:00","1:20"),//... 
 );

I want to create a list of persons sorted by startdate and personType, with the sum of duration.

Desired output:

("2023-02-02","member1","58:00","4:45"),
("2023-02-02","member2","11:00","2:00"),
("2023-02-03","member1","18:00","2:45"),
("2023-02-03","member2","12:00","3:00"),
...

My approach is to use something like this. However, I can't map duration and string values:

Map<LocalDate,List<Person>> result=personList.stream()
.collect(Collectors.groupingBy(Person::getstartDate))
        .entrySet().stream()
        .collect(Collectors.toMap(x -> {
            //how to sum duration values in here?
           // Duration duration1 = x.getValue().stream() ...;
            //how to use String values in here?
           // String string = x.getValue().stream()....
            return new Person(x.getKey(), string, duration1,duration2);
        }, Map.Entry::getValue));

Solution

  • One way to do it is with a cascade of groupings and a reduction to sum the durations. The following results in a 2-level map that contains the sum per date and type:

    Map<LocalDate, Map<String, Optional<Person>>> perDateAndTypeAggregated = 
        personList.stream().collect(Collectors.groupingBy(
            Person::getStartDate,
            Collectors.groupingBy(
                    Person::getType,
                    Collectors.reducing((p1, p2) ->
                        // this is where we sum the durations
                        new Person(p1.getStartDate(), p1.getType(), p1.getTime1().plus(p2.getTime1()), p1.getTime2().plus(p2.getTime2()))
                    )
            )
        ));
    

    If you want a list (the map gives richer information, but it's your application), you can flatten and sort the previous map as follows:

    Comparator<Person> byDateThenType =
        Comparator.comparing(Person::getStartDate).thenComparing(Person::getType);
    
    List<Person> result =
        perDateAndTypeAggregated.values().stream()
            .flatMap(m -> m.values().stream())
            .filter(Optional::isPresent)
            .map(Optional::get)
            .sorted(byDateThenType)
            .toList();
    

    EDIT: As per the comments from Holger, the above can be simplified using toMap instead of the groupingBy/reducing combination, with the added advantage of simplifying the second operation (toList) by removing the .filter().map() combo:

    Map<LocalDate, Map<String, Person>> perDateAndTypeAggregated2 = 
        personList.stream().collect(Collectors.groupingBy(
            Person::getStartDate,
            Collectors.toMap(
                    Person::getType,
                    Function.identity(),
                    (p1, p2) ->
                        // this is where we sum the durations
                        new Person(p1.getStartDate(), p1.getType(), p1.getTime1().plus(p2.getTime1()), p1.getTime2().plus(p2.getTime2()))
            )
        ));
    
    List<Person> result2 =
        perDateAndTypeAggregated2.values().stream()
            .flatMap(m -> m.values().stream())
            .sorted(byDateThenType)
            .toList();
    

    EDIT 2: I used the following Person class:

    public static class Person {
        private LocalDate startDate;
        private String type;
        private Duration time1;
        private Duration time2;
    
        public Person() {}
    
        public Person(String startDate, String type, String time1, String time2) {
            this.startDate = LocalDate.parse(startDate);
            this.type = type;
            this.time1 = Duration.parse(time1);
            this.time2 = Duration.parse(time2);
        }
    
        public Person(LocalDate startDate, String type, Duration time1, Duration time2) {
            this.startDate = startDate;
            this.type = type;
            this.time1 = time1;
            this.time2 = time2;
        }
    
        // getters and setters...
    
        @Override
        public String toString() {
            return "Person(" + startDate + "," + type + "," + time1 + "," + time2 + ")";
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Person person = (Person) o;
            return Objects.equals(startDate, person.startDate) && Objects.equals(type, person.type) && Objects.equals(time1, person.time1) && Objects.equals(time2, person.time2);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(startDate, type, time1, time2);
        }
    }
    

    And initialize the list as:

    List<Person> personList = Arrays.asList(
            new Person("2023-02-02","member1","PT8H00M","PT4H00M"),
            new Person("2023-02-02","member1","PT50H00M","PT0H45M"),
            new Person("2023-02-02","member2","PT10H00M","PT0H40M"),
            new Person("2023-02-02","member2","PT1H00M","PT1H20M"),
            new Person("2023-02-03","member1","PT8H00M","PT2H00M"),
            new Person("2023-02-03","member1","PT10H00M","PT0H45M"),
            new Person("2023-02-03","member2","PT10H00M","PT1H40M"),
            new Person("2023-02-03","member2","PT2H00M","PT1H20M")
    );
    

    All this code is giving me the desired outcome from the question.