使用Java Time重复发生的事件(java.time或任何时间库)

时间:2015-09-11 09:58:48

标签: java recurring java-time recurring-events

我使用非常简单的重复模式(每x天或每周)为Java中的重复事件建模。给定一个带有startDateTimeendDateTime的事件对象和以天为单位的重现(来自java.time包的类型为Period),我想知道某个事件是否发生在给定的事件中日期(考虑DST)。

关于"将DST考虑在内的一些背景" (在评论中提出要求后):

  • 这些事件用于安排员工的工作班次。
  • 员工每周7天,每天24小时轮班工作(通常为8小时)
  • 夜班从0:00开始,到8:00结束。因此,关于DST的变化, 班次可以是7小时或9小时。在任何情况下 应该有差距。

我目前正以不那么高效的方式这样做:

public class Event {
  ZonedDateTime start;
  ZonedDateTime end;
  Period recurrence;  // e.g. 7 days

  public boolean includes(ZonedDateTime dateTime) {
    ZonedDateTime tmpStart = startDate;
    ZoneDateTime tmpEnd = endDate;
    do {
        if (dateTime.isAfter(tmpStart) && dateTime.isBefore(tmpEnd))
            return true;
        tmpStart = tmpStart.plus(recurrence);
        tmpEnd = tmpEnd.plus(recurrence);
    } while (dateTime.isAfter(tmpStart));

    return false;
  }
}

示例 对于以下事件

start = 2015-09-07T00:00
end = 2015-09-07T08:00
recurrence = Period.ofDays(7)

调用includes会产生以下结果:

assertTrue(event.includes(2015-09-14T01:00)
assertTrue(event.includes(2015-09-21T01:00)
assertFalse(event.includes(2015-09-21T09:00)

执行此操作的高效方法(如上所述,将DST考虑在内)?我还想在日历上展示这些活动。

更新possible duplicate使用我在上面使用的完全相同的算法。如果没有循环遍历所有日期,有没有更快的方法呢?

2 个答案:

答案 0 :(得分:2)

避免使用ZonedDateTime类型 - 请参阅下面的更新

如果您的经常性时段总是只有几天,我会看到以下优化(特别是如果您的开始日期时间比您的dateTime参数早得多):

  public boolean includes(ZonedDateTime dateTime) {
    ZonedDateTime tmpStart = start;
    ZonedDateTime tmpEnd = end;

    int distance = (int) ChronoUnit.DAYS.between(start, dateTime) - 1;
    if (distance > 0) {
        int factor = (int) (distance / recurrence.getDays());
        if (factor > 0) {
            Period quickAdvance = recurrence.multipliedBy(factor);
            tmpStart = start.plus(quickAdvance);
            tmpEnd = end.plus(quickAdvance);
        }
    }

    while (!tmpStart.isAfter(dateTime)) { // includes equality - okay for you?
        if (tmpEnd.isAfter(dateTime)) {
            return true;
        }
        tmpStart = tmpStart.plus(recurrence);
        tmpEnd = tmpEnd.plus(recurrence);
    }

    return false;
  }

关于DST,只要你对JDK的标准策略很好,通过差距的大小推进无效的本地时间(从冬季到夏季的时间),班级ZonedDateTime就适合这个方案。对于重叠,此类提供特殊方法withEarlierOffsetAtOverlap()withLaterOffsetAtOverlap()

但是,这些特征与如何确定给定的重复间隔是否包含给定的日期时间的问题并不真正相关,因为时区校正将以相同的方式应用于所有相关的日期时间对象({ {1}},startend)提供了所有对象具有相同的时区。如果您想确定dateTimestart(或endtmpStart)之间的持续时间,则DST非常重要。

<强>更新

我发现了一个与日光效果相关的隐藏错误。详细说明:

tmpEnd

ZoneId zone = ZoneId.of("Europe/Berlin"); ZonedDateTime start = LocalDate.of(2015, 3, 25).atTime(2, 0).atZone(zone); // inclusive ZonedDateTime end = LocalDate.of(2015, 3, 25).atTime(10, 0).atZone(zone); // exclusive Period recurrence = Period.ofDays(2); ZonedDateTime test = LocalDate.of(2015, 3, 31).atTime(2, 30).atZone(zone); // exclusive System.out.println("test=" + test); // test=2015-03-31T02:30+02:00[Europe/Berlin] start = start.plus(recurrence); end = end.plus(recurrence); System.out.println("start + 2 days = " + start); // 2015-03-27T02:00+01:00[Europe/Berlin] System.out.println("end + 2 days = " + end); // 2015-03-27T10:00+01:00[Europe/Berlin] start = start.plus(recurrence); // <- DST change to summer time!!! end = end.plus(recurrence); System.out.println("start + 4 days = " + start); // 2015-03-29T03:00+02:00[Europe/Berlin] System.out.println("end + 4 days = " + end); // 2015-03-29T10:00+02:00[Europe/Berlin] start = start.plus(recurrence); end = end.plus(recurrence); System.out.println("start + 6 days = " + start); // 2015-03-31T03:00+02:00[Europe/Berlin] System.out.println("end + 6 days = " + end); // 2015-03-31T10:00+02:00[Europe/Berlin] boolean includes = !start.isAfter(test) && end.isAfter(test); System.out.println("includes=" + includes); // false (should be true!!!) 重复添加句点可能会因DST效应而改变本地时间间隔。但固定工作时间表通常根据本地时间戳(在上午2点到上午10点的例子中)定义。因此,在基本的本地时间问题上应用一种通用时间戳本身就是错误的。

解决匹配工作时间间隔问题的正确数据类型是:ZonedDateTime

LocalDateTime

它是否也是正确的数据类型,以小时为单位确定物理实际工作时间?如果您将 public boolean includes(LocalDateTime dateTime) { LocalDateTime tmpStart = start; LocalDateTime tmpEnd = end; int distance = (int) ChronoUnit.DAYS.between(start, dateTime) - 1; if (distance > 0) { int factor = (int) (distance / recurrence.getDays()); if (factor > 0) { Period quickAdvance = recurrence.multipliedBy(factor); tmpStart = start.plus(quickAdvance); tmpEnd = end.plus(quickAdvance); } } while (!tmpStart.isAfter(dateTime)) { if (tmpEnd.isAfter(dateTime)) { return true; } tmpStart = tmpStart.plus(recurrence); tmpEnd = tmpEnd.plus(recurrence); } return false; } 与时区合并,则为是。

LocalDateTime

<强>结论:

int minutes = (int) ChronoUnit.MINUTES.between(start.atZone(zone), end.atZone(zone)); System.out.println("hours=" + minutes / 60); // 7 (one hour less due to DST else 8) 类型有很多disadvantages。一个例子是这里涉及的时间算术。通过选择ZonedDateTime和显式时区参数的组合,可以更好地解决与DST相关的大多数问题。

关于解决无效的当地时间:

好问题。 JSR-310只提供一个内置transition strategy for gaps,因此解决当地时间02:30的最终结果将是3:30,而不是3:00。这也是LocalDateTime类型的作用。引文:

  

...将本地日期时间调整为稍后的长度   间隙。对于典型的一小时夏令时变化,当地   日期时间通常会在一小时后移动到偏移量中   对应“夏天”。

但是,可以应用以下解决方法来实现下一个有效时间(请注意,ZonedDateTime的文档显示错误的代码架构和示例):

ZoneRules.getTransition(LocalDateTime)

为了进行比较(因为您最初还要求在其他库中提供解决方案): Joda-Time 只会在本地时间无效时抛出异常,可能不是您想要的。在我的库 Time4J 中,我已经定义了与LocalDateTime ldt = LocalDateTime.of(2015, 3, 29, 2, 30, 0, 0); ZoneRules rules = ZoneId.of("Europe/Berlin").getRules(); ZoneOffsetTransition conflict = rules.getTransition(ldt); if (conflict != null && conflict.isGap()) { ldt = conflict.getDateTimeAfter(); } System.out.println(ldt); // 2015-03-29T03:00 对应的类型PlainTimestamp并以这种方式解决了解决问题(没有if-else-constructions):

LocalDateTime

答案 1 :(得分:0)

正如许多评论所示,您的问题没有准确定义。所以我会做一些假设。

您的问题的核心似乎是:

  

查明事件是否在指定日期发生(将DST考虑在内)

  • 我们假设您的意思是仅限日期。更具体地说,假设您的意思是给定日期与startDateTime ZoneId zoneId = ZoneId.of( "America/Montreal" ); // CRITICAL: Read the class doc to understand the policy used by java.time to handle DST cutovers when moving a LocalDateTime value into a zoned value. // http://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html#of-java.time.LocalDateTime-java.time.ZoneId- ZonedDateTime start = ZonedDateTime.of( LocalDateTime.parse( "2015-09-07T00:00" ), zoneId ); ZonedDateTime stop = ZonedDateTime.of( LocalDateTime.parse( "2015-09-07T08:00" ), zoneId ); Integer recurrence = 2; LocalDate givenDate = LocalDate.now( zoneId ); // The given date being in the same time zone as our shift start date-time is a *critical* assumption. LocalDate startLocalDate = start.toLocalDate( ); Period period = Period.between( startLocalDate, givenDate ); int days = period.getDays( ); int mod = ( days % recurrence ); Boolean eventHappensOnThatDate = ( mod == 0 ); 实例成员位于同一时区。
  • 我们假设班次包含在日期内(与昨天或明天没有重叠),如你所说。

在这种情况下,如果我们只关心日期,DST就无关紧要了。事实上,时间是完全无关紧要的。我们所需要的只是天数。模数将告诉我们给定日期是否是重复次数的倍数。

System.out.println("Does event: " + start   + "/" + stop + " recurring every " + recurrence + " days happen on " + givenDate + " ➙ " + eventHappensOnThatDate );

转储到控制台。

Does event: 2015-09-07T00:00-04:00[America/Montreal]/2015-09-07T08:00-04:00[America/Montreal] recurring every 2 days happen on 2015-09-11 ➙ true

跑步时。

System.out.println(readWords(filename)
                .mapToInt(String::length)
                .average()
                .getAsDouble()
);