UTC для локального преобразования времени для ранее сохраненных дат, если правила изменения часового пояса

Я храню продукт в db. Все даты (sql server datetime) - это UTC, а вместе с датами я храню идентификатор часового пояса для этого продукта. Пользователь вводит даты, когда продукт доступен "от" и "до" в листинге. Поэтому я делаю что-то вроде:

// Convert user datetime to UTC
var userEnteredDateTime = DateTime.Parse("11/11/2014 9:00:00");
// TimeZoneInfo id will be stored along with the UTC datetime
var tz = TimeZoneInfo.FindSystemTimeZoneById("FLE Standard Time");
// following produces: 9/11/2014 7:00:00 AM (winter time - 1h back)
var utcDateTime = TimeZoneInfo.ConvertTimeToUtc(userEnteredDateTime, tz);

и сохраните запись. Предположим, что пользователь сделал это 1 августа, в то время как его часовой пояс, смещенный на UTC, все еще равен +03: 00, тем не менее сохраненная дата для будущего списка имеет правильное значение +02: 00, потому что конверсия учитывала "зимний" период, времени для этого периода.

Вопрос в том, какое значение будет иметь значение datetime, если я попытаюсь преобразовать этот продукт с даты "от" и "до" в локальный часовой пояс продукта 11/11/2014, если, например, из-за некоторых новых правил переход к зимнему времени было отказано, поэтому часовой пояс все еще +03: 00 вместо +02: 00?

// Convert back
var userLocalTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, tz);

Я получу 10AM или верну 9AM, потому что патч OS/.NET справится с этим?

Спасибо!

PS: TimeZoneInfo имеет метод ToSerializedString(), если я скорее сохраню это значение вместо идентификатора часового пояса, это будет гарантировать, что через UTC datetime + serialized timezoneinfo я всегда смогу преобразовать исходный вход datetime пользователя?

Ответ 1

В описываемом вами сценарии вы получите 10:00. Функция преобразования часового пояса не имела бы представления о том, что значение было первоначально введено как 9:00 AM, потому что вы сохранили время UTC 7:00 утра.

Это иллюстрирует один из случаев, когда совет "всегда хранить UTC" является ошибочным. Когда вы работаете с будущими событиями, это не всегда работает. Проблема в том, что правительства часто меняют свое мнение о часовых поясах. Иногда они дают разумное уведомление (например, Соединенные Штаты, 2007), но иногда они этого не делают (например, Египет, 2014).

Когда вы сделали первоначальное преобразование с локального времени в UTC, вы намеренно решили, что правила часового пояса не будут меняться. Другими словами, вы решили, что вы назначили событие универсальной шкале времени, основанной исключительно на правилах часового пояса, как вы знали их в то время.

Способ избежать этого прост: Будущие события должны быть запланированы по местному времени. Теперь я не имею в виду "локальный для вашего компьютера", а скорее "локальный для пользователя", поэтому вам нужно знать часовой пояс пользователя, и вы также должны хранить идентификатор часового пояса где-то.

Вам также нужно будет решить, что вы хотите сделать, если событие попадает в переход <+ w760 > -forward или fall-back для летнего времени. Это особенно важно для моделей повторения.

В конечном счете, вам нужно выяснить, когда запускать событие. Или в вашем случае вам нужно решить, прошло ли событие или нет. Это можно сделать несколькими способами:

Вариант 1

  • Вы можете вычислить соответствующее значение UTC для каждого локального времени и сохранить его в отдельном поле.

  • В течение некоторого цикла (ежедневно, еженедельно и т.д.) вы можете пересчитать ожидаемые значения UTC из своих локальных значений и ваше текущее понимание правил часовых поясов. Или, если вы вручную применяете обновления часового пояса, вы можете в любой момент перераспределить все.

Вариант 2

  • Вы можете сохранить значения как DateTimeOffset, а не DateTime. Он будет содержать исходное местное время и смещение, которое вы рассчитали на основе правил часового пояса, как вы знали их во время ввода.

    Значения
  • DateTimeOffset могут быть легко возвращены в UTC, поэтому они, как правило, работают очень хорошо для этого. Вы можете прочитать больше в DateTime vs DateTimeOffset.

  • Как и в варианте 1, вы периодически пересматриваете значения или после обновлений данных часового пояса и корректируете смещения для выравнивания с новыми данными часового пояса.

  • Это то, что я обычно рекомендую, особенно если вы используете базу данных, поддерживающую типы DateTimeOffset, такие как SQL Server или RavenDB.

Вариант 3

  • Вы можете сохранить значения как локальные DateTime.

  • При запросе вы будете вычислять текущее время в целевом часовом поясе и сравнивать с этим значением.

    DateTime now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, targetTZ);
    bool passed = now >= eventTime;
    
  • Нижняя сторона этой опции заключается в том, что вам может потребоваться сделать много запросов, если у вас есть события во множестве разных часовых поясов.

  • У вас могут также возникнуть проблемы со значениями, близкими к переходу DST с возвратом, поэтому будьте осторожны, если вы используете этот подход.

Я рекомендую против идеи сериализации самого часового пояса. Если часовой пояс изменился, он изменился. Притвориться, что это не так, не является хорошим решением.