Nodatime 中的四舍五入时间到最近的间隔

Rounding time in Nodatime to nearest interval

我们需要将时间下限到最接近的任意间隔(由例如时间跨度或持续时间表示)。

举个例子,我们需要将它下限到最近的十分钟。 例如13:02 变为 13:00 并且 14:12 变为 14:10

如果不使用 Nodatime,您可以执行类似 this:

的操作
// Floor
long ticks = date.Ticks / span.Ticks;
return new DateTime( ticks * span.Ticks );

这将使用时间跨度的刻度将日期时间下限到特定时间。

NodaTime 似乎暴露了一些我们以前没有考虑过的复杂性。你可以这样写一个函数:

public static Instant FloorBy(this Instant time, Duration duration)
=> time.Minus(Duration.FromTicks(time.ToUnixTimeTicks() % duration.BclCompatibleTicks));

但该实现似乎不正确。 "Floor to nearest ten minutes"好像是依赖于timezone/offset的时候。 虽然在 UTC 中可能是 13:02,但在偏移量为 +05:45 的尼泊尔,时间将是 18:47.

这意味着在 UTC 中,取整到最接近的十分钟,意味着减去两分钟,而在尼泊尔,这意味着减去七分钟。

我觉得我应该能够以某种方式通过任意时间跨度来舍入 ZonedDateTime 或 OffsetDateTime。我可以通过编写这样的函数来接近

public static OffsetDateTime FloorToNearestTenMinutes(this OffsetDateTime time)
{
    return time
        .Minus(Duration.FromMinutes(time.Minute % 10))
        .Minus(Duration.FromSeconds(time.Second));
}

但这不允许我指定任意持续时间,因为 OffsetDateTime 没有滴答的概念。

如何在考虑时区的情况下以任意间隔正确舍入 Instant/ZonedDateTime/OffsetDateTime?

对于OffsetDateTime,我建议你写一个Func<LocalTime, LocalTime>,在Noda Time术语中实际上是一个"adjuster"。然后你可以只使用 With 方法:

// This could be a static field somewhere - or a method, so you can use
// a method group conversion.
Func<LocalTime, LocalTime> adjuster =>
    new LocalTime(time.Hour, time.Minute - time.Minute % 10, 0);

// The With method applies the adjuster to just the time portion,
// keeping the date and offset the same.
OffsetDateTime rounded = originalOffsetDateTime.With(adjuster);

请注意,这只有效,因为您的四舍五入永远不会改变日期。如果您需要一个也可以更改日期的版本(例如,将第二天的 23:58 舍入到 00:00),那么您需要获取新的 LocalDateTime 并构建一个新的 OffsetDateTimeLocalDateTime 和原始偏移量。我们没有为此提供方便的方法,但这只是调用构造函数的问题。

由于您给出的原因,

ZonedDateTime 从根本上说 更棘手。目前,尼泊尔不遵守夏令时 - 但将来可能会这样做。在 DST 边界附近四舍五入可能会使您进入模棱两可甚至跳过的时间。这就是为什么我们不为 ZonedDateTime 提供类似的 With 方法。 (在您的情况下,这不太可能,尽管从历史上看是可能的……使用日期调整器,您很容易会遇到这种情况。)

您可以做的是:

  • 致电ZonedDateTime.ToOffsetDateTime
  • 如上OffsetDateTime四舍五入
  • 调用 OffsetDateTime.InZone(zone) 返回 ZonedDateTime

可以 然后检查结果 ZonedDateTime 的偏移量是否与原始偏移量相同,如果你想检测奇怪的情况 - 但你然后需要决定如何实际处理它们。不过,这种行为是相当合理的——如果你从 ZonedDateTime 开始,时间部分是(比如)01:47,你最终会在同一时区从 7 开始得到 ZonedDateTime几分钟前。 可能 如果在过去 7 分钟内发生转换,则不会 01:40...但我 怀疑 你不其实不用担心。

我最终从 Jon Skeets 的回答中获取了一些东西并滚动了我自己的 Rounder,它采用任意 Duration 来进行舍入。 (这是我需要的关键内容之一,这也是我不接受该答案的原因)。

根据 Jons 的建议,我将 Instant 转换为 OffsetDateTime 并应用舍入器,它采用任意持续时间。示例和实现如下:

// Example of usage
public void Example()
{
    Instant instant = SystemClock.Instance.GetCurrentInstant();
    OffsetDateTime offsetDateTime = instant.WithOffset(Offset.Zero);
    var transformedOffsetDateTime = offsetDateTime.With(t => RoundToDuration(t, Duration.FromMinutes(15)));
    var transformedInstant = transformedOffsetDateTime.ToInstant();
}

// Rounding function, note that it at most truncates to midnight at the day.
public static LocalTime RoundToDuration(LocalTime timeToTransform, Duration durationToRoundBy)
{
    var ticksInDuration = durationToRoundBy.BclCompatibleTicks;
    var ticksInDay = timeToTransform.TickOfDay;
    var ticksAfterRounding = ticksInDay % ticksInDuration;
    var period = Period.FromTicks(ticksAfterRounding);

    var transformedTime = timeToTransform.Minus(period);
    return transformedTime;
}