ћерион Ќетворкс

11 минут

 огда вы разрабатываете какое-то программное обеспечение, вы поначалу можете не задумыватьс€ о часовых по€сах. ≈сли только вы не живете в стране, котора€ имеет несколько часовых по€сов, например, в —Ўј или –оссии.

Ќе так давно € столкнулс€ с проблемой, котора€ была св€зана с часовыми по€сами. Ѕыло несколько юнит-тестов, имеющих дело с датами. ќни работали в моем офисе во ‘ранции, но не работали в ћарокко у новых членов нашей команды.

¬от тот самый юнит-тест, который работает во ‘ранции, но не работает в ћарокко

¬от тот самый юнит-тест, который работает во ‘ранции, но не работает в ћарокко.

ƒл€ мен€ это была возможность научитьс€ правильно обрабатывать дату и врем€ в международном программном обеспечении. ¬ этой статье € расскажу о часовых по€сах и поделюсь некоторыми правилами, которым нужно следовать.


 раткое введение в часовые по€са

ѕоскольку земл€ имеет форму практически сферы, то солнце восходит в японии, а садитс€ в јмерике. ≈сли бы все использовали глобальное врем€, то, например, 9:00 было бы восходом солнца в японии, но закатом в јмерики. Ёто было бы не очень удобно.

ƒл€ того, чтобы врем€ согласовывалось с фазами солнца, необходимо перейти от глобального времени ко времени в соответствии с вашим местоположением. ¬ результате земной шар разбиваетс€ на часовые по€са (time zones), и каждый из них имеет некоторое смещение (offset). Ёто смещение представл€ет собой количество минут, которое необходимо добавить к глобальному времени, чтобы получить врем€ вашего часового по€са. Ёто смещение может быть положительным или отрицательным.

ќбщеприн€тые часовые по€са мира

√лобальное врем€ называетс€ UTC, оно расшифровываетс€ как Universal Time Coordinate (всемирное координированное врем€). ¬ы также могли слышать о GMT Ц часовом по€се без смещени€.

Ќапример, когда по UTC врем€ 10:50, то в —ан-‘ранциско Ц 03:50 со смещением -0700, а в ѕекине Ц 18:50 со смещением +0800. ќднако смещение может быть не только в целых часах. Ќапример, смещение Ќепала составл€ет +0545.

¬ придачу к этому смещению, св€занному с часовым по€сом, в некоторых странах еще дополнительно смещают часы два раза в год. DST, или летнее врем€, добавл€ет еще один час к смещению часового по€са перед наступлением летнего времени. «атем часы перевод€тс€ обратно зимой. ÷ель такого смещени€ заключаетс€ в том, чтобы сделать дневное врем€ длиннее.

—амый простой способ определить часовой по€с Ц использовать базу данных часовых по€сов IANA. ¬ результате вы получите строку, такую как Europe/Paris (≈вропа/ѕариж), в соответствии с шаблоном –егион/√ород.  роме того, Microsoft поддерживает собственную базу данных часовых по€сов Microsoft, котора€ используетс€ в их операционных системах. Ќо использование этой базы может вызвать проблемы при запуске кроссплатформенных приложений .NET Core.

IANA по-прежнему остаетс€ лучшим вариантом. Ѕаза данных Microsoft обновл€етс€ не так часто, имеет меньшую историю и использует довольно любопытные названи€ часовых по€сов (например, Romantic Standard Time Ц романтическое стандартное врем€) и подвержена ошибкам. Ќапример, постарайтесь не перепутать следующее: Arab Standard Time, Arabic Standard Time и Arabian Standard Time.

» последнее: есть множество способов записать дату.   счастью, спецификаци€ ISO 8601 устанавливает общее правило дл€ представлени€ даты.

Ќо€брь 11, 2018 в 12:51:43 AM (в часовом по€се UTC+00:00)
2018-11-05T12:51:43Z <- Z означает UTC

Ќо€брь 11, 2018 в 12:51:43 AM (в часовом по€се UTC +07:30)
2018-11-05T12:51:43+0730

 ак компьютеры обрабатывают даты

 омпьютеры умеют выполн€ть операции только с числами. ј это значит, что 2020-08-01 + 1 не равно 2020-08-02 и не может быть обработано.

ƒл€ того, чтобы упростить работу с датами, мы можем представить их в виде чисел. ¬от что такое метки времени - timestamps. Ёто количество миллисекунд, прошедших с заранее определенной даты (или начала отсчета времени - epoch) до указанной даты.

ќтлично, теперь тогда давайте выберем начало отсчета времени! Ќа самом деле общее начало отсчета уже установлено Ц 1 €нвар€ 1970 года (полночь UTC).

January 1, 1970

ƒл€ того, чтобы убедитьс€ в том, что вы все пон€ли, запустите фрагмент, представленный выше, в своем браузере. Ќу и как? ¬ы не получили тот же результат?

’орошо, € немного сжульничал, чтобы получить такой результат... я должен получить четверг, 1 €нвар€ 1970 года, 01:00 GMT +0100, потому что часовой по€с моего компьютера установлен на Europe/Paris.

Ќа самом деле, точка с нулевой меткой времени Ц это полночь в √ринвиче, а также 05:45 в ћумбаи и даже 1969-12-31“16:30 в —ан-‘ранциско, если учитывать смещение их часовых по€сов.

ѕравило є1: ћетки времени предназначены только дл€ сохранени€, а не дл€ отображени€. ќни учитываютс€ в формате UTC, поскольку не включают смещение или часовой по€с.

–аньше вы не получали Ђправильнуюї дату, потому что JavaScript использует ваш местный часовой по€с, чтобы показать вам наиболее точную дату/врем€.

ј теперь попробуйте запустить следующий фрагмент. я уверен, что сейчас вы получите тот же результат, что и €:

1970-01-01T00:00:00

ƒа, нулева€ метка времени Ц это 1970-01-01 “00:00:00 UTC дл€ всех по всему миру. “ем не менее, это не будет верным, если вы выберите другой часовой по€с.

“аким образом, toString отображает дату с использованием вашего местного часового по€са, а toUTCString с помощью UTC. “акже будьте осторожны с toISOString, который аналогичен toUTCString, но выводит дату и врем€ в формате ISO 8601 (он должен называтьс€ toUTCISOString).

я рекомендую команду date дл€ преобразовани€ второй метки времени (из миллисекунд) в читабельную строку. »спользование этой команды с параметром UTC гарантирует, что часовой по€с вашего компьютера/браузера не будет учитыватьс€.

# Linux
$ date -d @1586159897 -u 
Mon Apr  6 07:58:17 UTC 2020

# ƒл€ пользователей Osx
$ date -r 1586159897 -u 

ƒавайте исправим наш юнит-тест

ѕроблема, св€занна€ с часовыми по€сами, с которой € столкнулс€, была в моих юнит-тестах. ѕотратьте немного времени, чтобы прочитать и пон€ть, то он должен был сделать:

ёнит-тест

÷ель этого теста в том, чтобы проверить, что setHours устанавливает часы и минуты даты на ноль (т.е. полночь). —начала € выбираю случайную метку времени, котора€ не соответствует полуночи. «атем сравниваю результат с меткой времени того же дн€ в полночь.

Ёто, на самом деле, работает, но только в том случае, если смещение вашего часового по€са +0200 (с учетом летнего времени). “ак, например, он не будет работать дл€ Africa/Casablanca (+0100 с учетом летнего времени). ƒавайте посмотрим, как печатаютс€ эти метки времени:

timestamps

¬от и все, как мы и предполагали, дата UTC дл€ двух результатов не совпадает. Ёто также означает, что полученные метки времени также не совпадают.

 ак вы можете видеть, смещение дл€ ѕарижа Ц это +0200, а дл€  асабланки - +0100. Ќо они оба отображают полночь с toString. Ёто значит, что функци€ setHours использует часовой по€с вашего компьютера дл€ выполнени€ операции. » toString, соответственно, отображает дату, использу€ ваш часовой по€с.

Ёто не единственна€ проблема, возникающа€ в этом тесте. „то будет, если вы запустите этот тест в —ан-‘ранциско? ¬ерно, будет указан день 2020-07-31 дл€ обеих дат со смещени€ми -0700.

—амый безопасный способ сделать этот тест надежным и позволить ему работать по всему миру Ц использовать дату вашего местного часового по€са. » вам больше не потребуетс€ использовать метки времени дл€ установки начальных дат.

timestamps

“еперь модернизируем предыдущее правило о метках времени:

ѕравило є2: —троковые даты подход€т дл€ отображени€ с использованием часового по€са пользовател€ и вычислений. ќни не относ€тс€ к UTC, но, как правило, включают смещение.

ѕродолжайте в том же духе и на стороне сервера

ѕравило о метках времени по-прежнему примен€етс€ на стороне сервера. ќднако втрое правило о строковых датах не может быть использовано.

ƒействительно, в некоторых случа€х при использовании таких технологий, как PHP, Java и Rails, страницы отображаютс€ на стороне сервера (SSR). Ёто значит, что весь HTML-код генерируетс€ сервером, и он не имеет представлени€ о том, какой часовой по€с у клиента. ѕросто подумайте, сервер Ц это не более чем просто компьютер. ” него есть собственный часовой по€с, но он не об€зательно совпадает с часовым по€сом клиента.

ѕравило є3: —ерверы могут знать часовой по€с клиента или отправл€ть дату в формате UTC. „асовой по€с сервера значени€ не имеет.

Ќовый Java 8 Date/Time считаетс€ одним из самых пон€тных API, помогающих работать с датами. я не буду объ€сн€ть, как он работает, но давайте рассмотрим некоторые интересные моменты.

LocalDateTime, OffsetDateTime и ZonedDateTime Ц это 3 класса, предназначенные дл€ вычислени€ и отображени€ даты и времени. Ќет больше Date и DateTime, которые путают отображени€ локальной даты и даты в формате UTC.

—ледующие примеры были вз€ты из статьи, в которой описываетс€ API Java 8 Date/Time с множеством примеров.

ƒавайте посмотрим различи€ между этими трем€ классами:

API Java 8 Date/Time

ћежду OffsetDateTime и ZonedDateTime есть небольшое, но важное отличие, вы его заметили?

 ак следует из названи€, OffsetDateTime осведомлен только о смещении между локальной датой и UTC. Ёто значит, что он обрабатывает летнее врем€ не как дату, прив€занную к часовому по€су.

API Java 8 Date/Time

ѕример с часовым по€сом кажетс€ правильным. Ќа самом деле оба варианты верны, потому что добавление 1 дн€ может означать:

  • ƒобавление 1 дн€ и сохранение того же часа (обрабатывает летнее врем€ с ZonedDateTime)
  • ƒобавление 24 часов к текущей дате (с OffsetDateTime).

ѕомните правило є1 о метках времени? ƒл€ сохранени€ следует использовать только временную метку UTC. Java API представл€ет класс Instant, который €вл€етс€ меткой времени, которую вы можете получить их любого из трех классов, используемых дл€ отображени€ даты.

Instant

«аключение

»з этой статьи вы узнали, что метки времени предназначены дл€ сохранени€ (правило є1), а строковые даты Ц дл€ отображени€ (правило є2). ¬ы заметили, наверное, что количество секунд от начала отсчета времени Ч это дольно большое число?

¬от почему после проблемы Unix Millennium Bug (Y2K) (некорректна€ интерпретаци€ дат после 2000 года, или Ђошибка тыс€челети€ї) по€вл€етс€ проблема Y2K38, котора€ служит обозначением 2038 года. 19 €нвар€ 2038 года в 03:14:07 (2038-01-19T03:14:07Z) отметка времени (в секундах) достигнет своего максимума дл€ 32-битных целых чисел Ц 2 147 483 647. «атем, после добавлени€ еще одной секунды, оно превратитьс€ в отрицательное число.

Unix Millennium Bug

Ёто табло отображает €нварь 1900 года, а не €нварь 2000 года

Ќа форумах люди утверждают, что дл€ них это не имеет значени€, поскольку их программное обеспечение не будет использоватьс€ в течение 20 лет без перезаписи. Ёто может быть и так, но давайте подумаем над решением этой проблемы (с помощью MySQL):

  • ћожно изменить тип TIMESTAMP на 64-битные целые числа со знаком.
  • ћожно сохран€ть даты в формате UTC в столбцах DATETIME вместо TIMESTAMP.

ќба решени€ имеют свои плюсы и минусы. ѕервый вариант похож на танцы с бубном, проблема все равно по€витс€. “ем не менее, такой подход решает проблему почти на бесконечное количество времени (миллиарды лет).  огда проблема возникнет снова, то ваше программное обеспечение устареет и больше не будет использоватьс€.

¬торой вариант тоже решает проблемы с долгосрочным результатом (до 31 декабр€ 9999 года 23:59:59 (9999-12-31T23:59:59Z)).

ƒл€ логов рекомендуетс€ использовать TIMESTAMP, а дл€ других целей лучше использовать DATETIME. ¬сегда помните, что метка времени не может хранить дату до 1 €нвар€ 1970 года 00:00:00 (1970-01-01T00:00:00Z) и после 19 €нвар€ 2038 03:14:07 (2038-01-19T03:14:07Z). Ёто значит, что дл€ сохранени€ дат далекого прошлого и будущего вы должны использовать DATETIME.

 роме того, в MySQL TIMESTAMP хран€тс€ в формате UTC, но отображаютс€ в соответствии с указанным часовым по€сом (и конвертируютс€ в UTC перед сохранением). “акой механизм удобен в том случае, если вам нужно получить локальную дату или вы не хотите иметь дело с DATETIME.

» последнее слово о moment.js Ц попул€рной библиотеке дл€ работы с датами. я в первый раз экспериментировал с этой проблемой и хотел бы вас предупредить:

moment.js

ќба файла console.log будут выводить 2020-08-02 00:00. ≈сли вы привыкли к функциональному программированию, то будете ждать, что hours и minutes вернут новый объект времени, так как это чистые функции. Ќо это не так Ц они мен€ют дату ввода и возвращают ее дл€ упрощени€ образовани€ цепочек.


—кидки 50% в Merion Academy

¬ыбрать курс