Если ваше приложение зависит не только от локального времени, но и от его представлении в других часовых поясах, вы наверняка сталкивались со сложностью представления времени в разных временных зонах. Не сталкивались? Значит вы не портировали своё приложение в мир Unix.
Действительно, в ОС Windows для работы с временными зонами программисту предоставляется удобный набор специализированных функций WinAPI. Примером могут служить структура TIME_ZONE_INFORMATION и функция GetTimeZoneInformation к ней в придачу.
Но что делать, если вам необходимо знать смещение относительно UTC+0, правила перехода на «летнее время», учитывать при этом високосные годы с високосными секундами и прочую специфическую информацию для какого-нибудь региона, да всё это в unix-подобных операционных системах? Статья посвящена практике работы со всем этим барахлом на языке C/C++.
Эта тема неоднократно освещалась во многих статьях с разных точек зрения, но редко с практической на примерах конкретных языков, систем и технологий. Примеры можно найти на Stack Overflow (коих вопросов там огромное множество), да и на Хабре эту тему затрагивали достаточно глубоко, но с теоретической точки зрения. Кроме того, есть даже мини-исследование на тему локального времени, из которого можно почерпнуть, что проблема компьютерного времени совсем не тривиальна, какой кажется на первый взгляд. Проведя собственное расследование, я хотел бы кратко и доходчиво поделиться полученным немалым количеством сведений, оформив их в виде способов преобразования времени в разных часовых поясах.
Классика: стандартная библиотека языка C
Да-да, с помощью того мизера функций, определенного в заголовке time.h
, тоже можно выполнять преобразования времени. С определенными ограничениями.
Дело в том, что все локаль-зависимые функции (как то gmtime()
или localtime()
) для определения параметров локали используют переменную окружения TZ
. А это значит, что для конвертирования времени в нужную временную зону необходимо сначала установить эту переменную (с названием необходимой зоны), вызвать функцию преобразования, а затем снова убрать TZ
из окружения. Всё бы ничего, если бы не многозадачность и многопоточность. Естественно, такой способ может вызывать конфликты и приводить к появлению трудно предсказуемых ошибок.
Пример кода, использующего такой подход:
putenv("TZ=Asia/Calcutta"); /// будем преобразовывать ко времени Калькутты
tzset(); /// инициализируем данные зоны
time_t timeToConvert = time(0); /// будем конвертировать это время
struct tm *pCalcuttasTime; /// и сохранять локальное время здесь
pCalcuttasTime = localtime(&timeToConvert); /// конвертируем
putenv("TZ="); /// удаляем переменную TZ
/// ... работаем с полученным временем ...
tz database, или жонглирование байтами
Использование в своих целях базы данных Олсона — наиболее предпочтительный вариант. Плюсы очевидны: база наиболее полным образом отражает все мыслимые правила переходов для любого уголка Земли (учитывая изменения в этих правилах с начала прошлого века), распространяется со многими системами (см. /usr/share/zoneinfo
) и имеет унифицированный формат, при этом база обновляется вместе с системой. Однако, попробовав поработать с ней, я решил отказаться и от этого варианта.
База распространяется в бинарном формате (для этого используется компилятор zic
). Описание формата можно найти в заголовочном файле tzfile.h
(для его поиска воспользуйтесь официальным FTP базы). Инструментов для работы с базой я так и не нашел (возможно, плохо искал?). Но попробовав прочитать файлик нужной временной зоны, я столкнулся с проблемой интерпретации данных — во всех этих тонкостях и терминологии можно голову свернуть, забыв о цели всего этого копания. И, чтобы абстрагироваться от подобных мелочей, было решено пользоваться наиболее адекватным и удобным инструментом.
Boost.Date_Time
Как это часто бывает в подобных ситуациях, на помощь приходит именно boost. О широких возможностях набора библиотек Date_Time уже была статья, содержащая краткий перевод официальной документации. Кстати, хорошая новость для тех, кто не хочет вводить лишние зависимости в свой проект — библиотека является header-only (за исключением пары специфичных мест вроде создания объекта временной метки из строки определенного формата).
Для решения вопроса есть два пути: записывать правила для нужной временной зоны хардкодом в программе (и потом ненавидеть себя за это), либо хранить все правила в специальном файле CSV-формата. Такой файлик можно впоследствии автоматически обновлять (и поддерживать правила переходов в актуальном состоянии, что чрезвычайно важно). Файл распространяется с дистрибутивом boost’а (носит название date_time_zonespec.csv
), но может быть взят и из других мест. Плюс использования файла, кроме прочего, — в нём хранятся правила для всех регионов.
Без минусов тоже не обойдется. Что, если вам понадобится конвертировать ту временную метку, которая находится где-нибудь в начале двадцатого столетия, когда правила перехода были иными? Такие случаи тоже придется учитывать, и, к сожалению, boost здесь может мало помочь.
Для примера приведу код, использующий для конвертирования времени возможности набора библиотек Date_Time.
#include "boost/date_time/local_time/local_time.hpp"
#include "boost/date_time/posix_time/posix_time.hpp"
using namespace boost::local_time;
using namespace boost::posix_time;
ptime convertUTC0toCustomTimeZone(const ptime &utcTime, const std::string &tzName)
{
/// загружаем правила из файла
tz_database tzDB;
tzDB.load_from_file("./date_time_zonespec.csv");
/// создаем необходимую зону
time_zone_ptr timeZone = tzDB.time_zone_from_region(tzName); /// tzName в формате "Asia/Calcutta"
/// создаем необходимую локальную дату
local_date_time localTime(utcTime, timeZone);
/// ...и получаем локальное время
return localTime.local_time();
}
Итоги
Я предпочел использовать вариант с boost’ом. Конечно, использование tz database избавит вас от головомойки с поддержанием информации о часовых поясах в актуальном состоянии или при конвертировании временной метки, правила переходов которой были изменены. Но в большинстве приложений такие выкрутасы ни к чему, да и boost позволяет работать с датами чрезвычайно удобно.
Автор: injecto