Маленькие чудеса C#/.NET – структура DateTimeOffset

    Рассмотрим некоторые части .Net Framework'a, выглядящие тривиальными, но вполне способными сделать ваш код более простым как в написании, так и в сопровождении.


    Пишущие на .NET (а если вы этого не делаете, то зря читаете этот пост) наверняка время от времени используют для своих нужд структуру DateTime. Эта структура удобна для хранения дат, времени или даты/времени, относящихся к локальной временной зоне (или же к UTC).


    Однако, бывают случаи, когда вам необходимо сохранить время в виде смещения, а не конвертировать его в локальное время. И вот здесь вам на помощь придёт структура, впервые появившаяся в .NET 3.5 — DateTimeOffset.

    Проблема: парсинг DateTime может привести к конвертации в локальное время

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

    2012-03-01 00:00:00-05:00

    Такая запись говорит о том, что человек родился 1 марта 2012 года в неуказанное время (в конце концов, большая часть форм не требует от вас заполнения времени вашего рождения). Но поскольку экземпляр структуры DateTime был сериализован «в лоб», то он и содержит время, установленное в полночь, согласно своей временной зоне.


    Итак, зная, что эта дата совместима с Восточной временной зоной (Eastern Time Zone), а мы находимся в Центральной временной зоне (Central Time Zone) мы парсим её так:

    // ясно что здесь выполняется чтение файла/потока/и т.п.
    var dateString = "2012-03-01 00:00:00-05:00";
    // парсим в DateTime
    var birthDay = DateTime.Parse(dateString);

    Выглядит идеально, не так ли? Но тут кроется проблемка. Если мы проверим содержимое объекта DateTime на нашей локальной машине, где выставлена Центральная временная зона, то увидим вот что:

    2012-02-29 11:00:00 PM

    Что случилось? (Или как говорил один персонаж — Кто это сделал?) Да, метод DateTime.Parse() конвертировал дату в локальную временную зону поскольку оригинальная дата рождения хранилась с указанным смещением. Вам просто оказали услугу — конвертировали указанную дату и время в ваши локальные дату и время. Это не так и плохо, если бы речь не шла о дне рождения, которое с 1 марта переместилось на 29 февраля.


    Конечно, мы можем созвониться с третьей стороной и попросить перестать включать время в строку с датой или же перестать высылать смещение вместе со временем (в этом случае она перестанет конвертироваться в локальное время, но будет отмечена как DateTimeKind.Unspecified).


    Однако бывает, что у нас нет возможности таким образом изменить ситуацию.


    Бывают случаи, когда вы хотите считывать дату и время со смещением, но не конвертировать его в локальную временную зону. И вот тут вам пригодится DateTimeOffset.

    DateTimeOffset – хранит DateTime и Offset

    Так что там про DateTimeOffset? Структура так же проста, как и её имя, DateTimeOffset это дата+время+смещение. Именно поэтому она представляет намного более точную точку во времени, поскольку включает информацию о смещении, по которому были установлены текущие дата и время.


    По правде говоря, функциональность DateTime и DateTimeOffset во многом перекрывается, а поскольку у Microsoft есть руководство по выбору того или другого, то я рекомендую ознакомиться с ней в MSDN. Статья называется «Choosing Between DateTime, DateTimeOffset, and TimeZoneInfo».


    В целом, вы можете использовать DateTime, если вы «прикреплены» к одной временной зоне или используете только универсальное время в формате UTC. Но если вы хотите использовать даты и время из разных временных зон, а также хотите сохранить информацию о смещении без конвертации в локальное время, то лучше использовать DateTimeOffset.


    В структуре DateTimeOffset есть много таких же свойств, как и в структуре DateTime (Day, Month, Year, Hour, Minute, Second, и т.п.), потому здесь их я описывать не стану. Главное отличие состоит в нескольких новых свойствах:

    DateTime

    Возвращает DateTime без учёта смещения.

    LocalDateTime

    Возвращает конвертированный DateTime, с учётом смещения (т.е. в локальной временной зоне).

    Offset

    Возвращает смещение относительно UTC.

    UtcDateTime

    Возвращает DateTime как время UTC.


    Свойство DateTime возвращает вам DateTime (не приведенное к локальной временной зоне), а свойство Offset имеет формат TimeSpan, представляющее смещение от времени UTC. Также есть свойства LocalDateTime и UtcDateTime, конвертирующие данный DateTimeOffset в DateTime для локальной временной зоны или UTC.


    Также замечу, что свойства Now и UtcNow структуры DateTimeOffset возвращают не тип DateTime, а DateTimeOffsets с соответствующим смещением от UTC. Конечно, как и DateTime, DateTimeOffset обладает методами, оперирующими с датой/временем, возвращая тип DateTimeOffset вместо DateTime.


    Так как нам всё это поможет в выше приведенном примере? Теперь мы знаем, что третья сторона высылает нам дату и время своей временной зоны, которое не нужно конвертировать в локальные дату/время. Поэтому можно использовать DateTimeOffset.Parse() (или TryParse()) и выбрать только дату:

    // ясно что здесь выполняется чтение файла/потока/и т.п.
    var dateString = "2012-03-01 00:00:00-05:00";
    // парсим день рождения как смещение даты/времени (без конвертирования в локальные даты/время)
    var dtOffset = DateTimeOffset.Parse(dateString);
    // теперь если нам надо сравнить результат с другими объектами типа локального DateTime
    // мы просто используем свойство Date для получения даты
    // без времени или смещения
    var theDay = dtOffset.Date;


    Таким образом, вы можете легко парсить даты без необходимости отслеживания “полночного сдвига” или же использовать его там, где необходимо иметь дату, время и смещение без конвертирования в локальное время.

    Итоги

    Хоть структура DateTime и является достаточно мощной в плане парсинга, манипулирования и сравнения дат/времени, она может доставить немало неприятных минут в работе с датами в формате разных временных зон. DateTimeOffset в этом случае проявляет себя куда более гибкой, поскольку использует смещение от UTC.


    Вольный перевод (с) В.Ф.Чужа ака hDrummer, оригинал здесь.
    • +51
    • 23,7k
    • 7
    Поделиться публикацией

    Комментарии 7

      +1
        0
        Эта структура удобна для хранения дат, времени или даты/времени, относящихся к локальной временной зоне (иначе — UTC).

        Смысл второго абзаца немного искажен. В оригинале говорится о том что время можно хранить либо локальное либо UTC. Нету разницы пока не учавствует смещение.
          0
          Спасибо, поправлю.
            +1
                        var dateString = "2012-03-01 00:00:00";
                        // парсим в DateTime
                        var birthDay = DateTime.Parse(dateString);
            


            >Выглядит идеально, не так ли?
            Вот так выглядит идеально.

            Дата является частью времени. В ней передан часовой пояс и она переводится в локальное время.

            >Проблема: парсинг DateTime может привести к конвертации в локальное время
            тут нет никакой проблемы.
              0
              В статье опечатка, «согласно своей временной зоны» — должно быть «согласно (чему?) своей временной зоне».

              В родительном падеже пишется только канцеляризм «согласно договора», который является сокращением от «согласно (чему?) тексту договора».
                0
                Поправил.
                0
                а вы можете выложить исходники на гитхаб(или аналог). вещь полезная и возможно ктото ваше начинание продолжит :)

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

                Самое читаемое