如何按持续时间对对象列表进行分组和缩减

How to group and reduce a list of objects with duration

提问人:Mc_Android 提问时间:9/20/2023 最后编辑:Mark RotteveelMc_Android 更新时间:9/21/2023 访问量:119

问:

我想创建一个按持续时间排序和汇总的人员列表。LocalDateString

我做了一些关于在流中使用持续时间的研究:

以及使用流对对象列表进行分组并将对象列表简化为子组:

但是,我不可能解决我的问题,类是这样的:Person

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

创建的列表示例如下所示:

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"),//... 
 );

我想创建一个按 startdate 和 personType 排序的人员列表,以及持续时间的总和。

期望输出:

("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"),
...

我的方法是使用这样的东西。但是,我无法映射持续时间和字符串值:

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));
java java-stream java-time duration localdate

评论

3赞 Sweeper 9/20/2023
您显示了一个人员列表作为您的预期结果,但您正在收集一个?你到底想要哪一个?Map
0赞 deHaar 9/20/2023
是否希望每个成员在每个日期的所有持续时间的总和?你为什么要实例化一个 with ?Personnew Item(…)
0赞 Mc_Android 9/20/2023
因此,我想要一份人员名单。据我所知,我需要一张地图。如果地图中有所有结果,我可以再次创建一个列表。但是,如果可能没有地图,那就更好了。
1赞 Holger 9/20/2023
收集器的第一个参数是,因此它不是尝试对值求和的正确位置。第二个函数确定值,但仅确定单个元素的单个值,因此它也不是正确的位置。要聚合多个值,必须指定第三个参数。指定为第三个参数的函数将接收两个值,并允许您插入求和。toMap
1赞 Basil Bourque 9/21/2023
顺便一提。。。工期文本的标准格式在 ISO 8601 中定义:。例如:vs .PnYnMnDTnHnMnSPT8H08:00

答:

1赞 Nikos Paraskevopoulos 9/20/2023 #1

一种方法是使用级联分组和减少以求和持续时间。以下结果将生成一个 2 级映射,其中包含每个日期和类型的总和:

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

如果您想要一个列表(地图提供了更丰富的信息,但这是您的应用程序),您可以按如下方式对上一个地图进行展平和排序:

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

编辑:根据 Holger 的评论,可以使用 / 组合来简化上述操作,并通过删除组合来简化第二个操作 ():toMapgroupingByreducingtoList.filter().map()

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

编辑2:我使用了以下类:Person

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

并将列表初始化为:

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

所有这些代码都为我提供了问题所需的结果。

评论

1赞 Holger 9/20/2023
当您使用而不是 + 时,您不需要这些步骤。toMapgroupingByreducing.filter(Optional::isPresent) .map(Optional::get)
0赞 Mc_Android 9/20/2023
谢谢你的建议。看来我必须花更多的时间来了解java stream.api的可能性
0赞 Nikos Paraskevopoulos 9/20/2023
@Holger 过滤器/映射步骤很丑陋,但这是使用简单形式的 的结果。此 IMO 使分组/减少步骤变得不那么复杂,但代价是更丑陋的排序/到列表步骤。如果我使用过滤器/映射将不再需要,但代价是分组/减少操作中的代码更复杂。我不确定会有什么帮助,但我对一个例子很感兴趣。reducing​(BinaryOperator)reducing​(T identity, BinaryOperator<T> op)toMap
1赞 Holger 9/20/2023
与其简单地使用合并功能,不如使用与减少功能相同的合并功能。然后,结果将是 而不是 .另请参阅替换 groupingBy 和 reduce by toMapCollectors.groupingBy(Person::getType, Collectors.reducing((p1, p2) -> …))Collectors.toMap(Person::getType, Function.identity(), (p1, p2) -> …)Map<String, Person>Map<String, Optional<Person>>
0赞 Nikos Paraskevopoulos 9/21/2023
嘿@Holger是的,好主意,谢谢!我以为你的意思是替换第一个——这让我感到困惑;正如您在评论中明确指出的那样,替换第二个是完全有道理的。我会把它添加到答案中!groupingBy