Работа с датами и временем
Для работы с датами и временем в .NET предназначена структура DateTime . Она представляет дату и время от 00:00:00 1 января 0001 года до 23:59:59 31 декабря 9999 года.
Для создания нового объекта DateTime также можно использовать конструктор. Пустой конструктор создает начальную дату:
DateTime dateTime = new DateTime(); Console.WriteLine(dateTime); // 01.01.0001 0:00:00
То есть мы получим минимально возможное значение, которое также можно получить следующим образом:
Console.WriteLine(DateTime.MinValue);
Чтобы задать конкретную дату, нужно использовать один из конструкторов, принимающих параметры:
DateTime date1 = new DateTime(2015, 7, 20); // год - месяц - день Console.WriteLine(date1); // 20.07.2015 0:00:00
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); // год - месяц - день - час - минута - секунда Console.WriteLine(date1); // 20.07.2015 18:30:25
Если необходимо получить текущую время и дату, то можно использовать ряд свойств DateTime:
Console.WriteLine(DateTime.Now); Console.WriteLine(DateTime.UtcNow); Console.WriteLine(DateTime.Today);
20.07.2015 11:43:33 20.07.2015 8:43:33 20.07.2015 0:00:00
Свойство DateTime.Now берет текущую дату и время компьютера, DateTime.UtcNow — дата и время относительно времени по Гринвичу (GMT) и DateTime.Today — только текущая дата.
При работе с датами надо учитывать, что по умолчанию для представления дат применяется григорианский календарь. Но что будет, если мы захотим получить день недели для 5 октября 1582 года:
DateTime someDate = new DateTime(1582, 10, 5); Console.WriteLine(someDate.DayOfWeek);
Консоль высветит значение Tuesday, то есть вторник. Однако, как может быть известно из истории, впервые переход с юлианского календаря на григорианский состоялся в октябре 1582 года. Тогда после даты 4 октября (четверг) (еще по юлианскому календарю) сразу перешли к 15 октября (пятница)(уже по григорианскому календарю). Таким образом, фактически выкинули 10 дней. То есть после 4 октября шло 15 октября.
В большинстве случаев данный факт вряд ли как-то повлияет на вычисления, однако при работе с очень давними датами данный аспект следует учитывать.
Операции с DateTime
Основные операции со структурой DateTime связаны со сложением или вычитанием дат. Например, надо к некоторой дате прибавить или, наоборот, отнять несколько дней.
Для добавления дат используется ряд методов:
- Add(TimeSpan value) : добавляет к дате значение TimeSpan
- AddDays(double value) : добавляет к текущей дате несколько дней
- AddHours(double value) : добавляет к текущей дате несколько часов
- AddMinutes(double value) : добавляет к текущей дате несколько минут
- AddMonths(int value) : добавляет к текущей дате несколько месяцев
- AddYears(int value) : добавляет к текущей дате несколько лет
Например, добавим к некоторой дате 3 часа:
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); // 20.07.2015 18:30:25 Console.WriteLine(date1.AddHours(3)); // 20.07.2015 21:30:25
Для вычитания дат используется метод Subtract(DateTime date) :
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); // 20.07.2015 18:30:25 DateTime date2 = new DateTime(2015, 7, 20, 15, 30, 25); // 20.07.2015 15:30:25 Console.WriteLine(date1.Subtract(date2)); // 03:00:00
Здесь даты различаются на три часа, поэтому результатом будет дата «03:00:00».
Метод Substract не имеет возможностей для отдельного вычитания дней, часов и так далее. Но это и не надо, так как мы можем передавать в метод AddDays() и другие методы добавления отрицательные значения:
// вычтем три часа DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); // 20.07.2015 18:30:25 Console.WriteLine(date1.AddHours(-3)); // 20.07.2015 15:30:25
Кроме операций сложения и вычитания еще есть ряд методов форматирования дат:
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); Console.WriteLine(date1.ToLocalTime()); // 20.07.2015 21:30:25 Console.WriteLine(date1.ToUniversalTime()); // 20.07.2015 15:30:25 Console.WriteLine(date1.ToLongDateString()); // 20 июля 2015 г. Console.WriteLine(date1.ToShortDateString()); // 20.07.2015 Console.WriteLine(date1.ToLongTimeString()); // 18:30:25 Console.WriteLine(date1.ToShortTimeString()); // 18:30
Метод ToLocalTime() преобразует время UTC в локальное время, добавляя смещение относительно времени по Гринвичу. Метод ToUniversalTime() , наоборот, преобразует локальное время во время UTC, то есть вычитает смещение относительно времени по Гринвичу. Остальные методы преобразуют дату к определенному формату.
Маленькие чудеса 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 без учёта смещения.
LocalDateTime
Возвращает конвертированный DateTime, с учётом смещения (т.е. в локальной временной зоне).
Возвращает смещение относительно 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, оригинал здесь.
Структура DateTimeOffset
Представляет момент времени, который обычно выражается в виде даты и времени суток, относительно времени в формате UTC.
Пространство имен: System
Сборка: mscorlib (в mscorlib.dll)
Синтаксис
public struct DateTimeOffset : IComparable, IFormattable, ISerializable, IDeserializationCallback, IComparableDateTimeOffset>, IEquatableDateTimeOffset>
Конструкторы
Тип | Имя | Описание |
---|---|---|
![]() |
DateTimeOffset(Int64, TimeSpan) | Инициализирует новый экземпляр структуры DateTimeOffset с использованием заданного количества тактов и смещения. |
![]() |
DateTimeOffset(DateTime) | Инициализирует новый экземпляр структуры DateTimeOffset с использованием заданного значения DateTime . |
![]() |
DateTimeOffset(DateTime, TimeSpan) | Инициализирует новый экземпляр структуры DateTimeOffset с использованием заданного значения DateTime и смещения. |
![]() |
DateTimeOffset(Int32, Int32, Int32, Int32, Int32, Int32, TimeSpan) | Инициализирует новый экземпляр структуры DateTimeOffset , используя год, месяц, день, час, минуту, секунду и смещение. |
![]() |
DateTimeOffset(Int32, Int32, Int32, Int32, Int32, Int32, Int32, TimeSpan) | Инициализирует новый экземпляр структуры DateTimeOffset , используя год, месяц, день, час, минуту, секунду, миллисекунду и смещение. |
![]() |
DateTimeOffset(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Calendar, TimeSpan) | Инициализирует новый экземпляр структуры DateTimeOffset , используя год, месяц, день, час, минуту, секунду, миллисекунду и смещение для заданного календаря. |
Свойства
Тип | Имя | Описание |
---|---|---|
![]() |
Date | Получает значение DateTime , представляющее компонент даты в текущем объекте DateTimeOffset . |
![]() |
DateTime | Получает значение DateTime , представляющее дату и время в текущем объекте DateTimeOffset . |
![]() |
Day | Возвращает день месяца, представленный текущим объектом DateTimeOffset . |
![]() |
DayOfWeek | Возвращает день недели, представленный текущим объектом DateTimeOffset . |
![]() |
DayOfYear | Возвращает день года, представленный текущим объектом DateTimeOffset . |
![]() |
Hour | Возвращает часовой компонент, представленный текущим объектом DateTimeOffset . |
![]() |
LocalDateTime | Получает значение DateTime , представляющее местную дату и время в текущем объекте DateTimeOffset . |
![]() |
Millisecond | Возвращает временной компонент миллисекунд, представленный текущим объектом DateTimeOffset . |
![]() |
Minute | Возвращает компонент минут, представленный текущим объектом DateTimeOffset . |
![]() |
Month | Возвращает компонент месяца даты, представленный текущим объектом DateTimeOffset . |
![]() |
Now | Получает объект DateTimeOffset , для которого в качестве значения установлена текущая дата и время на текущем компьютере, а в качестве смещения — смещение местного времени от времени в формате UTC. |
![]() |
Offset | Возвращает смещение по времени от времени в формате UTC. |
![]() |
Second | Возвращает компонент секунд по показаниям часов, представленный текущим объектом DateTimeOffset . |
![]() |
Ticks | Возвращает количество тактов, представляющее местную дату и время в текущем объекте DateTimeOffset по показаниям часов. |
![]() |
TimeOfDay | Получает время суток текущего объекта DateTimeOffset . |
![]() |
UtcDateTime | Получает значение DateTime , представляющее дату и время текущего объекта DateTimeOffset в формате UTC. |
![]() |
UtcNow | Возвращает объект DateTimeOffset , в качестве даты и времени которого установлены текущие дата и время в формате UTC, а в качестве смещения — значение Zero . |
![]() |
UtcTicks | Возвращает количество тактов, которое представляет дату и время текущего объекта DateTimeOffset в формате UTC. |
![]() |
Year | Возвращает компонент года даты, представленный текущим объектом DateTimeOffset . |
Методы
Тип | Имя | Описание |
---|---|---|
![]() |
Add(TimeSpan) | Добавляет указанный интервал времени к объекту DateTimeOffset . |
![]() |
AddDays(Double) | Добавляет указанное количество полных и неполных дней к текущему объекту DateTimeOffset . |
![]() |
AddHours(Double) | Добавляет указанное количество полных и неполных часов к текущему объекту DateTimeOffset . |
![]() |
AddMilliseconds(Double) | Добавляет указанное количество миллисекунд к текущему объекту DateTimeOffset . |
![]() |
AddMinutes(Double) | Добавляет указанное количество полных и неполных минут к текущему объекту DateTimeOffset . |
![]() |
AddMonths(Int32) | Добавляет указанное количество месяцев к текущему объекту DateTimeOffset . |
![]() |
AddSeconds(Double) | Добавляет указанное количество полных и неполных секунд к текущему объекту DateTimeOffset . |
![]() |
AddTicks(Int64) | Добавляет указанное количество тактов к текущему объекту DateTimeOffset . |
![]() |
AddYears(Int32) | Добавляет указанное количество лет к объекту DateTimeOffset . |
![]() ![]() |
Compare(DateTimeOffset, DateTimeOffset) | Сравнивает два объекта DateTimeOffset , и указывает, как соотносятся между собой указанные в них моменты времени: первый раньше второго, первый и второй равны, или же первый позже второго. |
![]() |
CompareTo(DateTimeOffset) | Сравнивает текущий объект DateTimeOffset с заданным объектом DateTimeOffset и указывает, когда наступает момент, заданный в первом объекте: раньше, позже или одновременно с моментом, указанным во втором объекте DateTimeOffset . |
![]() |
Equals(Object) | Определяет, представляет ли объект DateTimeOffset тот же момент времени, что и заданный объект. (Переопределяет Object.Equals(Object).) |
![]() |
Equals(DateTimeOffset) | Определяет, представляет ли текущий объект DateTimeOffset тот же момент времени, что и заданный объект DateTimeOffset . |
![]() ![]() |
Equals(DateTimeOffset, DateTimeOffset) | Определяет, представляют ли два заданных объекта DateTimeOffset один и тот же момент времени. |
![]() |
EqualsExact(DateTimeOffset) | Определяет, представляет ли текущий объект DateTimeOffset тот же момент времени, что и заданный объект DateTimeOffset и совпадают ли их смещения. |
![]() ![]() |
FromFileTime(Int64) | Преобразует заданную временную характеристику файла Windows в ее эквивалент по местному времени. |
![]() ![]() |
FromUnixTimeMilliseconds(Int64) | |
![]() ![]() |
FromUnixTimeSeconds(Int64) | |
![]() |
GetHashCode() | Возвращает хэш-код для текущего объекта DateTimeOffset . (Переопределяет Object.GetHashCode().) |
![]() |
GetType() | Возвращает объект Type для текущего экземпляра. (Наследуется от Object.) |
![]() ![]() |
Parse(String) | Преобразует заданное строковое представление даты, времени и смещения в их эквивалент DateTimeOffset . |
![]() ![]() |
Parse(String, IFormatProvider, DateTimeStyles) | Преобразует заданное строковое представление даты и времени в его эквивалент DateTimeOffset , используя указанную информацию о форматировании, связанную с языком и региональными параметрами, а также заданный стиль форматирования. |
![]() ![]() |
Parse(String, IFormatProvider) | Преобразует заданное строковое представление даты и времени в его эквивалент DateTimeOffset , используя указанные сведения о форматировании, связанные с языком и региональными параметрами. |
![]() ![]() |
ParseExact(String, String[], IFormatProvider, DateTimeStyles) | Преобразует заданное строковое представление даты и времени в его эквивалент DateTimeOffset , используя заданные форматы, указанные сведения о форматировании, связанные с языком и региональными параметрами, а также стиль.Формат строкового представления должен полностью соответствовать одному из заданных форматов. |
![]() ![]() |
ParseExact(String, String, IFormatProvider, DateTimeStyles) | Преобразует заданное строковое представление даты и времени в его эквивалент DateTimeOffset , используя заданный формат, указанные сведения о форматировании, связанные с языком и региональными параметрами, а также стиль.Формат строкового представления должен полностью соответствовать заданному формату. |
![]() ![]() |
ParseExact(String, String, IFormatProvider) | Преобразует заданное строковое представление даты и времени в его эквивалент DateTimeOffset , используя указанные сведения о форматировании, связанные с языком и региональными параметрами.Формат строкового представления должен полностью соответствовать заданному формату. |
![]() |
Subtract(TimeSpan) | Вычитает указанный интервал времени из текущего объекта DateTimeOffset . |
![]() |
Subtract(DateTimeOffset) | Вычитает значение DateTimeOffset , представляющее определенную дату и время в текущем объекте DateTimeOffset . |
![]() |
ToFileTime() | Преобразует значение текущего объекта DateTimeOffset во временную характеристику файла Windows. |
![]() |
ToLocalTime() | Преобразует текущий объект DateTimeOffset в объект DateTimeOffset , представляющий местное время. |
![]() |
ToOffset(TimeSpan) | Преобразует значение текущего объекта DateTimeOffset в дату и время, указанные в значении смещения. |
![]() |
ToString(String, IFormatProvider) | Преобразует значение текущего объекта DateTimeOffset в эквивалентное ему строковое представление с использованием указанного формата и сведений об особенностях формата для данного языка и региональных параметров. |
![]() |
ToString(IFormatProvider) | Преобразует числовое значение текущего объекта DateTimeOffset в эквивалентное ему строковое представление с использованием указанных сведений об особенностях форматирования для данного языка и региональных параметров. |
![]() |
ToString(String) | Преобразует значение текущего объекта DateTimeOffset в эквивалентное ему строковое представление с использованием заданного формата. |
![]() |
ToString() | Преобразует значение текущего объекта DateTimeOffset в эквивалентное ему строковое представление. (Переопределяет Object.ToString().) |
![]() |
ToUniversalTime() | Преобразует текущий объект DateTimeOffset в значение DateTimeOffset , представляющее время в формате UTC. |
![]() |
ToUnixTimeMilliseconds() | |
![]() |
ToUnixTimeSeconds() | |
![]() ![]() |
TryParse(String, IFormatProvider, DateTimeStyles, DateTimeOffset) | Предпринимает попытку преобразования указанного строкового представления даты и времени в его эквивалент DateTimeOffset , и возвращает значение, позволяющее определить успешность преобразования. |
![]() ![]() |
TryParse(String, DateTimeOffset) | Предпринимает попытку преобразования указанного строкового представления даты и времени в его эквивалент DateTimeOffset , и возвращает значение, позволяющее определить успешность преобразования. |
![]() ![]() |
TryParseExact(String, String, IFormatProvider, DateTimeStyles, DateTimeOffset) | Преобразует заданное строковое представление даты и времени в его эквивалент DateTimeOffset , используя заданный формат, указанные сведения о форматировании, связанные с языком и региональными параметрами, а также стиль.Формат строкового представления должен полностью соответствовать заданному формату. |
![]() ![]() |
TryParseExact(String, String[], IFormatProvider, DateTimeStyles, DateTimeOffset) | Преобразует заданное строковое представление даты и времени в его эквивалент DateTimeOffset , используя заданный массив форматов, указанные сведения о форматировании, связанные с языком и региональными параметрами, и стиль форматирования.Формат строкового представления должен полностью соответствовать одному из заданных форматов. |
Поля
Тип | Имя | Описание |
---|---|---|
![]() ![]() |
MaxValue | Представляет максимально допустимое значение типа DateTimeOffset .Это поле доступно только для чтения. |
![]() ![]() |
MinValue | Представляет наиболее раннее из возможных значений DateTimeOffset .Это поле доступно только для чтения. |
developers/references/system.datetimeoffset.txt · Последние изменения: 2021/07/22 14:29 (внешнее изменение)
DateTime vs DateTimeOffset
What is the difference between a DateTime and a DateTimeOffset and when should one be used? Currently, we have a standard way of dealing with .NET DateTime s in a TimeZone-aware way: Whenever we produce a DateTime we do it in UTC (e.g. using DateTime.UtcNow ), and whenever we display one, we convert back from UTC to the user’s local time. That works fine, but I’ve been reading about DateTimeOffset and how it captures the local and UTC time in the object itself.
20.8k 67 67 gold badges 76 76 silver badges 102 102 bronze badges
asked Dec 2, 2010 at 2:39
David Reis David Reis
12.8k 7 7 gold badges 36 36 silver badges 43 43 bronze badges
When it comes to storage, stackoverflow.com/questions/4715620/… is interesting too.
Jul 21, 2016 at 18:22
Curious people might also want to read storing utc is not a silver bullet
Feb 23, 2021 at 8:20
10 Answers 10
DateTimeOffset is a representation of instantaneous time (also known as absolute time). By that, I mean a moment in time that is universal for everyone (not accounting for leap seconds, or the relativistic effects of time dilation). Another way to represent instantaneous time is with a DateTime where .Kind is DateTimeKind.Utc .
This is distinct from calendar time (also known as civil time), which is a position on someone’s calendar, and there are many different calendars all over the globe. We call these calendars time zones. Calendar time is represented by a DateTime where .Kind is DateTimeKind.Unspecified , or DateTimeKind.Local . And .Local is only meaningful in scenarios where you have an implied understanding of where the computer that is using the result is positioned. (For example, a user’s workstation)
So then, why DateTimeOffset instead of a UTC DateTime ? It’s all about perspective. Let’s use an analogy — we’ll pretend to be photographers.
Imagine you are standing on a calendar timeline, pointing a camera at a person on the instantaneous timeline laid out in front of you. You line up your camera according to the rules of your timezone — which change periodically due to daylight saving time, or due to other changes to the legal definition of your time zone. (You don’t have a steady hand, so your camera is shaky.)
The person standing in the photo would see the angle at which your camera came from. If others were taking pictures, they could be from different angles. This is what the Offset part of the DateTimeOffset represents.
So if you label your camera «Eastern Time», sometimes you are pointing from -5, and sometimes you are pointing from -4. There are cameras all over the world, all labeled different things, and all pointing at the same instantaneous timeline from different angles. Some of them are right next to (or on top of) each other, so just knowing the offset isn’t enough to determine which timezone the time is related to.
And what about UTC? Well, it’s the one camera out there that is guaranteed to have a steady hand. It’s on a tripod, firmly anchored into the ground. It’s not going anywhere. We call its angle of perspective the zero offset.
So — what does this analogy tell us? It provides some intuitive guidelines-
- If you are representing time relative to some place in particular, represent it in calendar time with a DateTime . Just be sure you don’t ever confuse one calendar with another. Unspecified should be your assumption. Local is only useful coming from DateTime.Now . For example, I might get DateTime.Now and save it in a database — but when I retrieve it, I have to assume that it is Unspecified . I can’t rely that my local calendar is the same calendar that it was originally taken from.
- If you must always be certain of the moment, make sure you are representing instantaneous time. Use DateTimeOffset to enforce it, or use UTC DateTime by convention.
- If you need to track a moment of instantaneous time, but you want to also know «What time did the user think it was on their local calendar?» — then you must use a DateTimeOffset . This is very important for timekeeping systems, for example — both for technical and legal concerns.
- If you ever need to modify a previously recorded DateTimeOffset — you don’t have enough information in the offset alone to ensure that the new offset is still relevant for the user. You must also store a timezone identifier (think — I need the name of that camera so I can take a new picture even if the position has changed). It should also be pointed out that Noda Time has a representation called ZonedDateTime for this, while the .Net base class library does not have anything similar. You would need to store both a DateTimeOffset and a TimeZoneInfo.Id value.
- Occasionally, you will want to represent a calendar time that is local to «whomever is looking at it». For example, when defining what today means. Today is always midnight to midnight, but these represent a near-infinite number of overlapping ranges on the instantaneous timeline. (In practice we have a finite number of timezones, but you can express offsets down to the tick) So in these situations, make sure you understand how to either limit the «who’s asking?» question down to a single time zone, or deal with translating them back to instantaneous time as appropriate.
Here are a few other little bits about DateTimeOffset that back up this analogy, and some tips for keeping it straight:
- If you compare two DateTimeOffset values, they are first normalized to zero offset before comparing. In other words, 2012-01-01T00:00:00+00:00 and 2012-01-01T02:00:00+02:00 refer to the same instantaneous moment, and are therefore equivalent.
- If you are doing any unit testing and need to be certain of the offset, test both the DateTimeOffset value, and the .Offset property separately.
- There is a one-way implicit conversion built in to the .Net framework that lets you pass a DateTime into any DateTimeOffset parameter or variable. When doing so, the .Kind matters. If you pass a UTC kind, it will carry in with a zero offset, but if you pass either .Local or .Unspecified , it will assume to be local. The framework is basically saying, «Well, you asked me to convert calendar time to instantaneous time, but I have no idea where this came from, so I’m just going to use the local calendar.» This is a huge gotcha if you load up an unspecified DateTime on a computer with a different timezone. (IMHO — that should throw an exception — but it doesn’t.)
Shameless Plug:
Many people have shared with me that they find this analogy extremely valuable, so I included it in my Pluralsight course, Date and Time Fundamentals. You’ll find a step-by-step walkthrough of the camera analogy in the second module, «Context Matters», in the clip titled «Calendar Time vs. Instantaneous Time».