用Java将具有startTime和endTime的LocalDateTime对象进行分组

huangapple go评论79阅读模式
英文:

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

Item 类:

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

    // 构造函数等
}

为简单起见,我只会提到分钟,但日期也很重要。在给定一个 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 的方法进行此类分组?

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

英文:

I want to implement a grouping algorithm to group this List into minute intervals.

Example list:

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

Item class :

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

    // constructor etc
}

For simplicity, I will only refer to minutes but dates also matter. Given an interval of 5 minutes 00:00 - 00:02 can be grouped to the group of range 00:00 - 00:05 while 00:03 - 00:07 can possibly be grouped to two groups 00:00 - 00:05 and 00:05 - 00:10.

Desired output for the above example list (names inluced only for readability output should contain the whole Item object):

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

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

Edit* To avoid negative comments I added my "non-efficient" solution in the answers.

答案1

得分: 3

A short answer to the main question:
> Is it possible to do such grouping using a method like Collectors#groupingBy?
is yes.

As mentioned in the comments, the main issue with this task is that a single item cannot be "grouped" into a single entry in the general case, but it needs to be split into several entries depending on both startTime and endTime.

Possibly, more than two 5-minute ranges can be used, for example: startTime: 00:02; endTime: 00:12 will cover three ranges: 00:00-00:05, 00:05-00:10, 00:10-00:15 -- this case is updated for item4.

That being said, the following solution can be offered:

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

public class Solution {
    public static void main(String args[]) {
        List&lt;Item&gt; items = Arrays.asList(
            // ... (item list initialization)
        );
        
        items.stream()
             .flatMap(Solution::convert)
             .collect(Collectors.groupingBy(x -&gt; x.getKey(), LinkedHashMap::new, Collectors.mapping(x -&gt; x.getValue(), Collectors.toList())))
             .values()
             .forEach(System.out::println);
    }
    
    public static Stream&lt;Map.Entry&lt;LocalDateTime, Item&gt;&gt; convert(Item item) {
        LocalDateTime start = getKey(item.getStartTime());
        LocalDateTime end = getKey(item.getEndTime()).plusMinutes(5);
        
        return Stream
                .iterate(start, d -&gt; d.isBefore(end), d -&gt; d.plusMinutes(5))
                .map(d -&gt; 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);
    }
}

Output

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

Note

Some Java 9 features are used in the code snippet:

Update

Java 9 features can be replaced with the following Java 8 compatible code:

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

If resulting values are filtered to contain at least two items, the result is as follows:

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

Output

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

英文:

A short answer to the main question:
> Is it possible to do such grouping using a method like Collectors#groupingBy?

is yes.

As mentioned in the comments, main issue with this task is that a single item cannot be "grouped" into a single entry in general case, but it needs to be multiplexed into several entries depending on both startTime and endTime.

Possibly, more than two 5-minute ranges can be used, for example: startTime: 00:02; endTime: 00:12 will cover three ranges: 00:00-00:05, 00:05-00:10, 00:10-00:15 -- this case is updated for item4.

That being said, the following solution can be offered:

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

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

Output

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

Note

Some Java 9 features are used in the code snippet:

Update

Java 9 features can be replaced with the following Java 8 compatible code:

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

If resulting values are filtered to contain valueat least two items, the result is as follows:

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

Output

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

答案2

得分: 1

以下是您的解决方案:

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]

原始问题的主要原因是要找到一种更合适和更高效的方法来执行此操作。重复遍历组以删除单个组不是最佳选择,特别是考虑到可能有许多组。

英文:

Here is my solution :

public static void main(String[] args) {
List&lt;Item&gt; items = Arrays.asList(
new Item(LocalDateTime.parse(&quot;2020-08-21T00:00:00&quot;), LocalDateTime.parse(&quot;2020-08-21T00:02:00&quot;), &quot;item1&quot;),
new Item(LocalDateTime.parse(&quot;2020-08-21T00:01:00&quot;), LocalDateTime.parse(&quot;2020-08-21T00:03:00&quot;), &quot;item2&quot;),
new Item(LocalDateTime.parse(&quot;2020-08-21T00:03:00&quot;), LocalDateTime.parse(&quot;2020-08-21T00:07:00&quot;), &quot;item3&quot;),
new Item(LocalDateTime.parse(&quot;2020-08-21T00:08:00&quot;), LocalDateTime.parse(&quot;2020-08-21T00:12:00&quot;), &quot;item4&quot;),
new Item(LocalDateTime.parse(&quot;2020-08-21T09:50:37&quot;), LocalDateTime.parse(&quot;2020-08-21T09:56:49&quot;), &quot;item5&quot;),
new Item(LocalDateTime.parse(&quot;2020-08-21T09:59:37&quot;), LocalDateTime.parse(&quot;2020-08-21T10:02:37&quot;), &quot;item6&quot;),
new Item(LocalDateTime.parse(&quot;2020-08-21T09:49:37&quot;), LocalDateTime.parse(&quot;2020-08-21T09:51:37&quot;), &quot;item7&quot;),
new Item(LocalDateTime.parse(&quot;2019-12-31T23:59:37&quot;), LocalDateTime.parse(&quot;2020-01-01T00:03:37&quot;), &quot;item8&quot;),
new Item(LocalDateTime.parse(&quot;2020-01-01T00:04:37&quot;), LocalDateTime.parse(&quot;2020-01-01T00:06:37&quot;), &quot;item9&quot;)
);
Map&lt;String, List&lt;Item&gt;&gt; groups = new HashMap&lt;&gt;();
items.stream().forEach(item -&gt; {
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() + &quot;**&quot; + firstGroupStartTime.plusMinutes(5).toString();
groups.computeIfAbsent(groupRange, s -&gt; new ArrayList&lt;&gt;()).add(item);
} else {
String firstGroupRange = firstGroupStartTime.toString() + &quot;**&quot; + firstGroupStartTime.plusMinutes(5).toString();
groups.computeIfAbsent(firstGroupRange, s -&gt; new ArrayList&lt;&gt;()).add(item);
String secondGroupRange = secondGroupStartTime.toString() + &quot;**&quot; + secondGroupStartTime.plusMinutes(5).toString();
groups.computeIfAbsent(secondGroupRange, s -&gt; new ArrayList&lt;&gt;()).add(item);
}
});
// remove groups that contain only a single item
groups.entrySet().removeIf(stringListEntry -&gt; stringListEntry.getValue().size() == 1);
for (String key : groups.keySet()) {
System.out.println(String.format(&quot;%s %s&quot;, key, groups.get(key).stream().map(item -&gt; item.name).collect(Collectors.toList())));
}
}

Output

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]

The main reason for my original question was to find a proper and more efficient way of doing that. Reiterating over groups to remove single groups is not the best thing to do considering that I would have a lot of groups.

huangapple
  • 本文由 发表于 2020年8月24日 19:51:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/63560490.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定