使用 Java 将 LocalDateTime 对象与 startTIme 和 endTime 分组

Grouping LocalDateTime objects with startTIme and endTime using Java

我想实现一个分组算法以将此列表分组为分钟间隔。

示例列表:

List<Item> items = Arrays.asList(
    new Item(LocalDateTime.parse("2020-08-21T00:00:00"), LocalDateTime.parse("2020-08-21T00:02:00"), "item1"),
    new Item(LocalDateTime.parse("2020-08-21T00:01:00"), LocalDateTime.parse("2020-08-21T00:03:00"), "item2"),
    new Item(LocalDateTime.parse("2020-08-21T00:03:00"), LocalDateTime.parse("2020-08-21T00:07:00"), "item3"),
    new Item(LocalDateTime.parse("2020-08-21T00:08:00"), LocalDateTime.parse("2020-08-21T00:12:00"), "item4"),
    new Item(LocalDateTime.parse("2020-08-21T09:50:37"), LocalDateTime.parse("2020-08-21T09:56:49"), "item5"),
    new Item(LocalDateTime.parse("2020-08-21T09:59:37"), LocalDateTime.parse("2020-08-21T10:02:37"), "item6"),
    new Item(LocalDateTime.parse("2020-08-21T09:49:37"), LocalDateTime.parse("2020-08-21T09:51:37"), "item7"),
    new Item(LocalDateTime.parse("2019-12-31T23:59:37"), LocalDateTime.parse("2020-01-01T00:03:37"), "item8"),
    new Item(LocalDateTime.parse("2020-01-01T00:04:37"), LocalDateTime.parse("2020-01-01T00:06:37"), "item9")
);

项目class:

class Item {
    LocalDateTime startTime;
    LocalDateTime endTime;
    String name;

    // constructor etc
}

为简单起见,我将仅提及分钟,但日期也很重要。给定 5 分钟的间隔 00:00 - 00:02 可以分为范围 00:00 - 00:05 的组,而 00:03 - 00:07 可以分为两组 00:00 - 00:0500:05 - 00:10

上述示例列表的期望输出(仅用于可读性输出的名称应包含整个 Item 对象):

{
    [item1, item2, item3],
    [item3, item4],
    [item5, item6],
    [item7, item5],
    [item8, item9]
}

是否可以使用类似 Collectors#groupingBy 的方法进行此类分组?

编辑* 为了避免负面评论,我在答案中添加了我的“非高效”解决方案。

主要问题的简短回答:

Is it possible to do such grouping using a method like Collectors#groupingBy?

如评论中所述,此任务的主要问题是在一般情况下无法将单个项目“分组”为单个条目,但需要根据两者将其多路复用为多个条目startTimeendTime.

可能可以使用两个以上的5分钟范围,例如:startTime: 00:02; endTime: 00:12将涵盖三个范围:00:00-00:0500:05-00:1000:10-00:15 --此案例已更新 item4

也就是说,可以提供以下解决方案:

import java.time.*;
import java.util.*;
import java.util.stream.*;

public class Solution {
    public static void main(String args[]) {
        List<Item> items = Arrays.asList(
            new Item(LocalDateTime.parse("2020-08-21T00:00:00"), LocalDateTime.parse("2020-08-21T00:02:00"), "item1"),
            new Item(LocalDateTime.parse("2020-08-21T00:01:00"), LocalDateTime.parse("2020-08-21T00:03:00"), "item2"),
            new Item(LocalDateTime.parse("2020-08-21T00:03:00"), LocalDateTime.parse("2020-08-21T00:07:00"), "item3"),
            new Item(LocalDateTime.parse("2020-08-21T00:04:00"), LocalDateTime.parse("2020-08-21T00:12:00"), "item4"),
            new Item(LocalDateTime.parse("2020-08-21T09:50:37"), LocalDateTime.parse("2020-08-21T09:56:49"), "item5"),
            new Item(LocalDateTime.parse("2020-08-21T09:59:37"), LocalDateTime.parse("2020-08-21T10:02:37"), "item6"),
            new Item(LocalDateTime.parse("2020-08-21T09:49:37"), LocalDateTime.parse("2020-08-21T09:51:37"), "item7"),
            new Item(LocalDateTime.parse("2019-12-31T23:59:37"), LocalDateTime.parse("2020-01-01T00:03:37"), "item8"),
            new Item(LocalDateTime.parse("2020-01-01T00:04:37"), LocalDateTime.parse("2020-01-01T00:06:37"), "item9"),
            // added to test a single entry within 5 min range
            new Item(LocalDateTime.parse("2020-01-01T00:42:37"), LocalDateTime.parse("2020-01-01T00:44:37"), "item10")
        );
        
        items.stream()
             .flatMap(Solution::convert)
             .collect(Collectors.groupingBy(x -> x.getKey(), LinkedHashMap::new, Collectors.mapping(x -> x.getValue(), Collectors.toList())))
             .values()
             .forEach(System.out::println);
    }
    
    public static Stream<Map.Entry<LocalDateTime, Item>> convert(Item item) {
        LocalDateTime start = getKey(item.getStartTime());
        LocalDateTime end = getKey(item.getEndTime()).plusMinutes(5);
        
        return Stream
                .iterate(start, d -> d.isBefore(end), d -> d.plusMinutes(5))
                .map(d -> Map.entry(d, item));
                    
    }

    public static LocalDateTime getKey(LocalDateTime time) {
        return LocalDateTime.of(time.getYear(), time.getMonthValue(), time.getDayOfMonth(), time.getHour(), time.getMinute() - time.getMinute() % 5);
    }
}

输出

[item1, item2, item3, item4]
[item3, item4]
[item4]
[item5, item7]
[item5, item6]
[item6]
[item7]
[item8]
[item8, item9]
[item9]
[item10]

备注

部分Java 9个特征在代码片段中被使用:

更新

Java9个特征可以用下面的Java8个兼容代码代替:

  • Map.entry -> new AbstractMap.SimpleEntry
  • 使用 Java 8 iterate + limit(ChronoUnit.MINUTES.between(start, end) / 5)
public static Stream<Map.Entry<String, Item>> convert(Item item) {
    LocalDateTime start = getKey(item.getStartTime());
    LocalDateTime end = getKey(item.getEndTime()).plusMinutes(5);
        
    return Stream
            .iterate(start, d -> d.plusMinutes(5))
            .limit(ChronoUnit.MINUTES.between(start, end) / 5)
            .map(d -> new AbstractMap.SimpleEntry(d + "**" + d.plusMinutes(5), item));
}

如果筛选出的结果值至少包含两项,则结果如下:

// ...
        .entrySet()
        .stream()
        .filter(x -> x.getValue().size() > 1)
        .forEach(System.out::println);

输出

2020-08-21T00:00**2020-08-21T00:05=[item1, item2, item3, item4]
2020-08-21T00:05**2020-08-21T00:10=[item3, item4]
2020-08-21T09:50**2020-08-21T09:55=[item5, item7]
2020-08-21T09:55**2020-08-21T10:00=[item5, item6]
2020-01-01T00:00**2020-01-01T00:05=[item8, item9]

Online demo

这是我的解决方案:

public static void main(String[] args) {
    List<Item> items = Arrays.asList(
        new Item(LocalDateTime.parse("2020-08-21T00:00:00"), LocalDateTime.parse("2020-08-21T00:02:00"), "item1"),
        new Item(LocalDateTime.parse("2020-08-21T00:01:00"), LocalDateTime.parse("2020-08-21T00:03:00"), "item2"),
        new Item(LocalDateTime.parse("2020-08-21T00:03:00"), LocalDateTime.parse("2020-08-21T00:07:00"), "item3"),
        new Item(LocalDateTime.parse("2020-08-21T00:08:00"), LocalDateTime.parse("2020-08-21T00:12:00"), "item4"),
        new Item(LocalDateTime.parse("2020-08-21T09:50:37"), LocalDateTime.parse("2020-08-21T09:56:49"), "item5"),
        new Item(LocalDateTime.parse("2020-08-21T09:59:37"), LocalDateTime.parse("2020-08-21T10:02:37"), "item6"),
        new Item(LocalDateTime.parse("2020-08-21T09:49:37"), LocalDateTime.parse("2020-08-21T09:51:37"), "item7"),
        new Item(LocalDateTime.parse("2019-12-31T23:59:37"), LocalDateTime.parse("2020-01-01T00:03:37"), "item8"),
        new Item(LocalDateTime.parse("2020-01-01T00:04:37"), LocalDateTime.parse("2020-01-01T00:06:37"), "item9")
    );

    Map<String, List<Item>> groups = new HashMap<>();

    items.stream().forEach(item -> {
        int startTimeMinute = item.startTime.getMinute();

        int startTimeMinutesOver = startTimeMinute % 5;

        int endTimeMinute = item.endTime.getMinute();

        int endTimeMinutesOver = endTimeMinute % 5;

        LocalDateTime firstGroupStartTime = item.startTime.truncatedTo(ChronoUnit.MINUTES).withMinute(startTimeMinute - startTimeMinutesOver);
        LocalDateTime secondGroupStartTime = item.endTime.truncatedTo(ChronoUnit.MINUTES).withMinute(endTimeMinute - endTimeMinutesOver);

        // check if item belongs to a single or more groups
        if (firstGroupStartTime.equals(secondGroupStartTime)) {
            String groupRange = firstGroupStartTime.toString() + "**" + firstGroupStartTime.plusMinutes(5).toString();

            groups.computeIfAbsent(groupRange, s -> new ArrayList<>()).add(item);
        } else {
            String firstGroupRange = firstGroupStartTime.toString() + "**" + firstGroupStartTime.plusMinutes(5).toString();
            groups.computeIfAbsent(firstGroupRange, s -> new ArrayList<>()).add(item);

            String secondGroupRange = secondGroupStartTime.toString() + "**" + secondGroupStartTime.plusMinutes(5).toString();
            groups.computeIfAbsent(secondGroupRange, s -> new ArrayList<>()).add(item);
        }
    });

    // remove groups that contain only a single item
    groups.entrySet().removeIf(stringListEntry -> stringListEntry.getValue().size() == 1);

    for (String key : groups.keySet()) {
        System.out.println(String.format("%s %s", key, groups.get(key).stream().map(item -> item.name).collect(Collectors.toList())));
    }
}

输出

2020-08-21T00:05**2020-08-21T00:10 [item3, item4]
2020-08-21T00:00**2020-08-21T00:05 [item1, item2, item3]
2020-08-21T09:50**2020-08-21T09:55 [item5, item7]
2020-01-01T00:00**2020-01-01T00:05 [item8, item9]
2020-08-21T09:55**2020-08-21T10:00 [item5, item6]

我最初提出问题的主要原因是要找到一种合适且更有效的方法。考虑到我会有很多组,重复组以删除单个组并不是最好的做法。