避免使用类型ZonedDateTime
- 请参阅下面的更新
如果您的经常性周期始终仅包含几天,我会看到以下优化(特别是对于您的开始日期时间远早于您的日期时间参数的情况):
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()
.
但是,这些功能与如何找出给定的循环间隔是否包含给定的日期时间的问题并不真正相关,因为时区校正将以相同的方式应用于所有涉及的日期时间对象(start
和end
)dateTime
提供所有对象都有相同的时区。如果您想确定start
和end
(或tmpStart
和tmpEnd
)之间的持续时间,则 DST 很重要。
更新:
我发现了一个与夏令时效果有关的隐藏错误。详细地:
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!!!)
ZonedDateTime
由于 DST 效应,重复添加一个周期可以永远改变本地时间间隔。但是固定的工作时间表通常是根据本地时间戳定义的(在示例中为凌晨 2 点到上午 10 点)。因此,在基本上是当地时间的问题上应用一种通用时间戳本质上是错误的。
解决匹配工作时间间隔问题的正确数据类型是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;
}
确定物理实际工作时间(以小时为单位)是否也是正确的数据类型?是的,如果您将 aLocalDateTime
与时区结合使用。
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)
结论:
该类型ZonedDateTime
有很多缺点。一个例子是这里涉及的时间算术。LocalDateTime
通过选择明确的时区参数的组合,可以更好地解决大多数与 DST 相关的问题。
关于解决无效当地时间:
好问题。JSR-310 只为间隙提供了一种内置转换策略,因此解决无效本地时间 02:30 的最终结果将是 3:30,而不是 3:00。这也是类型ZonedDateTime
所做的。引文:
...本地日期时间根据间隙的长度调整为较晚。对于典型的一小时夏令时更改,本地日期时间将在一小时后移动到通常对应于“夏季”的偏移量中。
但是,可以应用以下解决方法来实现下一个有效时间(请注意,文档ZoneRules.getTransition(LocalDateTime)
显示了错误的代码架构和示例):
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
为了比较(因为您最初还要求其他库中的解决方案):如果本地时间无效, Joda-Time只会抛出异常,可能不是您想要的。在我的库Time4J中,我定义了PlainTimestamp
对应的类型LocalDateTime
并以这种方式解决了解决问题(没有 if-else-constructions):
import static net.time4j.tz.GapResolver.NEXT_VALID_TIME;
import static net.time4j.tz.OverlapResolver.EARLIER_OFFSET;
PlainTimestamp tsp = PlainTimestamp.of(2015, 3, 29, 2, 30);
Moment nextValidTime = // equivalent of java.time.Instant
tsp.in(Timezone.of(EUROPE.BERLIN).with(NEXT_VALID_TIME.and(EARLIER_OFFSET)));
tsp = nextValidTime.toZonalTimestamp(EUROPE.BERLIN);
System.out.println(tsp);
// 2015-03-29T03 (minute is zero and therefore left out here)