Время округления в Nodatime до ближайшего интервала

Нам нужно довести время до ближайшего произвольного интервала (представленного, например, Timespan или Duration).

Предположим для примера, что нам нужно перекрыть его с точностью до десяти минут. например 13:02 становится 13:00, а 14:12 становится 14:10

Без использования Nodatime вы могли бы сделать что-то вроде вот этого:

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

Но такая реализация кажется неправильной. «От минимума до ближайших десяти минут», похоже, зависит от часового пояса / смещения времени. Хотя это может быть 13:02 по всемирному координированному времени, в Непале со смещением +05: 45 время будет 18:47.

Это означает, что в всемирном координированном координированном времени, округление до ближайших десяти минут, означало бы вычитание двух минут, в то время как в Непале это означало бы вычитание семи минут.

Я чувствую, что должен каким-то образом обойти 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 с произвольным интервалом с учетом часовых поясов?


person Gustav Wengel    schedule 17.04.2020    source источник


Ответы (2)


Для OffsetDateTime я бы посоветовал вам написать Func<LocalTime, LocalTime>, который фактически является «регулятором» в терминологии Noda Time. Затем вы можете просто использовать метод 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 и построить новый OffsetDateTime с этим LocalDateTime и исходным смещением. У нас нет удобного метода для этого, просто нужно вызвать конструктор.

ZonedDateTime принципиально сложнее по указанным вами причинам. Прямо сейчас в Непале не соблюдается летнее время, но, возможно, это произойдет в будущем. Округление около границы летнего времени может привести к неоднозначному или даже пропущенному времени. Вот почему мы не предоставляем аналогичный With метод для ZonedDateTime. (В вашем случае это маловероятно, хотя исторически возможно ... с регуляторами даты вы легко можете оказаться в такой ситуации.)

Что вы могли сделать:

  • Звоните ZonedDateTime.ToOffsetDateTime
  • Округлите OffsetDateTime, как указано выше
  • Позвоните OffsetDateTime.InZone(zone), чтобы вернуться к ZonedDateTime

Вы можете затем проверить, что смещение результирующего ZonedDateTime такое же, как и у оригинала, если вы хотите обнаружить странные случаи, но тогда вам нужно будет решить, что на самом деле с ними делать. Однако поведение довольно разумное - если вы начнете с ZonedDateTime с временным интервалом (скажем) 01:47, вы получите ZonedDateTime в том же часовом поясе за 7 минут до этого. возможно, что не было бы 01:40, если бы переход произошел в течение последних 7 минут ... но я подозреваю, что вам на самом деле не нужно об этом беспокоиться .

person Jon Skeet    schedule 17.04.2020

В итоге я взял кое-что из ответа Джона Скитса и запустил свой собственный Rounder, который занимает произвольную продолжительность для округления. (Это было одной из ключевых вещей, которые мне были нужны, и поэтому я не принимаю этот ответ).

Согласно предложению Джонса, я конвертирую 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;
}

person Gustav Wengel    schedule 19.04.2020