В вашей системе время играет важную роль? Ваши пользователи/компоненты распределены по территории всего земного шара, или хотя бы нашей необъятной родины? Значит, вам нужны часовые пояса. Что ж, это просто. Самое сложное, что вам придется сделать — не запутаться. Об этом мы с вами и поговорим. Для начала вам нужно научиться правильно думать. Думая правильно, все остальное будет для вас либо самоочевидным, либо достаточно простым.
Начнем с часов. Все мы привыкли определять время, глядя на часы на стене. При работе с часовыми поясами такое время называется Wall clock time. В принципе, ничего плохого в нем нет, только в разных местах земного шара в один и тот же момент времени часы показывают разное время. Если задаться целью, можно придумать алгоритм перевода wall clock time одного часового пояса в wall clock time другого. Обычно надо прибавить/отнять разницу в часах между часовыми поясами, кроме (внимание) моментов перехода на летнее/зимнее время. Вот когда начинается переход, вычисления становятся по-настоящему сложными.
Нам же нужно что-то простое и пуленепробиваемое, как… целое число. Так появилось понятие момента во времени (instant in time, Unix time, POSIX time, time since (unix) epoch), который представляет собой число секунд (в Java — миллисекунд), прошедших с 1 января 1970 года, 00:00:00 по GMT. Момент времени одинаков по всему земному шару — если представить, что в кто-то нажал на «паузу» и течение времени остановилось, число, соответствующее моменту времени по всей Земле будет одно и то же, независимо от часового пояса. Если бы кто-то нажал на паузу через час после того, как на Гринвиче наступил новый 1970 год, момент во времени по всей Земле показывал бы 3 600,000. А сейчас, например, это уже число 1 280 720 431,859.
Итак, момент во времени — это универсальная конвертируемая валюта временных вычислений. Он зависит только от, хм, времени, моменты можно сравнивать (соответственно, определять, какое из событий произошло раньше, а какое позже), и в этом не участвует никакая ерунда, связанная с географическим положением, часовыми поясами и переводами часов, что кардинально повышает надежность таких вычислений. Собственно, так реализована работа со временем в Java (с версии 1.1), где java.util.Date представляет собой обертку над long-моментом во времени (датам ранее 1970 соответствуют отрицательные long-и), является Comparable, а все человеческо-календарные преобразования вынесены в отдельные классы Calendar и DateFormat.
Про преобразования. Обычному человеку мало что скажет число 1 280 720 431,859 (хотя пытливый читатель может вычислить по нему время, когда я писал эти строки), поэтому нужно уметь переводить момент во времени в wall clock time, и, соответственно, парсить обратно wall clock time в момент во времени. Вот для этих преобразований уже требуется знать часовой пояс, и эти вычисления совсем не тривиальные. Дело в том, что в разных странах/территориях/местах мало того, что разное смещение относительно GMT (время по Гринвичу), так еще и правила этих смещений исторически несколько раз менялись и продолжают меняться (вводят/отменяют летнее время, объединяют пояса — слышали про такую инициативу у нас, наверное?, или вспомним, например, мой родной город Новосибирск, который в начале девяностых перенесли из GMT+7 в GMT+6, а в начале века в нем вообще два пояса было — граница пояса проходила по реке Оби, и на разных берегах были разные пояса). Короче, чтобы не сойти с ума, вся эта историческая информация аккуратно ведется в виде базы данных Olson tz database, названной в честь основателя Arthur David Olson, хотя редактором является Paul Eggert. В этой базе данных каждому крупному населенному пункту соответствует код (Новосибирск, например, по этой базе называется Asia/Novosibirsk) и список всех его приключений по часовым поясам, начиная с 1970 года. Эта база используется во многих (всех?) Linux/Unix/BSD-системах, насчет Windows не скажу, в Java Runtime Environment (у нее, например, были какие-то апдейты, связанные исключительно с обновлением tz database), и так далее, см., в общем, Википедию. Алгоритм преобразований времени в/из этой базы мы рассматривать не будем, будем считать, что он есть у нас готовый. Он, собственно, практически везде и есть готовый.
Итак, сформулируем правила обращения со временем для программ, работающих в нескольких часовых поясах:
Можно придумать разные варианты решения этой проблемы — хранить отдельной колонкой признак л/з, хранить моменты во времени в колонке типа NUMBER, но наименее радикальным и простым мне кажется хранение даты/времени в UTC. В часовом поясе UTC нет перехода на летнее/зимнее время, поэтому преобразования wall clock time instant in time всегда выполняются однозначно. Кроме того, что такой подход позволяет надежно хранить все моменты во времени в БД, включая переводы часов, он еще и:
Переводится naive в tz-aware datetime с помощью метода:
(обратите внимание на второй параметр, он нужен как раз из-за неоднозначного преобразования), либо
(перевод tz-aware даты-времени в другой часовой пояс).
Поскольку это все реализуется через один и тот же класс datetime.datetime и вся разница в наличии свойства tzinfo, нужно быть чертовски осторожным, чтобы не перепутать, где у нас даты с часовым поясом, а где нет. Здесь Питон хуже Джавы в том смысле, что в Джаве при распечатывании хочешь-не хочешь, а нужно DateFormat создать и часовой пояс указать, в Питоне же многие операции, в т.ч. и печать, могут и для наивных дат выполняться. Понятно, что в сколь-нибудь сложном приложении желательно позаботиться, чтобы все даты были с часовым поясом, потому что если в каком-то месте приложения окажется, что его нет, то уже фиг вычислишь, а какой он там должен быть. А с поясом и сравниваться даты будут корректно, и распечатываться, и вообще. Кроме того, поскольку при сохранении в/чтении из БД сохраняется только наивная часть (год месяц день час минута секунда микросекунда), единственный толковый способ с этим работать— это иметь в базе наивное представление в UTC.
Правила человека, работающего с календарными датами. Помните, что:
Хотя UTC и GMT очень похожи, но все-таки немного отличаются. Если GMT определяется по среднесолнечному времени в Королевской обсерватории в Гринвиче, то UTC отмеряется атомными часами (средневзвешенное время двухсот атомных часов в семидесяти лабораториях по всему миру, синхронизируемых через спутники). Расхождение GMT и UTC не должно превышать 0,9 секунды и компенсируется как раз добавлением leap seconds.
Ожидается, что хранение даты в 32 signed int в UNIX-системах приведет к проблеме 2038 года, когда 31 бит переполнится и последующим моментам во времени будут соответствовать отрицательные числа, что сломает все методы сравнения. Новые 64-х битные системы и программы уже используют для хранения времени 64 бита, но успеют ли такие системы полностью заменить 32-х разрядные к 2038 году?
Начнем с часов. Все мы привыкли определять время, глядя на часы на стене. При работе с часовыми поясами такое время называется Wall clock time. В принципе, ничего плохого в нем нет, только в разных местах земного шара в один и тот же момент времени часы показывают разное время. Если задаться целью, можно придумать алгоритм перевода wall clock time одного часового пояса в wall clock time другого. Обычно надо прибавить/отнять разницу в часах между часовыми поясами, кроме (внимание) моментов перехода на летнее/зимнее время. Вот когда начинается переход, вычисления становятся по-настоящему сложными.
Нам же нужно что-то простое и пуленепробиваемое, как… целое число. Так появилось понятие момента во времени (instant in time, Unix time, POSIX time, time since (unix) epoch), который представляет собой число секунд (в Java — миллисекунд), прошедших с 1 января 1970 года, 00:00:00 по GMT. Момент времени одинаков по всему земному шару — если представить, что в кто-то нажал на «паузу» и течение времени остановилось, число, соответствующее моменту времени по всей Земле будет одно и то же, независимо от часового пояса. Если бы кто-то нажал на паузу через час после того, как на Гринвиче наступил новый 1970 год, момент во времени по всей Земле показывал бы 3 600,000. А сейчас, например, это уже число 1 280 720 431,859.
Итак, момент во времени — это универсальная конвертируемая валюта временных вычислений. Он зависит только от, хм, времени, моменты можно сравнивать (соответственно, определять, какое из событий произошло раньше, а какое позже), и в этом не участвует никакая ерунда, связанная с географическим положением, часовыми поясами и переводами часов, что кардинально повышает надежность таких вычислений. Собственно, так реализована работа со временем в Java (с версии 1.1), где java.util.Date представляет собой обертку над long-моментом во времени (датам ранее 1970 соответствуют отрицательные long-и), является Comparable, а все человеческо-календарные преобразования вынесены в отдельные классы Calendar и DateFormat.
Про преобразования. Обычному человеку мало что скажет число 1 280 720 431,859 (хотя пытливый читатель может вычислить по нему время, когда я писал эти строки), поэтому нужно уметь переводить момент во времени в wall clock time, и, соответственно, парсить обратно wall clock time в момент во времени. Вот для этих преобразований уже требуется знать часовой пояс, и эти вычисления совсем не тривиальные. Дело в том, что в разных странах/территориях/местах мало того, что разное смещение относительно GMT (время по Гринвичу), так еще и правила этих смещений исторически несколько раз менялись и продолжают меняться (вводят/отменяют летнее время, объединяют пояса — слышали про такую инициативу у нас, наверное?, или вспомним, например, мой родной город Новосибирск, который в начале девяностых перенесли из GMT+7 в GMT+6, а в начале века в нем вообще два пояса было — граница пояса проходила по реке Оби, и на разных берегах были разные пояса). Короче, чтобы не сойти с ума, вся эта историческая информация аккуратно ведется в виде базы данных Olson tz database, названной в честь основателя Arthur David Olson, хотя редактором является Paul Eggert. В этой базе данных каждому крупному населенному пункту соответствует код (Новосибирск, например, по этой базе называется Asia/Novosibirsk) и список всех его приключений по часовым поясам, начиная с 1970 года. Эта база используется во многих (всех?) Linux/Unix/BSD-системах, насчет Windows не скажу, в Java Runtime Environment (у нее, например, были какие-то апдейты, связанные исключительно с обновлением tz database), и так далее, см., в общем, Википедию. Алгоритм преобразований времени в/из этой базы мы рассматривать не будем, будем считать, что он есть у нас готовый. Он, собственно, практически везде и есть готовый.
Итак, сформулируем правила обращения со временем для программ, работающих в нескольких часовых поясах:
- внутри программы работать только с моментами во времени;
- преобразование моментов во времени в wall clock time производить только во время ввода/вывода даты. Помните, что в этом преобразовании всегда (всегда!) участвует часовой пояс, поэтому нужно следить, какой именно (это не всегда видно, потому что по умолчанию берется текущий);
- еще один случай, когда требуется wall clock time, это календарные преобразования (вычислить начало следующего дня и т.п.). Здесь тоже нужно следить, чтобы эти преобразования происходили в правильном часовом поясе;
- при сохранении даты/времени в базу данных делать это в часовом поясе UTC.
Можно придумать разные варианты решения этой проблемы — хранить отдельной колонкой признак л/з, хранить моменты во времени в колонке типа NUMBER, но наименее радикальным и простым мне кажется хранение даты/времени в UTC. В часовом поясе UTC нет перехода на летнее/зимнее время, поэтому преобразования wall clock time instant in time всегда выполняются однозначно. Кроме того, что такой подход позволяет надежно хранить все моменты во времени в БД, включая переводы часов, он еще и:
- дисциплинирует (если вы забудете где-то в преобразованиях указать часовой пояс, то сразу увидите, что что-то не то, по крайней мере, если вы живете не в UTC);
- позволяет не запутаться в датах/временах, когда информация в БД поступает из разных часовых поясов — в базе время всегда в UTC;
- упрощает код, так как при преобразовании времени в/из БД можно не думать о часовом поясе, он всегда один и тот же.
Переводится naive в tz-aware datetime с помощью метода:
tzaware_datetime = some_timezone.localize(some_naive_datetime, is_dst=True)
(обратите внимание на второй параметр, он нужен как раз из-за неоднозначного преобразования), либо
another_tzaware_datetime = tzaware_datetime.astimezone(another_tz)
(перевод tz-aware даты-времени в другой часовой пояс).
Поскольку это все реализуется через один и тот же класс datetime.datetime и вся разница в наличии свойства tzinfo, нужно быть чертовски осторожным, чтобы не перепутать, где у нас даты с часовым поясом, а где нет. Здесь Питон хуже Джавы в том смысле, что в Джаве при распечатывании хочешь-не хочешь, а нужно DateFormat создать и часовой пояс указать, в Питоне же многие операции, в т.ч. и печать, могут и для наивных дат выполняться. Понятно, что в сколь-нибудь сложном приложении желательно позаботиться, чтобы все даты были с часовым поясом, потому что если в каком-то месте приложения окажется, что его нет, то уже фиг вычислишь, а какой он там должен быть. А с поясом и сравниваться даты будут корректно, и распечатываться, и вообще. Кроме того, поскольку при сохранении в/чтении из БД сохраняется только наивная часть (год месяц день час минута секунда микросекунда), единственный толковый способ с этим работать— это иметь в базе наивное представление в UTC.
Бонусы
Правила человека, работающего с календарными датами. Помните, что:
- не в каждом году 365 дней;
- не в каждом дне 24 часа;
- к счастью, в каждом часе 60 минут;
- не в каждой минуте 60 секунд (может оказаться 59 и 61. 61-ая называется leap second, добавляется либо 30 июня, либо 31 декабря, в это время часы в UTC должны показывать 23:59:60. Добавление 61-ой секунды вызвано замедляющимся вращением Земли. Возможность отнять одну секунду предусмотрена для случаев, если Земля начнет вращаться быстрее, но эта возможность еще ни разу не потребовалась).
Хотя UTC и GMT очень похожи, но все-таки немного отличаются. Если GMT определяется по среднесолнечному времени в Королевской обсерватории в Гринвиче, то UTC отмеряется атомными часами (средневзвешенное время двухсот атомных часов в семидесяти лабораториях по всему миру, синхронизируемых через спутники). Расхождение GMT и UTC не должно превышать 0,9 секунды и компенсируется как раз добавлением leap seconds.
Ожидается, что хранение даты в 32 signed int в UNIX-системах приведет к проблеме 2038 года, когда 31 бит переполнится и последующим моментам во времени будут соответствовать отрицательные числа, что сломает все методы сравнения. Новые 64-х битные системы и программы уже используют для хранения времени 64 бита, но успеют ли такие системы полностью заменить 32-х разрядные к 2038 году?