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