Часові пояси

Nov 30, 2014 17:48

Я завжди знав що тема часових поясів у програмуванні - це пекельні котли і авгієві конюшні (разом з темою локалізації), тому старався уникати її. Коли знайомі адміни дорікали мені за логи у UTC я казав що Грінвіч рулить (не коньяк) і взагалі у логах які скачуть туди-сюди двічі на рік фіг що знайдеш потім. Плюс там всякі політичні складові - переходити, чи не переходити... Тож уникав я цієї теми довго, поки не стикнувся з податковою службою Грузії. Не вдаючись у подробиці, знадобилось на одному проекті зафігачити кастомні часові пояси для перегляду певної статистики. Ржака в тому, що українські користувачі взагалі не скаржилися на час у UTC (відношення було приблизно таке: „лишь были б желуди, ведь я от них жирею“), російські користувачі трохи скаржились що треба перераховувати локальний час щоб подивитись статистику за добу, а грузини покликали на поміч своїх податківців і категорично відмовились щось там рахувати. Ну в принципі, позиція правильна. Я теж завжди кажу що це задача комп’ютера щось там рахувати - він для того створений був.

Так от. Соромно зізнатись, але веб-інтерфейс статистики написаний не на божественному Haskell а на богомерзькому PHP. Сподіваючись, що у XXI сторіччі мудрі програмісти вже навчились працювати з часовими поясами так щоб не виникала кровотеча із очей, носа і вух, поліз я у документацію... щоб відкрити там безодні пекла у головах цих самих... еее... похапешників. Енджойте:

[Spoiler (click to open)]

DateTimeZone::listAbbreviations
timezone_abbreviations_list

(PHP 5 >= 5.2.0)

DateTimeZone::listAbbreviations -- timezone_abbreviations_list - Returns associative array containing dst, offset and the timezone name
Description

Object oriented style
public static array DateTimeZone::listAbbreviations ( void )

Procedural style
array timezone_abbreviations_list ( void )
Return Values

Returns array on success or FALSE on failure.
Examples

Example #1 A timezone_abbreviations_list() example
php
$timezone_abbreviations = DateTimeZone::listAbbreviations();
print_r($timezone_abbreviations["acst"]);
?

The above example will output something similar to:

Array
(
[0] => Array
(
[dst] => 1
[offset] => -14400
[timezone_id] => America/Porto_Acre
)

[1] => Array
(
[dst] => 1
[offset] => -14400
[timezone_id] => America/Eirunepe
)

[2] => Array
(
[dst] => 1
[offset] => -14400
[timezone_id] => America/Rio_Branco
)

[3] => Array
(
[dst] => 1
[offset] => -14400
[timezone_id] => Brazil/Acre
)

)


Функція повертає асоціативний масив у якому є dst, зміщення і назва часового поясу, шо не ясно?! Нічого що функція насправді повертає асоціативний масив масивів структур. Ага. Але це ще не все. Документація PHP провокує на експериментальне програмування: „ми не скажемо що робить ця функція, сам подивись“. Тому я провів експеримент. Взяв отакий шматочок коду:

date_default_timezone_set("UTC");

$abbs = DateTimeZone::listAbbreviations();
foreach ($abbs as $abb => $data)
foreach ($data as $z)
if ($z['timezone_id'] == 'Europe/Kiev') {
echo "{$abb}:\n\t - dst: {$z['dst']};\n\t - offset: {$z['offset']};\n\t - id: {$z['timezone_id']}.\n";
}

(„Kiev“, хай йому грець!). А тепер вгадайте, скільки записів виведе ця програмка? Правильно, 7!

cest:
- dst: 1;
- offset: 7200;
- id: Europe/Kiev.
cet:
- dst: ;
- offset: 3600;
- id: Europe/Kiev.
eest:
- dst: 1;
- offset: 10800;
- id: Europe/Kiev.
eet:
- dst: ;
- offset: 7200;
- id: Europe/Kiev.
kmt:
- dst: ;
- offset: 7324;
- id: Europe/Kiev.
msd:
- dst: 1;
- offset: 14400;
- id: Europe/Kiev.
msk:
- dst: ;
- offset: 10800;
- id: Europe/Kiev.

WTF? GMT+1, GMT+2, GMT+3, GMT+4, GMT+02:02:04 (sic!)
Як же я матюкався! Тільки подумки. А то шо подумають про мене сусіди? Тим паче коли я сьогодні чемно поміняв лампочку на світильнику у тамбурі, бо, *****, цілий тиждень ніхто не міг лампочку замінити!

Тож я шо подумав: „Ну добре, то похапешники, бідні люди, грішно над ними сміятись. Але ж є ентерпрайзний C++!“. І пішов дивитись що там мудрі програмісти намудрували з цього приводу у, щасругнусь, Boost.

Матір божа (тут, насправді, було інше, лайливе слово)! Вони мені пропонують або визначати часовий пояс рядком у такому форматі:

[Spoiler (click to open)]

A posix_time_zone is unique in that the object is created from a Posix time zone string (IEEE Std 1003.1). A POSIX time zone string takes the form of:

"std offset dst [offset],start[/time],end[/time]" (w/no spaces).

'std' specifies the abbrev of the time zone. 'offset' is the offset from UTC. 'dst' specifies the abbrev of the time zone during daylight savings time. The second offset is how many hours changed during DST. 'start' and 'end' are the dates when DST goes into (and out of) effect. 'offset' takes the form of:

[+|-]hh[:mm[:ss]] {h=0-23, m/s=0-59}

'time' and 'offset' take the same form. 'start' and 'end' can be one of three forms:

Mm.w.d {month=1-12, week=1-5 (5 is always last), day=0-6}
Jn {n=1-365 Feb29 is never counted}
n {n=0-365 Feb29 is counted in leap years}

Exceptions will be thrown under the following conditions:

An exception will be thrown for an invalid date spec (see date class).
A boost::local_time::bad_offset exception will be thrown for:
A DST start or end offset that is negative or more than 24 hours.
A UTC zone that is greater than +14 or less than -12 hours.
A boost::local_time::bad_adjustment exception will be thrown for a DST adjustment that is 24 hours or more (positive or negative)

As stated above, the 'offset' and '/time' portions of the string are not required. If they are not given they default to 01:00 for 'offset', and 02:00 for both occurrences of '/time'.


Або завантажувати її по нормальному імені... із CSV-файлу! Тобто вони надають CSV-файл і пропонують його дистрибуцію разом з програмою... і пофігу на те що завтра Верховна Рада знову вирішить не переходити на літній/зимовий час і хоча локально на сервері все буде добре (якщо адмін адекватний), програмка ваша згниє. Альтернативно можна сконструювати власний часовий пояс in-place. Дуже дякую, жріть самі своє лайно!

Добре, подивимось що вміє pure C. Витримка із man tzset:

[Spoiler (click to open)]

The tzset() function initializes the tzname variable from the TZ environment variable. This function is automatically called by the other time con‐
version functions that depend on the timezone. In a System-V-like environment, it will also set the variables timezone (seconds West of UTC) and
daylight (to 0 if this timezone does not have any daylight saving time rules, or to nonzero if there is a time during the year when daylight saving
time applies).

If the TZ variable does not appear in the environment, the tzname variable is initialized with the best approximation of local wall clock time, as
specified by the tzfile(5)-format file localtime found in the system timezone directory (see below). (One also often sees /etc/localtime used here,
a symlink to the right file in the system timezone directory.)


Okay, пишемо простеньку програмку:

#include
#include

int main()
{
printf("Name: '%s/%s', tz: %d, dst: %d\n", tzname[0], tzname[1], timezone, daylight);
tzset();
printf("Name: '%s/%s', tz: %d, dst: %d\n", tzname[0], tzname[1], timezone, daylight);
return 0;
}

Вгадайте що вона виводить?

Name: 'GMT/GMT', tz: 0, dst: 0
Name: 'EET/EEST', tz: -7200, dst: 1

Вах! Знову бачимо timezone abbreviations, хоча Віка вважає що за використання цих абревіатур у культурному суспільстві треба бити канделябром по пальцям: „Time zones are often represented by abbreviations such as "EST, WST, CST" but these are not part of the international time and date standard ISO 8601 and their use as sole designator for a time zone is not recommended. Such designations can be ambiguous. For example, "ECT", could be interpreted as "Eastern Caribbean Time" (UTC−4h), "Ecuador Time" (UTC−5h) or "European Central Time" (UTC+1h).“.
А знаєте що значить -7200? „seconds West of UTC“!

Є ще такий монстр із 90-х як ICU. Страшна штука. Після знайомства з нею я розумію чому люди плюються на C++. З таким API простіше повіситись. Але там теж є інструменти для роботи з часовими поясами. Давайте спробуємо:

#include
#include
#include
#include

#include

#include

int main()
{
icu::TimeZone* tz = icu::TimeZone::createTimeZone("Europe/Kiev");

icu::UnicodeString name;
std::cout << "name: " << tz->getDisplayName(name) << "\n"
<< "offset: " << tz->getRawOffset() << "\n"
<< "dst: " << (tz->useDaylightTime() ? "yes" : "no") << "\n";

delete tz;
return 0;
}

Після 30 хвилин танців з бубнами щоб його злінкувати з правильними бібліотеками маємо таке:

name: Eastern European Standard Time
offset: 7200000
dst: yes

Ох... Ну це вже щось... Дивимось як воно працює у PHP:

$fmt = new IntlDateFormatter(
'en_US',
IntlDateFormatter::FULL,
IntlDateFormatter::FULL,
'UTC',
IntlDateFormatter::GREGORIAN,
'YYYY-MM-dd HH:mm:ss'
);
echo 'UTC: ' . $fmt->format(time()) . "\n";

$fmt = new IntlDateFormatter(
'en_US',
IntlDateFormatter::FULL,
IntlDateFormatter::FULL,
'Europe/Kiev',
IntlDateFormatter::GREGORIAN,
'YYYY-MM-dd HH:mm:ss'
);
echo 'Europe/Kiev: ' . $fmt->format(time()) . "\n";

Нну... Більш-менш пристойно:

UTC: 2014-11-30 16:46:31
Europe/Kiev: 2014-11-30 18:46:31

Тільки тут та-ж лажа що і з Boost - ICU зберігає всі данні у себе унутрях, підсвічуючи темними ночами неонкою. Такшо якщо завтра ВРУ вирішить пересунутись на GMT+3 - все згниє. Такі справи.

ненависть, locale, php, unicode, cpp, unix epoch, time_t, програмування

Previous post Next post
Up