img

Как обрабатывать часовые пояса при написании кода

Когда вы разрабатываете какое-то программное обеспечение, вы поначалу можете не задумываться о часовых поясах. Если только вы не живете в стране, которая имеет несколько часовых поясов, например, в США или России.

Не так давно я столкнулся с проблемой, которая была связана с часовыми поясами. Было несколько юнит-тестов, имеющих дело с датами. Они работали в моем офисе во Франции, но не работали в Марокко у новых членов нашей команды.

Вот тот самый юнит-тест, который работает во Франции, но не работает в Марокко

Вот тот самый юнит-тест, который работает во Франции, но не работает в Марокко.

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


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

Поскольку земля имеет форму практически сферы, то солнце восходит в Японии, а садится в Америке. Если бы все использовали глобальное время, то, например, 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 вернут новый объект времени, так как это чистые функции. Но это не так – они меняют дату ввода и возвращают ее для упрощения образования цепочек.

Ссылка
скопирована
Получите бесплатные уроки на наших курсах
Все курсы
DevOps
Скидка 25%
DevOps-инженер с нуля
Научитесь использовать инструменты и методы DevOps для автоматизации тестирования, сборки и развертывания кода, управления инфраструктурой и ускорения процесса доставки продуктов в продакшн. Станьте желанным специалистом в IT-индустрии и претендуйте на работу с высокой заработной платой.
Получи бесплатный
вводный урок!
Пожалуйста, укажите корректный e-mail
отправили вводный урок на твой e-mail!
Получи все материалы в telegram и ускорь обучение!
img
Еще по теме:
img
Git Flow - это специальная система ветвления для Git. Она помогает команде лучше контролировать и добавлять различные версии про
img
Docker — популярная платформа виртуализации на уровне ОС. Она поставляет приложения в пакетах (контейнерах), которые, представля
img
Хуки в Git — это bash-скрипты, которые запускаются до или после команд Git, например, коммитов и пушей. Они позволяют автоматизи
img
  Nomad и Kubernetes – это две самые популярные платформы оркестровки, предназначенные для оркестровки динамических рабочих нагр
img
  Давайте узнаем о новом Ops-течении – GitOps! DevOps поспособствовал цифровизации многих компаний. Речь идет о командах разрабо
img
  Канареечное (canary) развёртывание – это метод разработки и развертывания программного обеспечения, который позволяет выпускат
ЗИМНИЕ СКИДКИ
40%
50%
60%
До конца акции: 30 дней 24 : 59 : 59