[Перевод] Работа с датой и часовыми поясами в JavaScript

b68692769b8ee19e59f9fd9db86d7384.png?v=1

От переводчика

Однажды каждый разработчик коснется работы с часовыми поясами и будет фрустрирован необходимостью съесть слона для реализации, казалось бы, минорной задачи. Оказавшись в такой ситуации я нашел эту статью, которая дала ответы на все мои вопросы.

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

Статья немаленькая и делится логически на две части:

  • Детальная информация о принятом сегодня стандарте времени и часовых поясов, которая будет полезна для разработчика на любом языке

  • Обзор подхода к работе со временем, принятого в JS, объекта Date, его методов и лучших практик в клиент-серверной разработке, и вывод почему для полноценной работы с датой вам не обойтись без задействования специальных библиотек.

Поехали!

Обработка часового пояса в JavaScript

Недавно я работал над задачей добавления функции часового пояса в TOAST UI Calendar, библиотеку календарей JavaScript, созданную моей командой. Я хорошо знал, что поддержка часовых поясов в JavaScript довольно таки слабая и надеялся, что абстрагирование существующих инструментов легко решило бы многие проблемы. 

Однако моя надежда не оправдалась, и продвигаясь в решении задачи, я все больше осознавал, насколько непросто обрабатывать часовой пояс в JavaScript. Реализация функций часовых поясов помимо простого форматирования времени и вычисления временных данных с помощью сложных операций (например, календаря) была поистине сложной задачей. По этой причине я приобрел бесценный опыт решения проблемы, который привел к возникновению новых проблем.

Цель это статьи — обсудить проблемы и решения, связанные с особенностями применения часовых поясов в JavaScript. В процессе написания этой объемной статьи, я внезапно понял, что корень моей проблемы кроется в моем плохом понимании концепции часовых поясов. Поэтому я сначала подробно рассмотрю определение и стандарты, относящиеся к часовому поясу, а затем расскажу о JavaScript.

Что такое часовой пояс (time zone)?

Часовой пояс — это регион, который использует единый, законодательно установленный стандарт времени. У многих стран свой уникальный часовой пояс, а в некоторых крупных странах, таких как США или Канада, даже несколько часовых поясов. Интересно, что хотя Китай достаточно велик, чтобы иметь несколько часовых поясов, он использует только один часовой пояс. По этой причине солнце встает в западной части Китая около 10:00 утра.

GMT, UTC и Offset

GMT (время по Гринвичу)
Местное время в Корее обычно GMT+09:00. GMT — это сокращение от среднего времени по Гринвичу, которое является временем на часах Королевской Обсерватории в Гринвиче, Великобритания, расположенной на долготе 0. Система GMT начала распространяться 5 февраля 1925 года и была мировым стандартом времени до 1 января 1972 года.

UTC (универсальное глобальное время)
Многие считают GMT и UTC одним и тем же, и во многих случаях они взаимозаменяемы, но на самом деле у них есть существенные отличия. UTC было создано в 1972 году для компенсации проблемы замедления вращения Земли. Эта система времени основана на Международном атомном времени, которое использует атомную частоту цезия для установки стандарта времени. Другими словами, UTC — более точная система. Хотя фактическая разница во времени между ними мала, UTC является более точным выбором для разработчиков программного обеспечения.

Когда система еще находилась в разработке, англоязычное население хотело назвать систему CUT (всемирное координированное время), а франкоязычное население хотело назвать ее TUC (Мировое время). Однако ни одна из сторон не выиграла бой, поэтому они пришли к соглашению об использовании аббревиатуры UTC, поскольку она содержала все основные буквы (C, T и U).

Offset (смещение часового пояса относительно часового пояса UTC)
+09:00 в UTC+09:00 означает, что местное время на 9 часов опережает стандартное время UTC. Это означает, что когда в Корее 21:00, в регионе UTC+00:00 — полдень, 12:00. Разница во времени между стандартным временем UTC и местным временем называется смещением (offset), которое выражается следующим образом: +09:00, -03:00 и т. д.

Часто страны называют свои часовые пояса своими уникальными именами. Например, часовой пояс Кореи называется KST (стандартное время Кореи) и имеет определенное значение смещения, которое выражается как KST = UTC+09:00. Однако смещение +09:00 также используется не только Кореей, но и Японией, Индонезией и многими другими, что означает, что отношение между смещениями и именами часовых поясов не 1:1, а 1: N. Список стран со смещением +09:00 можно найти в википедии на странице UTC+09:00.

Некоторые смещения не производятся строго на почасовой основе. Например, в Северной Корее в качестве стандартного времени используется +08:30, а в Австралии +08:45 или +09:30, в зависимости от региона.

Полный список смещений UTC и их названия можно найти здесь: Список смещений времени UTC.

Time zone!== offset?
Как я уже упоминал ранее, мы используем названия часовых поясов (KST, JST) взаимозаменяемо со смещением, не различая их. Но это неправильно рассматривать время и смещение в определенном регионе одинаково, по следующим причинам:

Летнее время (DST)
Хотя это понятие может быть неизвестно в некоторых странах, во многих странах летнее время официально принято — в частности, в США, Великобритании и странах Европы. На международном уровне летнее время обычно называется Daylight Saving Time (DST). Во время перехода на DST мы переводим стрелки часов на один час вперед от стандартного времени в летнее время.

Например, в Калифорнии в США зимой используется PST (стандартноетихоокеанское время, UTC-08:00), а летом — PDT (тихоокеанское летнеевремя, UTC-07:00). Регионы Северной Америки, в которых используются два часовых пояса, вместе называются Тихоокеанским временем (PT), и это название принято во многих регионах США и Канады.

Теперь самое интересное: когда именно начинается и заканчивается лето? На самом деле, даты начала и окончания летнего времени остаются на собственное усмотрение каждой страны. Это раз. А два — страна может поменять время и до изменения мы должны будем учитывать одно время, а после изменения — другое.

Например, до 2006 года в США и Канаде летнее время начиналось с первого воскресенья апреля в 02:00 и длилось до последнего воскресенья октября в 12:00, а с 2007 года стало начинаться во второе воскресенье марта с 02:00 и длиться до 2:00 первого воскресенья ноября. В европейских странах летнее время применяется единовременно по всей стране, в то время как в США летнее время поочередно применяется к часовым поясам.

Меняется ли часовой пояс?

Как я вкратце упомянул ранее, каждая страна имеет собственное право определять, какой часовой пояс использовать, а это означает, что ее часовой пояс может быть изменен по любым политическим или экономическим причинам. Например, в штатах период перехода на летнее время был изменен в 2007 году, поскольку президент Джордж Буш подписал энергетическую политику в 2005 году. Египет и Россия использовали летнее время, но перестали его использовать с 2011 года.

В некоторых случаях страна может изменить не только летнее время, но и стандартное время. Например, Самоа использовало смещение UTC-10:00, но позже перешло на UTC+14:00, чтобы уменьшить потери в торговле, вызванные разницей во времени между Самоа и Австралией и Новой Зеландией. Это решение привело к тому, что страна пропустила весь день 30 декабря 2011 года, и об этом сообщили газеты по всему миру.

Нидерланды использовали смещение +0:19:32.13, которое является излишне точным с 1909 года, но изменили его на смещение +00:20 в 1937 году, а затем снова изменили на смещение +01:00 в 1940 году, и придерживаются его до сих пор.

1 часовой пояс : N смещений

Подводя итог: один часовой пояс может иметь одно или несколько смещений. Какое смещение страна будет использовать в качестве стандартного времени в определенный момент, может варьироваться по политическим или экономическим причинам.

Это не большая проблема в повседневной жизни, но эта проблема возникает при попытке систематизировать работу со временем на основе определённых правил. Представим, что вы хотите установить стандартное время для своего смартфона с помощью смещения (offset). Если вы живете в регионе, где применяется летнее время, время на вашем смартфоне следует корректировать всякий раз, когда летнее время начинается и заканчивается. В этом случае вам понадобится концепция, которая объединяет стандартное время и летнее время в один часовой пояс (например, Тихоокеанское время).

Но это не может быть реализовано с помощью пары простых правил. Например, поскольку штаты изменили даты начала и окончания летнего времени в 2007 г., 31 мая 2006 г. вам следовало жить по PDT (тихоокеанское летнеевремя, UTC-07:00), а после 31 марта 2007 г. уже по PST (стандартноетихоокеанское время, UTC-08:00). Это означает, что для обращения к определенному часовому поясу вы должны знать все исторические данные о стандартных часовых поясах или момент времени, когда правила летнего времени были изменены.

Вы не можете просто сказать: «Часовой пояс Сан-Франциско — PST, UTC-08:00». Вы должны быть более конкретными и сказать: «Сан-Франциско в настоящее время использует PST как стандартное время».

Пример, как страны Северной Америки стандартизировали эти расхождения для себя — это объединение PST и PDT в PT, учитывающее текущее стандартное время и его летнее время. Однако, это только североамериканская практика и перед нами продолжает стоять задача работы с датами в прошлом и будущем, на которые влияют произошедшие и ожидаемые изменения в правилах, во всех часовых поясах. Хорошо бы чтобы все это было оформлено в международный стандарт.

База данных часовых поясов IANA

По правде говоря, часовые пояса — это скорее база данных, чем набор правил, потому что они должны содержать все соответствующие исторические изменения. Существует несколько стандартных баз данных, предназначенных для обработки проблем с часовыми поясами, и наиболее часто используемой из них является База данных часовых поясов IANA. База данных часовых поясов IANA, также называемая базой данных tz (или tzdata), содержит исторические данные о местном стандартном времени по всему миру и изменениях летнего времени. Эта база данных организована так, чтобы содержать все исторические данные, которые в настоящее время можно проверить, чтобы гарантировать точность времени, начиная со времени Unix (1970.01 / 01 00:00:00). В ней также есть данные до 1970 года, но их точность не гарантируется.

Соглашение об именовании соответствует правилу Area/Location. Area обычно относится к названию континента или океана (Азия, Америка, Тихий океан), в то время как Location — к названию крупных городов, таких как Сеул и Нью-Йорк, а не к названию стран (это потому, что продолжительность жизни страны намного короче, чем города). Например, часовой пояс Кореи — Азия / Сеул, а часовой пояс Японии — Азия / Токио. Хотя эти две страны находятся географически в регионе, где принят стандартный offset UTC+09:00, они имеют разную историю изменений часовых поясов. Вот почему в этом стандарте они обрабатываются с использованием разных часовых поясов.

База данных часовых поясов IANA поддерживается многочисленными сообществами разработчиков и историков. Новые исторические факты и политические решения сразу же попадают в базу данных, что делает ее наиболее надежным источником. Более того, многие ОС на базе UNIX, включая Linux и macOS, а также популярные языки программирования, включая Java и PHP, используют эту базу данных.

Обратите внимание, что Windows отсутствует в приведенном выше списке поддержки. Это потому, что Windows использует собственную базу данных под названием Microsoft Time Zone Database. Однако эта база данных неточно отражает исторические изменения и поддерживается только Microsoft. Следовательно, она менее точна и надежна, чем IANA.

JavaScript и База данных часовых поясов IANA

Как я вкратце упомянул ранее, поддержка часового пояса в JavaScript довольно плохая. Поскольку по умолчанию он следует часовому поясу определенного региона (точнее, часовому поясу, выбранному во время установки ОС), возможности изменить его на новый часовой пояс нет. Кроме того, его спецификация для стандарта баз данных так же плохо определена, вы заметите это, если внимательно ознакомитесь с документацией ES2015. В отношении местного часового пояса и доступности летнего времени существует лишь несколько неопределенных заявлений. Например, летнее время определяется следующим образом: ECMAScript 2015 — Настройка летнего времени.

В зависимости от реализации, алгоритм использует наилучшую доступную информацию о часовых поясах для определения местной поправки на летнее время DaylightSavingTA (t), измеряемой в миллисекундах. Ожидается, что реализация ECMAScript сделает все возможное, чтобы определить местное летнее время.

Похоже, он просто говорит: «Эй, ребята, попробуйте и сделайте все возможное, чтобы это сработало». Это создает проблемы в кроссбраузерности. Вы можете подумать: «Это небрежно!», и тогда вы заметите еще одну строчку прямо под ним:

ПРИМЕЧАНИЕ. рекомендуется использовать информацию о часовых поясах из базы данных часовых поясов IANA http://www.iana.org/time-zones/.

Спецификации ECMA признается, что не имеет специальной стандартной базы данных в JavaScript и рекомендует использовать базу данных часовых поясов IANA. В результате разные браузеры используют свои собственные решения для расчета часовых поясов, и они часто несовместимы друг с другом. Позже в ECMA-402 добавили возможность использовать часовой пояс IANA в виде Intl.DateTimeFormat  для ECMAScript Internationalization API. Однако этот вариант по-прежнему гораздо менее надежен, чем в других языках программирования.

Часовой пояс в серверно-клиентской среде

Мы предположим простой сценарий, в котором необходимо учитывать часовой пояс. Допустим, мы собираемся разработать простое приложение-календарь, которое будет обрабатывать информацию о времени. Когда пользователь вводит дату и время в поле на странице реестра в клиентской среде, данные передаются на сервер и сохраняются в БД. Затем клиент получает зарегистрированные данные расписания с сервера и отображает их на экране.

Однако здесь есть то, что нужно учесть. Что, если некоторые из клиентов, обращающихся к серверу, находятся в разных часовых поясах? Расписание, зарегистрированное на 11 марта 2017 г. в 11:30 в Сеуле, должно отображаться как 10 марта 2017 г. в 21:30 при просмотре расписания в Нью-Йорке. Чтобы сервер поддерживал клиентов из разных часовых поясов, расписание, хранимое на сервере, должно иметь абсолютные значения, на которые не влияют часовые пояса. Каждый сервер имеет свой способ хранения абсолютных значений, и это выходит за рамки данной статьи, поскольку все зависит от сервера или среды базы данных. Однако для того, чтобы это работало, дата и время, передаваемые от клиента на сервер, должны быть значениями, основанными на том же смещении (обычно в формате UTC) или значениями, которые также включают данные часового пояса клиентской среды.

Обычно такие данные передаются в форме времени Unix на основе UTC или ISO-8601, содержащего информацию о смещении. В приведенном выше примере, если 11:30 утра 11 марта 2017 г. в Сеуле необходимо преобразовать во время Unix, это будет целочисленный тип со значением 1489199400. В соответствии с ISO-8601 это будет строковый тип, значение которого: 2017-03-11T11:30:00+09:00.

Если вы работаете с этим с помощью JavaScript в среде браузера, вы должны преобразовать введенное значение, как описано выше, а затем преобразовать его обратно в соответствии с часовым поясом пользователя. Необходимо учитывать обе эти две задачи. В языке программирования первый называется «синтаксическим анализом», а второй «форматированием». Теперь давайте выясним, как это обрабатывается в JavaScript.

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

Объект даты в JavaScript

В JavaScript задачи, связанные с датой или временем, обрабатываются с помощью объекта Date. Это собственный объект, определенный в ECMAScript, также, как Array или Object. Он в основном реализован в собственном коде, таком как C++. Его API хорошо описан в документации MDN. На это большое влияние оказывает класс Java java.util.Date. В результате он наследует некоторые нежелательные черты, такие как характеристики изменяемых данных и месяц, начинающийся с 0.

Объект Date в JavaScript внутренне управляет данными времени, используя абсолютные значения, такие как время Unix. Однако конструкторы и методы, такие как функции parse(), getHour(), setHour() и т.д. находятся под влиянием местного часового пояса клиента (точнее, часовой пояса операционной системы, в которой запущен браузер). Следовательно, если вы создаете объект Date с использованием данных, вводимых пользователем, данные будут напрямую отражать местный часовой пояс клиента.

Как я упоминал ранее, JavaScript не предоставляет произвольного способа изменения часового пояса. Поэтому я предполагаю здесь ситуацию, когда можно напрямую использовать настройку часового пояса браузера.

Создание объекта даты с пользовательским вводом

Вернемся к первому примеру. Предположим, что пользователь вошел в 11:30 11 марта 2017 г. на устройстве, которое соответствует часовому поясу Сеула. Эти данные хранятся в 5 целых числах 2017, 2, 11, 11 и 30, каждое из которых представляет год, месяц, день, час и минуту соответственно. (Поскольку месяц начинается с 0, значение должно быть 3–1 = 2.) С помощью конструктора вы можете легко создать объект Date, используя числовые значения.

const d1 = new Date(2017, 2, 11, 11, 30);
d1.toString(); // Sat Mar 11 2017 11:30:00 GMT+0900 (KST)

Если вы посмотрите на значение, возвращаемое d1.toString(), то узнаете, что абсолютное значение созданного объекта составляет 11:30 11 марта 2017 г. на основе смещения +09:00 (KST).

Вы также можете использовать конструктор вместе со строковыми данными. Если вы передаете строку при создании Date, он внутренне вызывает Date.parse() и вычисляет правильное значение. Эта функция поддерживает спецификации RFC2888 и ISO-8601. Однако, как описано в документе MDN Date.parse (), возвращаемое значение этого метода варьируется от браузера к браузеру, и формат типа строки может повлиять на предсказание точного значения. Таким образом, рекомендуется не использовать этот метод.

Например, строка вида 2015-10-12 12:00:00 возвращает NaN в Safari и Internet Explorer, в то же время эта строка возвращает местный часовой пояс в Chrome и Firefox. В некоторых случаях она возвращает значение, основанное на стандарте UTC.

Создание объекта даты с использованием данных сервера

Предположим теперь, что вы собираетесь получать данные с сервера. Если данные имеют числовое значение времени Unix, вы можете просто использовать конструктор для создания объекта Date. Когда конструктор Date получает единственный числовой параметр, он распознается, как значение времени Unix в миллисекундах. (Внимание: JavaScript обрабатывает время Unix в миллисекундах. Это означает, что стандартное числовое значение времени Unix необходимо умножить на 1000) Результат выполнения следующего кода будет таким же, как и в предыдущем примере.

const d1 = new Date(1489199400000);
d1.toString(); // Sat Mar 11 2017 11:30:00 GMT+0900 (KST)

Что если вместо времени Unix использовать строковый тип, такой как ISO-8601? Как я объяснил в предыдущем абзаце, метод Date.parse() ненадежен, и его лучше не использовать. Однако, поскольку в ECMAScript 5 и более поздних версиях указана поддержка ISO-8601, вы можете использовать строки в формате, указанном в ISO-8601, для конструктора Date в Internet Explorer 9.0 или более поздней версии, который поддерживает ECMAScript 5 при осторожном использовании.

Если вы поддерживаете старые браузеры, не забудьте добавить букву Z в конце. Без неё старые браузеры иногда интерпретируют строку на основе вашего местного времени, а не UTC. Ниже приведен пример запуска в Internet Explorer 10.

const d1 = new Date('2017-03-11T11:30:00');
const d2 = new Date('2017-03-11T11:30:00Z');
d1.toString(); // "Sat Mar 11 11:30:00 UTC+0900 2017"
d2.toString(); // "Sat Mar 11 20:30:00 UTC+0900 2017"

Согласно техническим характеристикам результирующие значения в обоих случаях должны быть одинаковыми. Однако, как вы можете видеть, результирующие значения отличаются как d1.toString() и d2.toString(). В последней версии браузера эти два значения будут одинаковыми. Чтобы избежать проблем такого рода, всегда следует добавлять Z в конце строки, если нет данных о часовом поясе.

Создание данных для передачи на сервер

Теперь используя созданный ранее объект Date, вы можете свободно добавлять или вычитать время в зависимости от местных часовых поясов. Но не забудьте преобразовать данные обратно в предыдущий формат в конце обработки, прежде чем передавать их обратно на сервер.

Если это время Unix, вы можете просто использовать метод getTime() для выполнения этого. (Обратите внимание на использование миллисекунд.)

const d1 = new Date(2017, 2, 11, 11, 30);
d1.getTime(); // 1489199400000

А как насчет строк формата ISO-8601? Как объяснялось ранее, Internet Explorer 9.0 или выше, который поддерживает ECMAScript 5 или выше, поддерживает формат ISO-8601. Вы можете создавать строки формата ISO-8601, используя метод toISOString() или toJSON(). toJSON() может использоваться для рекурсивных вызовов с JSON.stringify() или другими. Оба метода дают одинаковые результаты, за исключением случая, когда они обрабатывают недопустимые данные:

const d1 = new Date(2017, 2, 11, 11, 30);
d1.toISOString(); // "2017-03-11T02:30:00.000Z"
d1.toJSON();      // "2017-03-11T02:30:00.000Z"
const d2 = new Date('Hello');
d2.toISOString(); // Error: Invalid Date
d2.toJSON();      // null

Вы также можете использовать метод toGMTString() или toUTCString() для создания строк в формате UTC. Поскольку они возвращают строку, удовлетворяющую стандарту RFC-1123, вы можете использовать это по мере необходимости.

Объекты Date включают toString(), toLocaleString() и их методы расширения. Однако, поскольку они в основном используются для возврата строки, основанной на местном часовом поясе, и возвращают различные значения в зависимости от используемого браузера и ОС, они не очень полезны.

Изменение местного часового пояса

Теперь вы можете видеть, что в JavaScript слабая поддержка часовых поясов. Что делать, если вы хотите изменить настройку местного часового пояса в своем приложении, не соблюдая настройки часового пояса вашей ОС? Или что, если вам нужно отображать несколько часовых поясов одновременно в одном приложении? Как я уже говорил несколько раз, JavaScript не позволяет вручную изменять местный часовой пояс. Единственное решение этой проблемы — добавить или удалить значение смещения от даты при условии, что вы уже знаете значение смещения часового пояса. Но пока не расстраивайтесь. Посмотрим, есть ли какое-нибудь решение, чтобы обойти это.

Давайте продолжим предыдущий пример, предполагая, что часовой пояс браузера установлен на Сеул. Пользователь входит в 11:30 11 марта 2017 г. по сеульскому времени, но хочет видеть местное время Нью-Йорка. Сервер передает данные времени Unix в миллисекундах и вы можете преобразовать их, если знаете смещение местного часового пояса, а мы знаем, что для Нью-Йорка это -05:00

Для вычисления смещений вы можете использовать метод getTimeZoneOffset(). Этот метод — единственный API в JavaScript, который можно использовать для получения информации о местном часовом поясе. Он возвращает значение смещения текущего часового пояса в минутах относительно часового пояса UTC.

const seoul = new Date(1489199400000);
seoul.getTimeZoneOffset(); // -540

Возвращаемое значение -540 означает, что часовой пояс Сеула опережает UTC на 540 минут. Обратите внимание, что знак минус перед значением противоположен знаку плюса в стандартном обозначении смещения (+09:00). Не знаю почему, но вот так это отображается. Если мы вычислим смещение Нью-Йорка с помощью этого метода, мы получим 60×5 = 300. Преобразуйте разницу 840 в миллисекунды и создайте новый объект Date. Затем вы можете использовать get-методы этого объекта для преобразования значения в любой формат по вашему выбору. Давайте создадим простую функцию форматирования для сравнения результатов.

function formatDate(date) {
 return date.getFullYear() + '/' +
   (date.getMonth() + 1) + '/' +
   date.getDate() + ' ' +
   date.getHours() + ':' +
   date.getMinutes();
}

const seoul = new Date(1489199400000);
const ny = new Date(1489199400000 - (840 * 60 * 1000));
formatDate(seoul);  // 2017/3/11 11:30
formatDate(ny);     // 2017/3/10 21:30

formatDate() показывает правильную дату и время в соответствии с разницей часовых поясов между Сеулом и Нью-Йорком. Похоже, мы нашли простое решение. Тогда можем ли мы преобразовать его в местный часовой пояс, если мы знаем смещение региона? К сожалению, ответ отрицательный. Помните, что я сказал ранее? Что данные часового пояса — это своего рода база данных, содержащая историю всех изменений смещения? Чтобы получить правильное значение часового пояса, вы должны знать значение смещенияна момент даты (а не на текущую дату).

Проблема преобразования местного часового пояса

Если вы продолжите работать с приведенным выше примером еще немного, вы скоро столкнетесь с проблемой. Пользователь хочет проверить время по местному времени Нью-Йорка, а затем изменить дату с 10-го на 15-е. Если вы используете метод setDate() объекта Date, вы можете изменить дату, не изменяя другие значения.

ny.setDate(15);
formatDate(ny);   // 2017/3/15 21:30

Выглядит достаточно просто, но здесь есть ловушка. Что бы вы сделали, если бы вам пришлось передать эти данные обратно на сервер? Поскольку данные были изменены, вы не можете использовать такие методы, как getTime() или getISOString(). Следовательно, вы должны отменить преобразование, прежде чем отправлять его обратно на сервер.

const time = ny.getTime() + (840 * 60 * 1000);  // 1489631400000

Некоторые из вас могут задаться вопросом, почему я добавил использование преобразования данных, когда мне все равно нужно преобразовать их обратно перед возвратом. Выглядит так, будто я могу просто обработать их без преобразования и временно создать преобразованный объект Date только при форматировании. Однако не все так просто. Если вы измените дату объекта Date по сеульскому времени с 11-го на 15-е, добавляются 4 дня (24×4 * 60×60 * 1000). Однако по местному времени Нью-Йорка, поскольку дата была изменена с 10-го на 15-е, в результате было добавлено 5 дней (24×5 * 60×60 * 1000). Это означает, что для получения корректного результата вы должны рассчитывать даты на основе местного смещения часового пояса относительно часового пояса UTC.

Проблемы на этом не заканчиваются. Вот еще одна, где вы не получите желаемое значение, просто добавив или вычтя смещения часового пояса. Поскольку 12 марта является датой начала летнего времени по местному времени Нью-Йорка, смещение часового пояса 15 марта 2017 г. должно быть -04:00, а не -05:00. Поэтому, когда вы отключаете преобразование, вы должны добавить 780 минут, что на 60 минут меньше, чем раньше.

const time = ny.getTime() + (780 * 60 * 1000);  // 1489627800000

Напротив, если местным часовым поясом пользователя является Нью-Йорк и он хочет узнать время в Сеуле, нет необходимости применять летнее время, что вызывает другую проблему.

Проще говоря, вы не можете использовать только полученное смещение для выполнения точных операций в зависимости от выбранного вами часового пояса. Если вы вспомните то, что мы обсуждали в предыдущей части этого документа, вы поймете, что в этом преобразовании все еще есть дыра, при условии что вы помните правила летнего времени. Чтобы получить точное значение, вам нужна база данных, содержащая всю историю изменений смещения часового пояса, например База данных часовых поясов IANA.

Чтобы решить эту проблему, необходимо сохранить всю базу данных часовых поясов и всякий раз, когда данные о дате или времени извлекаются из объекта Date, найти дату и соответствующее смещение часового пояса, а затем преобразовать значение, используя описанный выше процесс. Теоретически это возможно. Но на самом деле это требует слишком больших усилий, и проверка целостности преобразованных данных также будет сложной задачей. Но пока не расстраивайтесь. До сих пор мы обсуждали некоторые проблемы JavaScript и способы их решения. Теперь мы готовы использовать хорошо созданную библиотеку.

Moment timezone

Moment — это хорошо зарекомендовавшая себя библиотека JavaScript, которая является почти стандартом для обработки даты. Предоставляя различные API-интерфейсы для дат и форматирования, в последнее время многие пользователи признают moment стабильным и надежным. И есть модуль расширения Moment Timezone, который решает все проблемы, описанные выше. Этот модуль расширения содержит данные базы данных часовых поясов IANA для точного расчета смещений и предоставляет различные API-интерфейсы, которые можно использовать для изменения и форматирования часового пояса.

В этой статье я не буду подробно обсуждать, как использовать библиотеку или структуру библиотеки. Я просто покажу вам, насколько просто решить проблемы, которые я обсуждал ранее. Если кому-то интересно, см. Документацию Moment Timezone.

Давайте решим проблему, показанную на картинке, с помощью Moment Timezone.

const seoul = moment(1489199400000).tz('Asia/Seoul');
const ny = moment(1489199400000).tz('America/New_York');
seoul.format(); // 2017-03-11T11:30:00+09:00
ny.format();    // 2017-03-10T21:30:00-05:00
seoul.date(15).format();  // 2017-03-15T11:30:00+09:00
ny.date(15).format();     // 2017-03-15T21:30:00-04:00

Как вы видите в результате, смещение seoul останется прежним, в то время как смещение ny было изменено с -05:00 на -04:00. И если вы используете функцию format(), вы можете получить строку в формате ISO-8601, которая точно применила смещение. Видите насколько это просто по сравнению с тем, что мы делали ранее.

Заключение

До сих пор мы обсуждали API-интерфейсы часовых поясов, поддерживаемые JavaScript, и их проблемы. Если вам не нужно вручную изменять местный часовой пояс, вы можете реализовать необходимый функционал даже с помощью базовых API-интерфейсов при условии, что вы используете Internet Explorer 9 или выше. Однако, если вам нужно вручную изменить местный часовой пояс, все становится очень сложным. В регионе, где нет летнего времени и политика часовых поясов практически не меняется, вы можете частично реализовать ее, используя getTimezoneOffset() для преобразования данных. Но если вам нужна полная поддержка часовых поясов, не создавайте ее с нуля. Лучше используйте такую библиотеку, как Moment Timezone.

Я сам пытался реализовать часовой пояс, но у меня ничего не вышло, что неудивительно. Вывод здесь после нескольких неудач — лучше использовать библиотеку. Когда я впервые начал писать эту статью, я не знал, о каком выводе собираюсь написать, и вот мы здесь. В заключение я бы сказал, что слепое использование внешних библиотек без знания того, какие функции они поддерживают в JavaScript и какие у них проблемы, плохая идея. Как всегда, важно выбрать правильный инструмент для вашей ситуации. Надеюсь, эта статья помогла вам принять собственное решение.

Заключение от переводчика

Очевидно, здесь повествование обрывается, и хотелось бы углубиться в особенности различных библиотек для работы с датами в JS, в частности в Moment.JS, Luxon и Date FNS. Возможно в дальнейшем здесь появится ссылка на статью посвященную этим библиотекам.

Благодарю за помощь в подготовке перевода Степана Омельченко и Марию Пилипончик. Это было не просто, но мы смогли!

© Habrahabr.ru