寻找下一个时间出现的情况,类似于TemporalAdjuster。

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

Find next occurrence of a time, like TemporalAdjuster

问题

有没有在JSR-310中寻找给定时间的下一个出现的内容?我真的在寻找与这个问题相同的内容,但是针对的是时间而不是日期。

例如,从2020-09-17的UTC时间06:30开始,我想找到下一个出现的时间是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)); // 这个方法不存在

在上面的代码中,我希望nextTime代表2020-09-18的UTC时间05:30,即第二天早上的05:30。

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

----------------------------------------
| startDateTime    | 期望的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  |
----------------------------------------
英文:

Is there anything in JSR-310 for finding the next occurrence of a given time? I'm really looking for the same thing as this question but for times instead of days.

For example, starting with a date of 2020-09-17 at 06:30 UTC, I'd like to find the next occurrence of 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

in the above, I'd like nextTime to represent 2020-09-18 at 05:30 UTC, i.e. 05:30 the following morning.

To clarify my expectations, all with a time of 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  |
----------------------------------------

答案1

得分: 4

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

Java 的时间 API 提供了4种具有这种特性的类型:LocalDateTimeOffsetDateTimeZonedDateTimeInstant

为了完全支持这4种类型,代码需要特殊处理 Instant,因为 InstantLocalTime 之间没有直接关联,还需要处理 ZonedDateTime 来处理夏时制的重叠。

以下是可以处理所有情况的实现:

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

重叠测试

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

// 我们从 2020 年 11 月 1 日凌晨 1:45 EDT 开始
ZoneId usEastern = ZoneId.of("America/New_York");
ZonedDateTime earlierOffset = ZonedDateTime.of(2020, 11, 1, 1, 45, 0, 0, usEastern);
System.out.println(earlierOffset);
// 现在我们寻找 1:20 AM 的下一个时间点,结果是 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 < 1:45)看起来更早,但实际上它是一个更晚的时间。

英文:

An adjuster like next(LocalTime time) only makes sense for types with both a date and a time.

Java's Time API comes with 4 types like that: LocalDateTime, OffsetDateTime, ZonedDateTime, and Instant.

To fully support all 4, the code need special handling for Instant, since Instant and LocalTime are not directly relatable, and for ZonedDateTime, to handle DST overlap.

This implementation can handle all that:

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

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

Test (with now() being 2020-09-18 at some time after 10 AM)

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

Output

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

Test Overlap

For US Eastern time zone, DST ends at 2:00 AM on Sunday, November 1, 2020.

// We start at 1:45 AM EDT on November 1, 2020
ZoneId usEastern = ZoneId.of(&quot;America/New_York&quot;);
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))));

Output

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

Even though the time of day appears earlier (1:20 < 1:45), it is actually a later time.

答案2

得分: 3

如果您只想使此代码适用于 LocalDateTimeLocalTime,或者任何具有24小时制的时间属性的其他类型的 Temporal,逻辑非常简单:

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

但是,对于所有具有时间属性的 Temporal,实际上要做到这一点相当困难。想想看如果针对 ZonedDateTime 要怎么做。我们可能不仅仅需要添加 24 小时,还可能需要添加 23 小时或 25 小时,因为夏令时转换会使得一个 "天" 变短或变长。你可以通过添加一个 "天" 来部分解决这个问题:

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 分钟,但上述代码将给我们返回第二天的日期时间。要处理这种情况,您需要像安德烈亚斯的答案中那样做一些复杂的操作。

如果您查看其他内置的时间调整器,它们都相当简单,所以我猜他们可能不想引入这种复杂性。

英文:

If you just want this to work with LocalDateTimes and LocalTimes, or any other type of Temporal that has a 24-hour day, the logic is quite simple:

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

But doing this for all Temporals that have a time component is actually quite hard. Think about what you'd have to do for ZonedDateTime. Rather than adding 24 hours, we might need to add 23 or 25 hours because DST transitions make a "day" shorter or longer. You can somewhat deal with this by adding a "day" instead:

public static TemporalAdjuster nextTime(LocalTime time) {
    return temporal -&gt; {
        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);
        }
    };
}

However, it still doesn’t always handle gaps and overlaps correctly. For example, when we are asking for the next 01:30 since 01:31, and there is an overlap transition of 1 hour at 02:00, i.e. the clock goes back an hour at 02:00. The correct answer is to add 59 minutes, but the above code will give us a date time in the next day. To handle this case you'd need to do something complicated like in Andreas' answer.

If you look at the other built in temporal adjusters, they are all pretty simple, so I guess they just didn't want to put in this complexity.

答案3

得分: 0

如果您将 ZonedDateTime 用作 refDateTime,@Andreas 的回答等同于以下内容,

fun ZonedDateTime.next2(targetTime: LocalTime): ZonedDateTime {
  val adjusted = with(targetTime)
  if (isBefore(adjusted)) {
    return adjusted
  }
  return plus(1, ChronoUnit.DAYS).with(targetTime)
}

我使用当前时间 (refDateTime) 在 2020 年到 2026 年之间的每一天进行了测试,并将目标时间设置为每天的每个小时和分钟,上述代码返回与 @Andreas 的回答相同的结果。

这里唯一的注意事项(对于这两种解决方案都适用)是我们不能返回不存在的时间。例如,如果您要求它返回夏时制开始前一天(02:30 之后)时间点 02:30 的下一个出现时间,您将得到夏时制开始时的 03:30,因为夏时制开始时的 02:30 不存在(实际上与 03:30 是相同的时间)。

英文:

If you use ZonedDateTime as refDateTime, @Andreas's answer amounts to this,

fun ZonedDateTime.next2(targetTime: LocalTime): ZonedDateTime {
  val adjusted = with(targetTime)
  if (isBefore(adjusted)) {
    return adjusted
  }
  return plus(1, ChronoUnit.DAYS).with(targetTime)
}

I tested with the current time (refDateTime) equal to every day between 2020 and 2026, and with the target time set to every hour and minute in the day, and the above returns the same result as @Andreas's answer.

The only caveat (with both solutions) here is that we can't return times that don't exist. E.g. if you ask it for the next occurrence of the time 02:30 on
the day before DST start (after 02:30), you get 03:30 on DST start, because 02:30 on DST start doesn't exist (and is actually the "same" time as 03:30 anyway).

huangapple
  • 本文由 发表于 2020年9月17日 17:09:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/63934816.html
匿名

发表评论

匿名网友

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

确定