【问题标题】:Check if dates are overlapping and return the maximum count检查日期是否重叠并返回最大计数
【发布时间】:2021-01-11 12:55:41
【问题描述】:

我有多个日期的开始和结束。 这些日期可能如下所示:

d1:      |----------|
d2:            |------|
d3:        |--------------|
d4:                         |----|
d5:   |----|

现在我需要检查重叠日期的最大计数。 所以在这个例子中,我们得到了最多 3 个重叠的日期(d1、d2、d3)。 考虑一下,可能有 0 到 n 个日期。

你能帮我完成这个任务吗? 提前谢谢你。

更新

输入:带有开始和结束点的 Java-Dates 列表,例如 List,其中 MyCustomDate 包含开始和结束日期

输出: 重叠日期(作为 MyCustomDate 列表)

每个时间跨度都包括一个 LocalDateTime 类型的起点和终点,包括小时和秒。

【问题讨论】:

  • 你的输入输出是什么样的?
  • 数据是否适合内存?你如何代表每个时期?您是否必须支持半开放期,例如没有结束日期?
  • 我已经更新了我的问题。
  • 对于包含范围,我可以想到一个 O(n^2) 解决方案。虽然不确定边缘情况。首先按开始对范围进行排序。对于排序列表中的每个范围r,检查r 的开头落在多少个范围内。找到最大值。
  • @IQbrod 我只需在您的代码中替换它即可使用 LocalDateTime 获得解决方案。谢谢!

标签: java date compare


【解决方案1】:

我的回答会考虑:

  • 假设 (d3, d5) 不重叠 => 重叠 (d1,d3,d5) = 2 因为在给定时间只有两个日期会重叠。
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

class Event {
    LocalDate startDate; // inclusive
    LocalDate endDate; // inclusive

    Event(LocalDate st, LocalDate end) {
        this.startDate = st;
        this.endDate = end;
    }

    // Getters & Setters omitted
}

public class Main {
    public static void main(String[] args) {
        List<Event> events = new ArrayList<Event>();
        events.add(new Event(LocalDate.of(2019,1,1), LocalDate.of(2019,5,1))); // d1
        events.add(new Event(LocalDate.of(2019,3,1), LocalDate.of(2019,6,1))); // d2
        events.add(new Event(LocalDate.of(2019,2,1), LocalDate.of(2019,7,1))); // d3
        events.add(new Event(LocalDate.of(2019,8,1), LocalDate.of(2019,12,1))); // d4
        // d5 do not overlap d3
        events.add(new Event(LocalDate.of(2018,12,1), LocalDate.of(2019,1,31))); // d5

        Integer startDateOverlaps = events.stream().map(Event::getStartDate).mapToInt(date -> overlap(date, events)).max().orElse(0);
        Integer endDateOverlaps = events.stream().map(Event::getEndDate).mapToInt(date -> overlap(date, events)).max().orElse(0);

        System.out.println(Integer.max(startDateOverlaps, endDateOverlaps));
    }

    public static Integer overlap(LocalDate date, List<Event> events) {
        return events.stream().mapToInt(event -> (! (date.isBefore(event.startDate) || date.isAfter(event.endDate))) ? 1 : 0).sum();
    }
}

我们对每个重叠的日期求和(即使它本身,否则 (d1, d2, d3) 只会计算 (d2, d3) 以进行 d1 检查)并测试每个 startDate 和 endDate。

【讨论】:

  • 无需发明自己的课程。请参阅 ThreeTen-Extra 库中的 LocalDateRange。该项目由领导 java.time 类 (JSR 310) 及其前身 Joda-Time 的人 Stephen Colebourne 领导。 LocalDateRange 提供多种比较方法,包括:交集、重叠、isBefore/isAfter、邻接等。
  • @BasilBourque 这是一个不错的库,但您依靠range.contains() 来确定@​​987654330@ 是包含还是排除,而我依靠存储在overlap() 中的日期比较。
  • 要比较一对日期范围,请参阅overlaps 方法:LocalDateRange#overlaps​( LocalDateRange other )
  • 给定 d1 = new Event(LocalDate.of(2019,1,1), LocalDate.of(2019,5,1));d5 = new Event(LocalDate.of(2018,12,1), LocalDate.of(2019,1,1)); 如果 endDate 是独占的,例如 overlap(d1,d5) = 0 怎么办?你不能,因为你依赖上面给出的方法。
  • java.timeThreeTen-Extra 在设计上都是线程安全的,使用immutable objects。具体来说,LocalDateTime 类 Javadoc 声明:这个类是不可变的和线程安全的。
【解决方案2】:

您可以简单地为每个Event(按天计算)生成startDateendDate 之间的所有事件并计算Map,其中键是LocalDate(作为单独的一天),值是该日期被看到的次数:

long l =
    Collections.max(
            events.stream()
                  .flatMap(x -> Stream.iterate(x.getStartDate(), date -> date.plusDays(1))
                        .limit(ChronoUnit.DAYS.between(x.getStartDate(), x.getEndDate().plusDays(1))))
                  .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
                  .values()
    );

【讨论】:

  • 如何使用带有小时和秒的 LocalDateTime 来实现这一点?
  • @Marcel 不要误会我的意思,但你问这个问题的事实意味着你并没有真正理解这应该做什么。无论如何:用date -&gt; date.plus(1, ChronoUnit.SECONDS)替换:date -&gt; date.plusDays(1)
【解决方案3】:

这个问题最初要求提供日期。发布答案后,问题更改为要求LocalDateTime。我会留下这个答案,因为它 (a) 回答了最初发布的问题,并且 (b) 可能对其他人有帮助。


其他答案看起来很有趣并且可能是正确的。但我发现以下代码更易于遵循和验证/调试。

警告:我并不声称这段代码是最好的、最精简的,也不是最快的。坦率地说,我在这里的尝试只是为了突破我自己对使用 Java 流和 lambda 的理解的极限。

不要发明自己的类来保存开始/结束日期。 ThreeTen-Extra 库提供了一个 LocalDateRange 类,以将附加到时间轴的时间跨度表示为一对 java.time.LocalDate 对象。 LocalDateRange 提供了几种方法:

  • 比较比如abutsoverlaps
  • unionintersection 等工厂方法。

我们可以在 Java 9 及更高版本中使用方便的List.of 方法定义输入,以创建一个不可修改的LocalDateRange 列表。

List < LocalDateRange > dateRanges =
        List.of(
                LocalDateRange.of( LocalDate.of( 2019 , 1 , 1 ) , LocalDate.of( 2019 , 5 , 1 ) ) ,
                LocalDateRange.of( LocalDate.of( 2019 , 3 , 1 ) , LocalDate.of( 2019 , 6 , 1 ) ) ,
                LocalDateRange.of( LocalDate.of( 2019 , 2 , 1 ) , LocalDate.of( 2019 , 7 , 1 ) ) ,
                LocalDateRange.of( LocalDate.of( 2019 , 8 , 1 ) , LocalDate.of( 2019 , 12 , 1 ) ) , // Not connected to the others.
                LocalDateRange.of( LocalDate.of( 2018 , 12 , 1 ) , LocalDate.of( 2019 , 1 , 31 ) )  // Earlier start, in previous year.
        );

确定所涉及的总体日期范围,即第一次开始和最后一次结束。

请记住,我们正在处理一个日期范围列表 (LocalDateRange),其中每个都包含一对日期 (LocalDate) 对象。比较器比较每个LocalDateRange 中存储的开始/结束LocalDate 对象,以获得最小值或最大值。这里看到的get 方法得到了一个LocalDateRange,所以我们然后调用getStart/getEnd 来检索存储在其中的开始/结束LocalDate

LocalDate start = dateRanges.stream().min( Comparator.comparing( localDateRange -> localDateRange.getStart() ) ).get().getStart();
LocalDate end = dateRanges.stream().max( Comparator.comparing( localDateRange -> localDateRange.getEnd() ) ).get().getEnd();

列出该间隔内的所有日期。 LocalDate#datesUntil 方法生成在日期对开始和结束之间找到的LocalDate 对象流。开始是包容的,而结束是排斥的。

List < LocalDate > dates =
        start
                .datesUntil( end )
                .collect( Collectors.toList() );

对于每个日期,获取包含该日期的日期范围列表。

Map < LocalDate, List < LocalDateRange > > mapDateToListOfDateRanges = new TreeMap <>();
for ( LocalDate date : dates )
{
    List < LocalDateRange > hits = dateRanges.stream().filter( range -> range.contains( date ) ).collect( Collectors.toList() );
    System.out.println( date + " ➡ " + hits );  // Visually interesting to see on the console.
    mapDateToListOfDateRanges.put( date , hits );
}

对于每个日期,获取包含该日期的日期范围的计数。我们想要对我们放入上面地图的每个List 进行计数。我的问题Report on a multimap by producing a new map of each key mapped to the count of elements in its collection value 讨论了生成一个新地图,其值是原始地图中集合的计数,我从Answer by Syco 提取代码。

Map < LocalDate, Integer > mapDateToCountOfDateRanges =
        mapDateToListOfDateRanges
                .entrySet()
                .stream()
                .collect(
                        Collectors.toMap(
                                ( Map.Entry < LocalDate, List < LocalDateRange > > e ) -> { return e.getKey(); } ,
                                ( Map.Entry < LocalDate, List < LocalDateRange > > e ) -> { return e.getValue().size(); } ,
                                ( o1 , o2 ) -> o1 ,
                                TreeMap :: new
                        )
                );

不幸的是,似乎没有办法让流按最大值过滤映射中的多个条目。请参阅:Using Java8 Stream to find the highest values from map

因此,首先我们要找到映射中一个或多个条目的值中的最大值。

Integer max = mapDateToCountOfDateRanges.values().stream().max( Comparator.naturalOrder() ).get();

然后我们只过滤具有该数字值的条目,将这些条目移动到新地图。

Map < LocalDate, Integer > mapDateToCountOfDateRangesFilteredByHighestCount =
        mapDateToCountOfDateRanges
                .entrySet()
                .stream()
                .filter( e -> e.getValue() == max )
                .collect(
                        Collectors.toMap(
                                Map.Entry :: getKey ,
                                Map.Entry :: getValue ,
                                ( o1 , o2 ) -> o1 ,
                                TreeMap :: new
                        )
                );

转储到控制台。

System.out.println( "dateRanges = " + dateRanges );
System.out.println( "start/end = " + LocalDateRange.of( start , end ).toString() );
System.out.println( "mapDateToListOfDateRanges = " + mapDateToListOfDateRanges );
System.out.println( "mapDateToCountOfDateRanges = " + mapDateToCountOfDateRanges );
System.out.println( "mapDateToCountOfDateRangesFilteredByHighestCount = " + mapDateToCountOfDateRangesFilteredByHighestCount );

短期结果。

[警告:我没有手动验证这些结果。使用此代码需要您自担风险,并自行验证。]

mapDateToCountOfDateRangesFilteredByHighestCount = {2019-03-01=3, 2019-03-02=3, 2019-03-03=3, 2019-03-04=3, 2019-03-05=3, 2019-03- 06=3, 2019-03-07=3, 2019-03-08=3, 2019-03-09=3, 2019-03-10=3, 2019-03-11=3, 2019-03-12= 3, 2019-03-13=3, 2019-03-14=3, 2019-03-15=3, 2019-03-16=3, 2019-03-17=3, 2019-03-18=3, 2019-03-19=3, 2019-03-20=3, 2019-03-21=3, 2019-03-22=3, 2019-03-23=3, 2019-03-24=3, 2019- 03-25=3, 2019-03-26=3, 2019-03-27=3, 2019-03-28=3, 2019-03-29=3, 2019-03-30=3, 2019-03- 31=3, 2019-04-01=3, 2019-04-02=3, 2019-04-03=3, 2019-04-04=3, 2019-04-05=3, 2019-04-06= 3, 2019-04-07=3, 2019-04-08=3, 2019-04-09=3, 2019-04-10=3, 2019-04-11=3, 2019-04-12=3, 2019-04-13=3, 2019-04-14=3, 2019-04-15=3, 2019-04-16=3, 2019-04-17=3, 2019-04-18=3, 2019- 04-19=​​3, 2019-04-20=3, 2019-04-21=3, 2019-04-22=3, 2019-04-23=3, 2019-04-24=3, 2019-04- 25=3, 2019-04-26=3, 2019-04-27=3, 2019-04-28=3, 2019-04-29=3, 2019-04-30=3}

完整代码

为了方便您复制粘贴,这里有一个完整的类来运行此示例代码。

package work.basil.example;


import org.threeten.extra.LocalDateRange;

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

public class DateRanger
{
    public static void main ( String[] args )
    {
        DateRanger app = new DateRanger();
        app.demo();
    }

    private void demo ( )
    {
        // Input.
        List < LocalDateRange > dateRanges =
                List.of(
                        LocalDateRange.of( LocalDate.of( 2019 , 1 , 1 ) , LocalDate.of( 2019 , 5 , 1 ) ) ,
                        LocalDateRange.of( LocalDate.of( 2019 , 3 , 1 ) , LocalDate.of( 2019 , 6 , 1 ) ) ,
                        LocalDateRange.of( LocalDate.of( 2019 , 2 , 1 ) , LocalDate.of( 2019 , 7 , 1 ) ) ,
                        LocalDateRange.of( LocalDate.of( 2019 , 8 , 1 ) , LocalDate.of( 2019 , 12 , 1 ) ) , // Not connected to the others.
                        LocalDateRange.of( LocalDate.of( 2018 , 12 , 1 ) , LocalDate.of( 2019 , 1 , 31 ) )  // Earlier start, in previous year.
                );


        // Determine first start and last end.
        LocalDate start = dateRanges.stream().min( Comparator.comparing( localDateRange -> localDateRange.getStart() ) ).get().getStart();
        LocalDate end = dateRanges.stream().max( Comparator.comparing( localDateRange -> localDateRange.getEnd() ) ).get().getEnd();
        List < LocalDate > dates =
                start
                        .datesUntil( end )
                        .collect( Collectors.toList() );

        // For each date, get a list of the date-dateRanges containing that date.
        Map < LocalDate, List < LocalDateRange > > mapDateToListOfDateRanges = new TreeMap <>();
        for ( LocalDate date : dates )
        {
            List < LocalDateRange > hits = dateRanges.stream().filter( range -> range.contains( date ) ).collect( Collectors.toList() );
            System.out.println( date + " ➡ " + hits );  // Visually interesting to see on the console.
            mapDateToListOfDateRanges.put( date , hits );
        }

        // For each of those dates, get a count of date-ranges containing that date.
        Map < LocalDate, Integer > mapDateToCountOfDateRanges =
                mapDateToListOfDateRanges
                        .entrySet()
                        .stream()
                        .collect(
                                Collectors.toMap(
                                        ( Map.Entry < LocalDate, List < LocalDateRange > > e ) -> { return e.getKey(); } ,
                                        ( Map.Entry < LocalDate, List < LocalDateRange > > e ) -> { return e.getValue().size(); } ,
                                        ( o1 , o2 ) -> o1 ,
                                        TreeMap :: new
                                )
                        );

        // Unfortunately, there seems to be no way to get a stream to filter more than one entry in a map by maximum value.
        // So first we find the maximum number in a value for one or more entries of our map.
        Integer max = mapDateToCountOfDateRanges.values().stream().max( Comparator.naturalOrder() ).get();
        // Then we filter for only entries with a value of that number, moving those entries to a new map.
        Map < LocalDate, Integer > mapDateToCountOfDateRangesFilteredByHighestCount =
                mapDateToCountOfDateRanges
                        .entrySet()
                        .stream()
                        .filter( e -> e.getValue() == max )
                        .collect(
                                Collectors.toMap(
                                        Map.Entry :: getKey ,
                                        Map.Entry :: getValue ,
                                        ( o1 , o2 ) -> o1 ,
                                        TreeMap :: new
                                )
                        );

        System.out.println( "dateRanges = " + dateRanges );
        System.out.println( "start/end = " + LocalDateRange.of( start , end ).toString() );
        System.out.println( "mapDateToListOfDateRanges = " + mapDateToListOfDateRanges );
        System.out.println( "mapDateToCountOfDateRanges = " + mapDateToCountOfDateRanges );
        System.out.println( "mapDateToCountOfDateRangesFilteredByHighestCount = " + mapDateToCountOfDateRangesFilteredByHighestCount );
    }
}

【讨论】:

  • 如果使用 LocalDate 也可以使用 LocalDateTime 吗?
  • 这里的主要问题是datesUntil,这意味着粒度是实际的一天。
  • @Marcel 不,此代码用于日期。当我写这个答案时,你在问日期。然后您将问题更改为LocalDateTime。写完答案后,您不应更改问题的性质。在发帖之前花更多的精力准备你的问题。你浪费我们的时间来创作答案是非常粗鲁的,这些答案后来看起来毫无意义和离题。
  • @Eugene 我的答案对实际日期的粒度是此问题的原始版本的解决方案,而不是问题。作者后来把Question改成LocalDateTime
  • “你在我写这个答案时询问日期”然后你没有阅读这个问题,因为在这个答案发布前 9 个多小时,问题是更新了“精确到毫秒” 说明,明确说明问题是针对完整时间戳,而不仅仅是针对日期。 --- 即使只有例如2 个日期范围,但它们涵盖例如3 年,代码将处理 1000 多天。代码不能很好地扩展。 --- 我当然没有发现 40 多行主逻辑代码比我回答中的 10 行主逻辑代码“更容易遵循和验证/调试”
【解决方案4】:

此时存在的其他 2 个答案都是 O(n2),对所有事件进行笛卡尔连接。此答案显示了一种具有 O(n log n) 时间复杂度的替代方法。

我们所做的是构建一个有序的日期列表,并为每个日期记录在该日期开始和结束的范围。它可以存储为单个数字,例如如果一个范围结束 (-1) 并且 3 个范围开始 (+3),则日期的增量为 +2。

基本上,每个事件实际上是2个事件,一个开始事件和一个结束事件。

然后我们按日期顺序迭代列表,更新运行总计,并记住最大运行总计。

有几种编码方式。我将使用常规循环,而不是流,并且由于问题说每个日期都有一个低至毫秒的起点和终点,我们将使用带有两个 Instant 字段的 DateRange 对象。

static int maxRangeOverlaps(List<DateRange> ranges) {
    Map<Instant, Delta> dateDelta = new TreeMap<>();
    for (DateRange range : ranges) {
        dateDelta.computeIfAbsent(range.getStart(), k -> new Delta()).value++;
        dateDelta.computeIfAbsent(range.getEnd(), k -> new Delta()).value--;
    }
    int total = 0, max = 0;
    for (Delta delta : dateDelta.values())
        if ((total += delta.value) > max)
            max = total;
    return max;
}
public final class DateRange {
    private final Instant start; // inclusive
    private final Instant end; // exclusive

    // Constructor and getter methods here
}
final class Delta {
    public int value;
}

测试

//       ....:....1....:....2....:....3
// d1:      |----------|
// d2:            |------|
// d3:        |--------------|
// d4:                         |----|
// d5:   |----|
List<DateRange> ranges = Arrays.asList(
        new DateRange(LocalDate.of(2021,1, 4), LocalDate.of(2021,1,15)),
        new DateRange(LocalDate.of(2021,1,10), LocalDate.of(2021,1,17)),
        new DateRange(LocalDate.of(2021,1, 6), LocalDate.of(2021,1,21)),
        new DateRange(LocalDate.of(2021,1,23), LocalDate.of(2021,1,28)),
        new DateRange(LocalDate.of(2021,1, 1), LocalDate.of(2021,1, 6)));

System.out.println(maxRangeOverlaps(ranges)); // prints 3

通过添加辅助构造函数,将上述测试简化为使用LocalDate 而不是Instant

public DateRange(LocalDate start, LocalDate end) {
    this(start.atStartOfDay().toInstant(ZoneOffset.UTC),
         end.atStartOfDay().toInstant(ZoneOffset.UTC));
}

【讨论】:

  • 最好的 Stackoverflow。一个非常有趣的答案,完全被忽略了。到目前为止,这里是最优秀、最聪明的人。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-04-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-27
  • 1970-01-01
相关资源
最近更新 更多