How to group and reduce a list of objects with duration

129 Views Asked by At

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));
1

There are 1 best solutions below

11
Nikos Paraskevopoulos On BEST ANSWER

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.