【问题标题】:Find next occurrence of a time, like TemporalAdjuster查找下一次出现的时间,例如 TemporalAdjuster
【发布时间】:2021-01-04 03:46:09
【问题描述】:

JSR-310 中是否有任何内容可以查找给定时间的下一次出现?我真的在寻找与 this question 相同的东西,但要寻找几次而不是几天。

例如,从 2020 年 9 月 17 日 06:30 UTC 开始,我想找到下一个 05:30 的出现:

LocalTime time = LocalTime.of(5, 30);
ZonedDateTime startDateTime = ZonedDateTime.of(2020, 9, 17, 6, 30, 0, 0, ZoneId.of("UTC"));

ZonedDateTime nextTime = startDateTime.with(TemporalAdjusters.next(time)); // This doesn't exist

在上面,我希望nextTime 代表 2020-09-18 世界标准时间 05:30,即第二天早上 05:30。

为了澄清我的期望,所有时间都在 05:30:

----------------------------------------
| startDateTime    | expected nextTime |
| 2020-09-07 06:30 | 2020-09-08 05:30  |
| 2020-09-07 05:00 | 2020-09-07 05:30  |
| 2020-09-07 05:30 | 2020-09-08 05:30  |
----------------------------------------

【问题讨论】:

  • 所以它是一个类似于我已经在我的库中实现的功能Time4J 好吧,你必须在 JSR 310 中编写自己的调整器......

标签: java java-time jsr310


【解决方案1】:

如果您只是希望它与LocalDateTimes 和LocalTimes 或任何其他类型的Temporal 一起工作,每天 24 小时,逻辑非常简单:

public static TemporalAdjuster nextTime(LocalTime time) {
    return temporal -> {
        LocalTime lt = LocalTime.from(temporal);
        if (lt.isBefore(time)) {
            return temporal.with(time);
        } else {
            return temporal.plus(Duration.ofHours(24)).with(time);
        }
    };
}

但是对所有具有时间组件的Temporals 执行此操作实际上非常困难。想想你必须为ZonedDateTime 做些什么。我们可能需要增加 23 或 25 小时,而不是增加 24 小时,因为 DST 转换会使“一天”变短或变长。您可以通过添加“一天”来解决这个问题:

public static TemporalAdjuster nextTime(LocalTime time) {
    return temporal -> {
        LocalTime lt = LocalTime.from(temporal);
        if (lt.isBefore(time) || !temporal.isSupported(ChronoUnit.DAYS)) {
            return temporal.with(time);
        } else {
            return temporal.plus(1, ChronoUnit.DAYS).with(time);
        }
    };
}

但是,它仍然总是正确处理间隙和重叠。例如,当我们要求从 01:31 开始的下一个 01:30 时,在 02:00 有 1 小时的重叠转换,即时钟在 02:00 时回退一个小时。正确答案是加上 59 分钟,但是上面的代码会给我们一个第二天的日期时间。要处理这种情况,您需要做一些复杂的事情,比如 Andreas 的回答。

如果你看看其他内置的时间调整器,它们都很简单,所以我猜他们只是不想加入这种复杂性。

【讨论】:

  • nextTime(LocalTime time) 这样的方法只对带有日期和时间的类型有意义。对于没有时间的类型当然没有意义,对于没有日期的类型,您只需使用输入,所以这也没有任何意义。在标准 Java 中,只有 4 种类型同时具有这两种类型,即 LocalDateTimeOffsetDateTimeZonedDateTimeInstant,而 Instant 不支持 LocalTime.from(),因此可以使用此代码。其余 3 种类型均支持ChronoUnit.DAYS,因此无需检查。
  • @OleV.V. - 就我而言,它是一个简单的应用程序,用于告诉 EV 何时(或不)充电,价格变化的时间始终是 UTC,所以 - 就我而言 - 不是问题。我可以看到,这可能是它不包含在核心库中的众多原因之一。
【解决方案2】:

next(LocalTime time) 这样的调整器只对同时具有日期和时间的类型有意义。

Java 的 Time API 有 4 种类型:LocalDateTimeOffsetDateTimeZonedDateTimeInstant

要完全支持所有 4 个,代码需要对 Instant 进行特殊处理,因为 InstantLocalTime 不直接相关,对于 ZonedDateTime,需要处理 DST 重叠。

这个实现可以处理所有这些:

public static TemporalAdjuster next(LocalTime time) {
    return temporal -> {
        if (temporal instanceof Instant) {
            OffsetDateTime utcDateTime = ((Instant) temporal).atOffset(ZoneOffset.UTC);
            return next(utcDateTime, time).toInstant();
        }
        return next(temporal, time);
    };
}

@SuppressWarnings("unchecked")
private static <T extends Temporal> T next(T refDateTime, LocalTime targetTime) {
    T adjusted = (T) refDateTime.with(targetTime);
    if (refDateTime.until(adjusted, ChronoUnit.NANOS) > 0)
        return adjusted;
    if (adjusted instanceof ChronoZonedDateTime<?>) {
        ChronoZonedDateTime<?> laterOffset = ((ChronoZonedDateTime<?>) adjusted).withLaterOffsetAtOverlap();
        if (laterOffset != adjusted && refDateTime.until(laterOffset, ChronoUnit.NANOS) > 0)
            return (T) laterOffset;
    }
    return (T) refDateTime.plus(1, ChronoUnit.DAYS).with(targetTime);
}

测试(now() 是 2020-09-18 上午 10 点之后的某个时间)

System.out.println(LocalDateTime.now().with(next(LocalTime.of(10, 0))));
System.out.println(OffsetDateTime.now().with(next(LocalTime.of(10, 0))));
System.out.println(ZonedDateTime.now().with(next(LocalTime.of(10, 0))));
System.out.println(Instant.now().with(next(LocalTime.of(10, 0))));

输出

2020-09-19T10:00
2020-09-19T10:00-04:00
2020-09-19T10:00-04:00[America/New_York]
2020-09-19T10:00:00Z

测试重叠

对于美国东部时区,DST 于 2020 年 11 月 1 日星期日凌晨 2:00 结束。

// We start at 1:45 AM EDT on November 1, 2020
ZoneId usEastern = ZoneId.of("America/New_York");
ZonedDateTime earlierOffset = ZonedDateTime.of(2020, 11, 1, 1, 45, 0, 0, usEastern);
System.out.println(earlierOffset);
// Now we look for next 1:20 AM after the 1:45 AM, and will find 1:20 AM EST
System.out.println(earlierOffset.with(next(LocalTime.of(1, 20))));

输出

2020-11-01T01:45-04:00[America/New_York]
2020-11-01T01:20-05:00[America/New_York]

尽管一天中的时间出现得较早(1:20

【讨论】:

  • 老实说,我认为处理 Instants 没有意义。 Instants 表示自纪元以来的秒数,我不会将 Instant 的 UTC 时间称为 Instant 的“时间分量”。另外,如果我们从ZonedDateTime 需要的只是withLaterOffsetAtOverlap,我们不需要未经检查的强制转换,因为我们可以只使用Temporal 的方法来实现该方法。例如您可以通过adjusted.query(TemporalQueries.zone()) 获取该区域。这也将为ChronoZonedDateTime 提供支持。
  • @Sweeper 由于ZonedDateTime 实现了ChronoZonedDateTime,它声明了withLaterOffsetAtOverlap() 方法,所以您只需要对此进行区分即可。 --- 当我们可以使用我们已经可用的方法时,为什么我们要“获取区域”,然后必须执行更复杂的逻辑来检查和应用稍后的偏移量? --- 我认为在 Instant 上调用“下一个上午 10:30”没有什么荒谬的,因为 Instant 显然有一个时间。如果以这种方式调用,拥有一个可以处理Instant 的更完整的方法是一个很好的实用方法。
猜你喜欢
  • 2021-09-25
  • 2016-05-10
  • 2011-05-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-20
  • 1970-01-01
相关资源
最近更新 更多