Время округления в Нодатиме до ближайшего интервала - PullRequest
1 голос
/ 17 апреля 2020

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

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

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

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

, которое будет использовать тики временного промежутка для представления даты и времени в определенное время c.

Кажется, NodaTime демонстрирует некоторую сложность, которую мы раньше не рассматривали. Вы можете написать такую ​​функцию:

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

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

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

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

Ответы [ 2 ]

1 голос
/ 17 апреля 2020

Для 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 минут ... но я подозреваю вам на самом деле не нужно беспокоиться об этом.

0 голосов
/ 19 апреля 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;
}

...