Часовые пояса представляют собой довольно запутанную концепцию, но не безнадёжно. И далее я опишу точную принципиальную модель, которая вам понадобится для их понимания.
Обсуждение часовых поясов, как правило, заключается в перечислении всех заблуждений, которые о них имеют программисты. Но подобные списки не несут особой пользы, так как сложно докопаться до истины, оперируя лишь общим представлением о некотором заблуждении.
Так что я предлагаю другой подход. В этой статье я покажу вам несколько странных часовых поясов, можно даже сказать самых странных — так как страннее, пожалуй, уже некуда.
Asia/Kathmandu
имеет неестественное отклонение от UTC.Africa/Casablanca
не встраивается точно в модель часовых поясов, поэтому прописывается жёстко.America/Nuuk
выполняет переход на летнее время в -01:00 (да-да, в отрицательное время).- В
Africa/Cairo
иAmerica/Santiago
переход производят в 24:00 часа (а не в 0:00). Australia/Lord_Howe
, с населением 382 человека и вот такими древесными омарами, использует самое странное правило перехода на летнее время.
Чтобы понять, как странность этих регионов представляется в ПО, мы разберём базовые файлы часовых поясов, на которые в конечном итоге опирается любое программное обеспечение. Это прояснит для нас два момента:
- Да, это реально странно.
- Но не безнадёжно, поскольку в итоге компьютеру всё же нужно их реализовать.
Начать же я предлагаю с темы календарей.
▍ PGXIIREAM: вокруг меня всем правит римский папа Григорий XIII
Если только вы не собираетесь создавать нечто реально экзотическое со словами вроде:
Ах, да. Оптическое распознающее устройство (OCR) считывает с японских водительских прав информацию в виде, например, «平成 8». Так они иногда выражают 1996. Поэтому в парсере мы прописываем подобный код:
eras = { "大正": 1912, "昭和": 1926, "平成": 1989 }
Для одного из таких дней нам нужно будет добавить
"令和": 2019
, но он ещё не наступил.
Или:
Нам нужно будет настроить для каждой страны фича-флаг, определяющий, закрыты ли банки по случаю празднования Ураза-Байрама. При этом Саудовская Аравия и Иран расходятся в том, когда начинается лунный месяц.
Тогда, естественно, вам потребуется писать ПО, разбирающееся в японской и исламской календарных системах.
Но подобных случаев очень мало. В мире доминирует западная система времяисчисления, поэтому даже в той же Японии или мусульманском мире практически все, кто пользуется компьютерами, знакомы с григорианской системой.
С помощью компьютеров мы проецируем григорианскую систему в прошлое, называя это пролептическим григорианским календарём, который не является исторически точным, но волнует это, разве что, исследователей русской революции.
Эта календарная система достаточно хороша, и если не произойдёт никаких масштабных рационалистических переворотов, то она останется с нами надолго. Григорианский календарь хорош тем, что при его использовании Солнце с течением лет продолжает в одно и то же время оставаться в одном и том же месте небосвода. Он исключает блуждание месяцев между разными сезонами, как это происходило при римском календаре.
Технически такое «примерное сохранение положения солнца в одном месте в одно и то же время дня» называется «средним солнечным временем». Эта же идея лежит в основе выражения Greenwich Mean Time (GMT, среднее гринвичское время), которое подразумевает среднее солнечное время королевской обсерватории в Гринвиче.
Кстати, технически мы больше не называем это время GMT. Если только речь не идёт о времени, которое называют люди в Лондоне, то наверняка имеется в виду UTC.
Coordinated Universal Time — это, по сути, просто современная формализация GMT. Оно полезно, потому что почти все на нашей планете согласились заложить в основу расчёта времени своих часов смещение относительно UTC. Это по-прежнему среднее солнечное время, но связи с Гринвичем здесь уже нет.
Я заговорил обо всём этом, так как вы могли слышать о новомодном чудачестве, связанном с идеей папы Григория о необходимости следить за движением Солнца:
▍ Високосные секунды неважны
Вращение Земли замедляется. Дни становятся длиннее. Поэтому, если мы хотим сохранять синхронность реальных дней с компьютерными, нам необходимо вносить соответствующие корректировки.
Эта задача была поручена учёным из Международной службы вращения Земли (International Earth Rotation and Reference Systems Service, IERS), перед которыми стоят две основные цели:
- Наблюдать за вращением Земли и составлять отчёты.
- Ломать CSS-код Википедии своим длинным названием.
Полиция времени
Если дни удлиняются и делают это с непредсказуемой скоростью, то проще всего, если IERS будет периодически просто добавлять ко дню дополнительную секунду, тем самым замедляя часы. Эта секунда называется високосной.
Но вам не следует принимать этот факт. Да, он представляет собой новую крутую тенденцию, но она является лишь незначительной деталью, которую можно полностью игнорировать. И вот почему:
- Языки программирования всё равно не поддерживают возможность выражать минуты как состоящие из 61 секунды.
- Вы (и под вами я имею в виду вашего облачного провайдера) можете просто замедлить свои часы на время високосной секунды и сообщить всем остальным через NTP (Networking Time Protocol), что их часы спешат. Такой подход называется «размазыванием» високосной секунды.
Кстати, аббревиатура UTC (Universal Time Coordinated? Серьёзно?*) получилась из-за того, что устанавливающие это время люди также устанавливают время UT1, представляющее UTC без високосных секунд. До появления координированного (coordinated) варианта были и другие версии UT.
*Прим. пер.: здесь автор как бы посмеивается над очерёдностью слов в выражении Universal Time Coordinated, которая с точки зрения грамматики английского языка является ошибочной. Правильно это выражение звучит как Coordinated Universal Time.
▍ Необычные часовые пояса
Хорошо, перейдём к знакомству с необычными часовыми поясами и разберёмся, каким образом ваш компьютер понимает, как их правильно представлять.
Asia/Kathmandu
имеет необычное временно́е смещение
Большинство регионов планеты смещены от UTC на целое количество часов. При этом около 1/5 населения всего мира живёт по времени, отличающемуся от UTC на дробное количество часов. В частности, Индия опережает UTC на 5 ч. 30 м.
Непал опережает UTC на 5 ч. 45 м.:
$ TZ=UTC date ; TZ=Asia/Kathmandu date
Tue Jul 30 23:52:11 UTC 2024
Wed Jul 31 05:37:11 +0545 2024
Если вы мыслите как я, то наверняка задумывались, откуда компьютер может знать все эти факты.
Вот вам подсказка:
$ TZ=Asia/Kathmandu strace -e trace=openat date
...
openat(AT_FDCWD, "/usr/share/zoneinfo/Asia/Kathmandu", O_RDONLY|O_CLOEXEC) = 3
Wed Jul 31 05:40:49 +0545 2024
У вас в файловой системе есть база данных под названием IANA Timezone Database, она же tzdb или zoneinfo. По факту она представляет кучу исполняемых файлов, закодированных в Time Zone Information Format (TZIF). Имена этих файлов выступают в качестве идентификаторов часовых поясов, и в них вы можете найти строки вроде America/Los_Angeles
или Europe/London
:
$ tree /usr/share/zoneinfo
...
├── America
│ ├── Los_Angeles
├── Europe
│ ├── London
...
В самом же конце /usr/share/zoneinfo/Asia/Kathmandu
находится вот такая мини-строка:
cat /usr/share/zoneinfo/Asia/Kathmandu
...
<+0545>-5:45
Синтаксис здесь весьма непонятный, но означает эта запись следующее:
UTC отстаёт от этого часового пояса на 5 ч. 45 м., если не указано иное. Называйте это время
+0545
.
Именно так программное обеспечение определяет время в Непале. И по той же причине вывод date
выше содержит +0545
.
Почему строки вроде PDT
или CET
практически бессмысленны
В примере выше +0545
называется «определителем». Эта строка описывает, в какой части часового пояса находится временна́я метка. Она предназначена для вывода временны́х меток и становится однозначно понятной, только когда вы уже знаете, из какого часового пояса временна́я метка была взята.
А насколько эти определители могут быть неопределёнными? Я написал скрипт tzdump
, преобразующий файлы TZIF в JSON.
find -L /usr/share/zoneinfo -type f
| xargs -n1 ./tzdump
| jq -r '"(.ID)t(.Transitions[].LocalTimeType.Designation)"'
| sort | uniq | sort -k 2 | uniq -f 1 -c | sort -n | awk '{ print $1 "t" $3 }' | tail -r
И вот самые популярные определители:
66 CST
58 CDT
56 CET
56 CEST
Лидирует определитель CST
, который использовали в прошлом или используют в настоящем 66 часовых поясов. При этом многие часовые пояса функционально являются точными клонами друг друга — к примеру, нет отличия между America/Phoenix
и America/Creston
, но каждому из них всё равно посвящается отдельный файл. Во всём этом много неопределённости.
Кому интересно, лишь 33 определителя являются уникальными для часовых поясов. Намного большее их число уникально чисто функционально, но я слишком ленив, чтобы отфильтровывать дубликаты.
И ещё одна занятна мелочь: определители не обязательно должны быть представлены с использованием только верхнего регистра и чисел. Например, ChST
, относящийся к Pacific/Saipan
, означает Chamorro Standard Time. Это единственный определитель с именем, содержащим нижний регистр. К сожалению тех из нас, кто любит баги, CHST
не используется.
Как выражаются часовые пояса с переходом на летнее время?
Когда мы рассматривали Катманду, то на правила определения времени в Непале указывала эта строка:
<+0545>-5:45
Хорошо, тут всё довольно просто. Но что насчёт часовых поясов с переходом на летнее время? В их синтаксисе обычно много предустановленных значений (переход на летнее время подразумевает перевод часов на один час и происходит обычно в 2:00), но Europe/Athens
является хорошим примером, где используется бо́льшая часть возможного синтаксиса:
$ cat /usr/share/zoneinfo/Europe/Athens
...
EET-2EEST,M3.5.0/3,M10.5.0/4
Этот код следует читать так (перевод сделан с учётом сохранения последовательности значений в строке, — прим. пер.):
Стандартное время называется EET
, и оно на 2 часа опережает UTC. DST (daylight savings time, летнее время) называется EEST
(опережает UTC на 3 часа и подразумевается в качестве дефолта относительно стандартного времени). Начинается DST в 3
-м месяце в последнее (5
) воскресенье (0
) в 3:00 по местному времени (/3
). Завершается DST в 10
-м месяце также в последнее воскресенье, но уже в 4:00 по местному времени (5.0/4
).
Так что да-а-а, ваш компьютер обрабатывает немало витиеватой логики, чтобы определить, какой дате и времени соответствует та или иная временна́я метка, после чего выясняет, находится ли она в зоне с DST, чтобы уже точно определить местное время. Шикарно.
Кстати, кому интересно: согласно спецификации, «5» означает «последняя», а «1» — «первая» неделя, но при этом используются только недели «1», «2» и «5»:
$ find -L /usr/share/zoneinfo -type f | xargs -n1 ./tzdump | jq -r 'if .Rules.DST == null then empty else "(.ID)t(.Rules.DST.Week)" end' | sort -k2 | uniq -f 1 -c | awk '{ print $1 "t" $3 }'
18 1
89 2
81 5
А вот забавный поворот: на моём Mac все часовые пояса либо не имеют DST вообще, либо используют для перехода на летнее время обозначенные правила с подсчётом недель/дней. Но внутри /var/db/timezone
есть другие версии tzdb, среди которых есть одна, которая содержит иные виды часовых поясов:
$ cat /var/db/timezone/tz/2024a.1.0/zoneinfo/Africa/Casablanca
...
XXX-2<+01>-1,0/0,J365/23
Этот часовой пояс, по сути, говорит: «Мы постоянно находимся в режиме летнего времени», поскольку синтаксис J###
означает «###
-ый день года без учёта 29 февраля, если таковой в этот год присутствует» (J
означает «Julian calendar»).
Технически этот часовой пояс также использует для указания дней синтаксис без приставок (то есть без M
или J
), в котором ###
означает «###
-ый день года с учётом 29 февраля». Но в данном случае это чисто условное различие.
Вся эта информация берётся из POSIX. Документация GNU, описывающая переменную среды tz
— на которую опирается TZIF — является лучшим онлайн-ресурсом по этой теме из мне известных.
Но это лишь начало той странности, которую демонстрирует Africa/Casablanca
.
Africa/Casablanca
и Asia/Gaza
ориентируются на Луну в то время, как часовые пояса — на Солнце
Формат TZIF поддерживает три вида правил для принятия решения о том, в какой день переходить на летнее время:
- Правила в стиле «первый вторник марта».
- Правила в стиле «45-ый день года».
- Правила в стиле «45-ый день года без учёта 29 февраля».
В Марокко и Газе при переходе на летнее время ориентируются на Рамадан. Рамадан длится один месяц по исламскому календарю, который основан на циклах Луны. Этот лунный календарь не является точно кратным солнечному. С григорианской перспективы Луна вращается вокруг Земли медленнее, в результате чего при делении на 12 месяцев в остатке получается больше дней, чем в случае солнечного календаря. И здесь для наших героев из tzdb возникает проблема.
Каким же образом она решается? Да самым примитивным.
Файл TZIF оканчивается синтаксической конструкцией, о которой мы говорили выше. Начинается же он с длинного списка исторических данных о переводе времени в этом часовом поясе. Если страна когда-либо поменяет правила вычисления времени в своём регионе, они окажутся отражены там же в футере файла TZIF вместе с жёстко прописанными данными о совершённых ранее переходах.
Но вы можете просто взять этот список прописанных переходов, которые преобладают над футером, и спроецировать их в будущее. В итоге программисты, работающие с TZIF, сделали следующее:
- Выбрали достаточно отдалённый год в будущем (2086).
- Написали на Emacs Lisp скрипт для вычисления периодов празднования Рамадана.
- Использовали вывод этого скрипта для генерации переходов на летнее время в Марокко и Газе.
И именно поэтому информация по Марокко и Газе, в отличие от всех других часовых поясов, просто прописана в tzdb жёстко.
Если вы вдруг надеетесь, что есть ещё подобные весёлые часовые пояса, то вам не повезло. Остальные представители составленного мной списка, который не учитывает переходы на летнее время после 2025 года, просто аналогичны Касабланке и Газе.
$ find -L /var/db/timezone/tz/2024a.1.0/zoneinfo/ -type f | xargs -n1 ./tzdump | jq 'select(.Transitions[].TransitionTime > 1735689600) | .ID' -r | uniq -c | sort -n
26 /var/db/timezone/tz/2024a.1.0/zoneinfo//Africa/Cairo
...
26 /var/db/timezone/tz/2024a.1.0/zoneinfo//US/Pacific
26 /var/db/timezone/tz/2024a.1.0/zoneinfo//WET
26 /var/db/timezone/tz/2024a.1.0/zoneinfo//posixrules
130 /var/db/timezone/tz/2024a.1.0/zoneinfo//Africa/Casablanca
130 /var/db/timezone/tz/2024a.1.0/zoneinfo//Africa/El_Aaiun
184 /var/db/timezone/tz/2024a.1.0/zoneinfo//Asia/Gaza
184 /var/db/timezone/tz/2024a.1.0/zoneinfo//Asia/Hebron
Похоже, что каждый второй часовой пояс после 2025 года имеет всего 26 переходов. Думаю, это сделано лишь для того, чтобы ПО, которое ничего не знает о прописанных в футере TZIF правилах перехода, в любом случае сохраняло точность в течение нескольких лет в будущем.
America/Nuuk
переходит на летнее время в -1:00
Нуук находится в Гренландии и является частью обширной киновселенной Евросоюза.
Вся Европа (не знаю, как именно эту область правильно обозначить: EU/EEZ/EFTA/CoE) синхронизирует свои переходы на летнее время, за исключением Исландии, где этот переход вообще не производят (Atlantic/Reykjavik
, который технически является «синонимом» для Africa/Abidjan
, и в их строке правила указано просто GMT0
).
Большинству европейцев знакомо три основных часовых пояса, которые мы можем назвать Europe/Lisbon
(западная часть), Europe/Brussels
(центральная часть) и Europe/Athens
(восточная часть). Каждый из этих поясов на один час опережает предыдущий, и их переходы выглядят так:
# Я использую дополнительные пробелы, чтобы подчеркнуть симметричность, а также пропишу скрытую «/2»
Europe/Lisbon: WET0WEST ,M3.5.0/1,M10.5.0/2
Europe/Brussels: CET-1CEST,M3.5.0/2,M10.5.0/3
Europe/Athens: EET-2EEST,M3.5.0/3,M10.5.0/4
Иными словами, Лиссабон переходит на летнее время в 1:00, Брюссель следует за ним в 2:00, а Афины догоняют их в 3:00. Но всё это время местное — в реальности переходы происходят одновременно.
И это вполне разумно. Такая система отсчёта времени, когда его отличие в любых двух точках Европы всегда одинаково, удобна для бизнеса.
И Гренландия не прочь стать частью этой системы, но загвоздка в том, что эта страна расположена довольно далеко на Западе континентальной Европы. Если в Лиссабоне стандартное время равно UTC, то Гренландия отстаёт от UTC на 3 часа. Вот их правила перехода на летнее время:
$ cat /var/db/timezone/tz/2024a.1.0/zoneinfo/America/Nuuk
<-02>2<-01>,M3.5.0/-1,M10.5.0/0
Обратите внимание на M3.5.0/-1
. Первая часть — это стандартный день перехода на летнее время в Европе. А что означает /-1
? Эта часть означает, что Гренландия производит этот переход не в 2:00 (/2
), а в -1:00 (/-1
). Согласно прописанным в файле правилам, переход должен происходить в воскресенье, но по факту происходит в 23:00 субботы. Супер странно.
Думаю, что это ломает ПО, так как America/Nuuk
и его «синонимы» относятся к тем часовым поясам, чьи правила перехода на летнее время в /usr/share/zoneinfo
на моём Mac полностью пропущены и доступны только в других копиях tzdb в /var/db/timezone
.
America/Santiago
и Africa/Cairo
переводят часы в 24:00
Нуук производит переход самым первым, а Сантьяго и Каир последними. Оба этих региона переходят на летнее время в 24:00? То есть имеется в виду следующий день?
America/Santiago: <-04>4<-03>,M9.1.6/24,M4.1.6/24
Africa/Cairo: EET-2EEST,M4.5.5/0,M10.5.4/24
Думаю, что оба этих часовых пояса прописаны таким образом в связи со странностью правил, определяемых их правительствами. Например, M10.5.4/24
должно означать «последний четверг октября, 24:00», а по факту означает «день после последнего четверга октября». Причём это не то же самое, что «последняя пятница октября», так как месяц может заканчиваться в четверг.
Оба этих файла также находятся на Mac в списке капризных часовых поясов, которые не попадают в /usr/share/zoneinfo
.
▍ Australia/Lord_Howe
практикует самый необычный переход на летнее время
Когда вы переходите на летнее время, то либо «шагаете вперёд», либо «отступаете назад». Естественно, все согласятся, что это подразумевает сдвиг на один час?
А вот вам скрипт, который проверяет «Каково будет отличие между стандартным и летним временем в каждом часовом поясе?»
$ find -L /usr/share/zoneinfo -type f | xargs -n1 ./tzdump | jq 'if .Rules.DST == null then "(.ID)t0" else "(.ID)t(.Rules.DST.LocalTimeType.UTCOffsetSeconds - .Rules.Std.LocalTimeType.UTCOffsetSeconds)" end' -r | sort -n -k 2 | uniq -c -f 1 | awk '{ print $1 "t" $3 }'
410 0
2 1800
185 3600
1 7200
Хмм… В 410 часовых поясах переход на летнее время вообще не практикуется. В 185 разница составляет 3 600 секунд, то есть один час. Но есть и особые случаи.
Например, в Antarctica/Troll
сдвиг происходит на 7 200 секунд, то есть на 2 часа. Такая вот корректировка.
<+00>0<+02>-2,M3.5.0/1,M10.5.0/3
Поэтому в течение зимы (то есть северного лета) они используют норвежское время? Но в Тролле зимой живёт сколько, 6 человек? Действительно ли этим шести душам так важен их вклад в эзотерические таинства мира программного обеспечения? Надеюсь, что да. Очевидно, что на практике они в течение года сдвигают часы четыре раза, но в коде нет синтаксиса, чтобы это выразить.
Хорошо, но настоящий вопрос в том, как обстоят дела с двумя результатами по 1 800 секунд. Это синонимичные случаи, касающиеся часового пояса Australia/Lord_Howe
, где происходит мощный 30-минутный переход на летнее время:
<+1030>-10:30<+11>-11,M10.1.0,M4.1.0
Опережая UTC на 10 ч. 30 м., они переходят на летнее время в 11:00. Могу лишь порадоваться за них. При такой системе ежечасное выполнение задач Cron ведёт к очень неудобной координации с местным временем. Во всех других точках планеты каждые 60 минут стрелки ваших часов оказываются в одном и том же месте циферблата.
За исключением острова Лорд-Хау. Что сказать — герои! Здесь в первое воскресенье октября, 60-минутный интервал сдвигает часы лишь на 30 минут. Теперь все ваши задачи Cron оказались смещены относительно местных настенных часов.
Если кому любопытно, этот остров принадлежит Австралии, и по данным последней переписи проживает на нём 382 человека. В какой-то степени этот уголок планеты можно назвать природным раем, с целью сохранения которого на остров допускается не более 400 туристов одновременно.
Пожалуй, самой известной особенностью Лорда-Хау является вулканический остров-сосед Болс-Пирамид.
Болс-Пирамид как памятник найденным на нём древесным омарам (dryococelus australis), которые считались вымершим видом насекомых, а также программным инженерам, которые пишут код для обработки часовых поясов.
По факту это старый потухший вулкан, который выглядит очень впечатляюще.
▍ Основные выводы
Часовые пояса — запутанная тема, но не безнадёжно. Состоят они из:
- ID, например,
America/Los_Angeles
. - Набора жёстко прописанных переходов, которые простираются из прошлого в будущее.
- Набора правил для выполнения будущих переходов на летнее время.
Любое отдельное взятое время в часовом поясе — это всего лишь:
- Смещение относительно UTC.
- С «определяющим» временем, которое не имеет особого значения.
- Плюс фактор того, является ли оно летним, хотя эта информация обычно нигде не выводится.
Вы всегда можете уникально определить, какое время UTC человек имеет в виду, если он сообщит вам свой часовой пояс + местное время + текущий определитель времени. Часовой пояс + определитель дают вам смещение, которое вы можете применить к местному времени, чтобы получить UTC.
И да, это странно, запутанно, но всё же не столь ужасно.
Добавлю:
- Не ведитесь на чужие рассуждения в духе «Если что-то очень сложно, то значит невозможно».
- Ведь практически каждый стандарт (за исключением, пожалуй, ISO8601) — это просто файл, который вы можете прочесть. Вы грамотны. Вы это реально можете. Вникните в странность системы перехода на летнее время в Гренландии. Верьте в себя!
- Если бы я был генеральным секретарём ООН, то исключал бы любые страны, которые, на мой взгляд, недостаточно уважительно относятся ко времени Пола Эггерта.
▍ Дополнение: прочие странности в zoneinfo
Честно говоря, в zoneinfo есть кое-что, с чем я не могу разобраться, так как даже у моего ботанского ума есть пределы. Пусть это будет задачкой для читателей.
Перечисленные ниже часовые пояса имеют сотни жёстко прописанных переходов в будущем, и я не понимаю, почему? Не похоже, чтобы они все использовали лунный календарь.
- У
Asia/Jerusalem
прописано 780 будущих переходов из всего 901. - У
Africa/Cairo
прописано 800 будущих переходов из 929 всего. - У
America/Nuuk
прописано 800 будущих переходов из всего 889. - У
America/Santiago
прописано 800 будущих переходов из всего 931. - У
Pacific/Easter
прописано 800 будущих переходов из всего 911. - У
Asia/Gaza
прописано 982 будущих перехода из всего 1106.
Во всех этих файлах отсутствует футер с правилом, но у нашего товарища Africa/Casablanca
жёстко прописано всего 132 перехода, и тоже нет футера. В чём тут дело?
Автор: Bright_Translate