Практика работы со временем в разных часовых поясах в unix-like системах

в 9:16, , рубрики: boost, c/c++, c++, UNIX, время, метки: , , , ,

Если ваше приложение зависит не только от локального времени, но и от его представлении в других часовых поясах, вы наверняка сталкивались со сложностью представления времени в разных временных зонах. Не сталкивались? Значит вы не портировали своё приложение в мир 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


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js