📜 ⬆️ ⬇️

Working with time zones in javascript



Recently, I was working on the task of adding time zones to the JS-library of the calendar, which my team is leading. I was well aware of the useless support for time zones in JavaScript, but I hoped that abstracting existing data objects would make it easy to solve most of the difficulties.

However, my dreams were ruined. When I delved into the task, I realized that in this language it is really difficult to work with time zones. Implementing something more complicated than simply formatting the time display and calculating the date using complex operations (calendar functions) was extremely difficult. I gained valuable experience solving this problem, and this entailed new difficulties.

In this article I want to discuss what I encountered and how I solved it. While I was writing the text, I realized that the cause of all adversity was my poor understanding of the very theme of time zones. In the light of this awareness, I suggest first to talk in detail about definitions and standards, and only then move on to JavaScript.

What is the time zone?


The time zone is a geographic region in which the uniform local time is used, set by the government of the country. Many countries entirely belong to some specific time zones, and in the territories of large states, like Russia and the United States, several time zones are used. It is curious that although China is also quite large, however, it adopted only one time zone. Sometimes this leads to strange situations when sunrise begins in the western part of the country around 10 am.

GMT, UTC and offset


GMT


Local South Korean time is designated as GMT+09:00 . GMT stands for Greenwich Mean Time (Greenwich Mean Time), that is, the time on the Royal Observatory clock in Greenwich, United Kingdom. It is located on the zero meridian. The GMT-system's radio signal began broadcasting on February 5, 1924, and it itself became a world standard on January 1, 1972.

UTC


Many people believe that GMT and UTC are the same thing, often using them as interchangeable systems. But this is a mistake. The UTC system appeared in 1972 as a way to compensate for the effect of the rotation of the Earth. The system is based on International Atomic Time (International Atomic Time), calculated from the frequency of electromagnetic oscillations of cesium atoms. In other words, UTC is a more accurate replacement for GMT. Although the real time difference between the two systems is very small, it is still better for software developers to rely on UTC.

An interesting fact: when UTC was still being developed, in English-speaking countries it was suggested to call it CUT (Coordinated Universal Time), and in French-speaking countries - TUC (Temps Universal Coordonn). However, none of the camps could not win, and the system agreed to call UTC, by letter of both the proposed options (C, T and U).

Bias


+09:00 in UTC+09:00 means that local time is 9 hours ahead of standard UTC time. That is, when it is 9 pm in South Korea, it is noon in the UTC region. The difference between standard UTC-time and local is called “offset”, which is expressed as positive or negative values: +09:00 , -03:00 , etc.

In many countries, it is customary to give your time zones unique names. For example, the time zone of South Korea is called KST (Korea Standard Time), its offset is expressed as KST = UTC+09:00 . However, the +09:00 offset is used not only by South Korea, but also by Japan, Indonesia and many other countries, therefore the relationship between offsets and belts is expressed not 1: 1, but 1: N. The list of countries with offset +09:00 is presented here .

Some offsets operate not only on the clock. For example, in North Korea, the standard time is +08:30 , and in Australia, in some regions, they use +8:45 , +09:30 and +10:30 .

The full list of UTC offsets is here .

Time zone! == offset?


As I already said, we use time zone names (KST, JST) with displacements interchangeably, not distinguishing them. But it will be wrong to consider the same time and the shift of a particular region. There are several reasons:

Summer time (DST)


In some countries, this term is unknown, but in many states there is a daylight saving time, mainly in Europe. For this, the international term DST is adopted - Daylight Saving Time. It means the transfer of hours in the summer period one hour ahead of the relative standard time.

For example, California uses PST (Pacific Standard Time, Pacific Standard Time) in winter, and PDT (Pacific Daylight Time, UTC-07:00 ) in summer. In the United States and Canada, the term Pacific Time (PT, Pacific Time) applies to regions that use two time zones.

When does summer time begin and end? It all depends on the country. For example, in the United States and Canada until 2006, DST was used from 2 am on the first Sunday of April to 12 am on the last Sunday of October. And from 2007, summertime began to count from 2 am on the second Sunday of March to 2 am on the first Sunday of November. In Europe, in different countries, the progressive use of DST is practiced depending on each time zone.

Do time zones change?


Each country itself determines which time zones to use it, so the local time may vary for any political and / or economic reasons. For example, in the USA, the DST borders were changed in 2007, because George Bush initiated an energy policy in 2005. Egypt and Russia used to switch to summer time, but they refused from 2011.

In some cases, the government can change not only the order of daylight saving time, but also the standard time. For example, earlier on Samoa UTC-10:00 offset was used, but then they switched to UTC+14:00 to reduce losses in trade due to the time difference with Australia and New Zealand. This decision led to the loss of the whole day from the life of the country - December 30, 2011, about which newspapers around the world wrote.

In the Netherlands, since 1909, an offset of +0:19:32.13 , since 1937 the country has moved to +00:20 , and since 1940 to +01:00 , since then most of the time has not changed there.

Time Zone 1: N Offset


So, the time zone can have one or more offsets. What time is taken as the standard depends on the current political and / or economic reasons in a particular country.

In everyday life, this does not cause difficulties until you try to systematize this data on the basis of some rules. Imagine that you want to set a standard time on your smartphone using an offset. If you live in a region where daylight saving time is practiced, then the smartphone should know exactly when to go back and forth. That is, you need to establish the relationship between standard and summer time in the same time zone (for example, Pacific Time).

But this can not be done with a couple of simple rules. For example, when the beginning and the end of DST were changed in the USA in 2007, on May 31, 2006 it was supposed to use PDT ( -07:00 ) as the standard time, and on March 31, 2007 - PST ( -08:00 ). It turns out that in order to refer to a specific time zone, you need to know the whole history of changing time zones or the date of changing the rules for daylight saving time.

You can say: "The time zone in New York is PST ( -08:00 )." However, it is necessary to clarify: "The current time zone in New York is PST." At the same time for the exact implementation of the system you need to use an even more accurate expression. Forget the term "time zone". You have to say: “PST is now used as a standard time in New York.”

So what should we use instead of offset to determine the time zone of a particular region? The name of this region. More precisely, you should group into a single time zone those regions in which the same time goes to daylight saving time and standard time. Names like PT (Pacific Time) can be used, but they only combine current standard and summer time, and do not necessarily take into account all historical changes. Moreover, since PT is only available in the United States and Canada, you need to rely on established standards from reputable organizations to ensure the versatility of your software.

IANA Time Zone Database


I must confess that time zone information is rather a database, not a set of rules, because this information should contain all relevant historical changes. There are several standard databases designed for solving problems related to time zones. The most commonly used IANA Time Zone Database , and usually called its tz database (or tzdata). The database contains historical data on changes in standard time and DST across the globe. Moreover, it is organized so that you can check all the historical data and verify the accuracy of time since the Unix time ( 1970.01/01 00:00:00 ). Although you can find information in the database until 1970, their accuracy is not guaranteed.

The naming convention uses the “region / place” rule. As the region is usually used the name of the continent or ocean (Asia, America, Pacific), and as a place - the names of major cities (Seoul, New York). The reason is that cities usually exist longer than countries. For example, the time zone of South Korea is Asia/Seoul , and that of Japan is Asia/Tokyo . Although both countries use the same offset UTC+09:00 , their local time varied in different ways, so they were divided into different time zones.

The IANA database is run by many communities of developers and historians. Freshly-available historical data is immediately entered into it and current policies are updated, so that today the base can be considered the most reliable source. Moreover, it is used under the hood of many Unix-systems, including Linux and MacOS, as well as a number of popular programming languages, including Java and PHP.

Please note that Windows uses a Microsoft database . However, it is inaccurate in terms of historical data and is supported only by Microsoft itself. Therefore, the base is less reliable than the IANA base.

JavaScript and IANA base


The time zone-related functionality is implemented in JavaScript. By default, the language uses the current zone of the region (more precisely, the belt selected when installing the OS), and there is no way to change it. Moreover, even the specifications for the standard database in JavaScript are vague, and you will understand this yourself if you decide to deal with the specification for ES2015. About the local time zone and the availability of DST there are only a couple of vague statements. For example, DST is defined as: ECMAScript 2015 - Daylight Saving Time Adjustment .

This is an implementation-dependent algorithm that uses the best available time zone information to determine the Daylight SavingTA (t) setting for local time, which is calculated in milliseconds. The ECMAScript implementation should help best define the local summer time setting.

Looks like they just say: "Dude, try to make it work." Among other things, you have to solve the problem of compatibility with different browsers. You say, “What a mess!”, And then read the following line:

Note: We recommend that you use information from the IANA time zone database http://www.iana.org/time-zones/ in your implementations.

Yes. ECMA specifications give you the ball such a simple recommendation to use the IANA database, because JavaScript does not have a special standard database. As a result, different browsers use their own time zone operations to calculate time, which are often incompatible with each other. Later, the option of using IANA data in ECMA-402 Intl.DateTimeFormat format was added to ECMA for the international API. But this option is much less reliable than analogs in other programming languages.

Time Zone in Server-Client Environment


Consider a simple scenario: we need to define a time zone. Suppose we create a calendar that will process time information. When a user in the client environment enters the date and time in the registration window, the date is transferred to the server and stored in the database. Then the client receives from the server the date registered in the schedule for display on the screen.

Here you need to decide on something. What if some of the clients accessing the server are in different time zones? The event in the schedule, which is registered in Seoul on March 10, 2017 at 23.30, in New York should be displayed as March 10, 2017 at 9.30. In order for the server to serve clients from different time zones, the schedule stored on it must contain absolute values ​​that do not depend on the belt in any way. Each server has its own way of storing such values, this question is beyond the scope of the article, since everything depends on the particular server or database. In general, the date and time transmitted from the client to the server should be presented either as values ​​based on a single offset (usually UTC), or as values ​​containing information about the time zone of the client environment.

Typically, such data is transmitted in the form of Unix-time in UTC format or according to the ISO-8601 standard with offset information. In our example, if we convert Seoul to Unix-time at 21.30 on March 10, 2017, we get the integer value 1489113000 . And in ISO-8601 format, the string value will be 2017–03–10T11:30:00+09:00 .

If you are using JavaScript in your browser environment, you must convert the entered value as described above, and then convert it back to fit your custom time zone. We need to solve both of these problems. From the point of view of a programming language, the first operation is called “parsing,” and the second is “formatting.” Now let's see how this is done in javascript.

Even when you work with JS in a server environment using Node.js, you may need to parse the data received from the client. But since the time zones of servers and databases are usually synchronized, and formatting is imposed on clients, in a browser environment you need to determine several factors. Further I will explain with reference to the browser environment.

Javascript date object


Tasks that involve working with a given data or time are solved using the Date object. This is a native object defined in ECMAScript as Array or Function . That is, it is, for the most part, implemented using native code like C ++. The API is well described in the MDN documentation . The object was greatly influenced by the java.util.Date class from Java, so it inherited some undesirable properties, such as the characteristics of changeable data and a month starting from zero.

Under the hood, the Date object in JavaScript works with time using absolute values ​​in Unix-time format. But constructors and methods like parse() , getHour() , setHour() and other functions are affected by the client's time zone (more precisely, the belt specified in the OS in which the browser is running). So if you create a Date object directly using the data entered by the user, then the local time zone of the client will be reflected in this data.

As I mentioned, JavaScript does not provide any way to arbitrarily change the time zone. Therefore, we will assume that we can directly use the value of the time zone specified in the browser.

Creating a Date object using user-entered data


Let's return to the first example. Suppose a user entered Seoul time at 11:30 am on March 11, 2017. This data is saved as five numbers: 2017, 2, 11, 11, and 30 — year, month, day, hour, and minute, respectively (since the month starts at 0, its the value should be 3–1 = 2). Using the constructor, you can easily create a Date object:

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

If you look at the value returned by d1.toString() , you will see that the absolute value of the created object is 11.00 March 11, 2017, it is calculated using +09:00 mixing (KST).

You can use in the constructor and string data. If you apply them to Date , the object will internally call Date.parse() and calculate the correct value. This feature supports the specifications of RFC2888 and ISO-8601 . But the MDN documentation for Date.parse () says that the value returned by this method depends on the browser, and the format of the string type can affect the exact final value. Therefore, it is better not to use this method. For example, in Safari and Internet Explorer, a string value like 2015–10–12 12:00:00 returns NaN , and in Chrome and Firefox it returns the local time zone. In some situations, a UTC-based value is returned.

Creating a Date object using server data


Suppose you want to get data from a server. If they are in the form of numeric Unix time, then you can simply use a constructor to create a Date object. I haven’t mentioned that when the Date constructor gets one value as a single parameter, it calculates the Unix time value in milliseconds (note: JS processes Unix time in milliseconds. This means that the second value needs to be multiplied by 1000). When executing the following code, we will get the same value as in the previous example:

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

And if instead of Unix-time to use the string type ISO-8601? As I explained above, then the Date.parse() method becomes unreliable and it’s better not to use it. However, starting with ECMAScript 5, you can use the string in ISO-8601 format in the Date Designer in Internet Explorer 9.0 and higher.

If you are not using the most recent browser, then make sure that the letter Z at the end of the values. Without it, your old browser can interpret the value based on local time, not UTC. Here is an example of using 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" 

According to the specification, in both cases the same value should be obtained. But they are different. In a later browser, the values ​​will be the same. To avoid this problem, always add a Z at the end of the line if there is no time zone information.

Creating a date to send to the server


Now you can use the previously created Date , freely extract or add time based on local time zones. Only at the end of processing do not forget to convert the data to the previous format before returning to the server.

If this is Unix time, you can use the getTime() method (don't forget about milliseconds).

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

What about the string value in ISO-8601 format? As I said, Internet Explorer 9.0 and above support ECMAScript 5, and later versions support ISO-8601. Therefore, using the toISOString() or toJSON() methods, you can create a string in ISO-8601 (you can use toJSON() for recursive calls with JSON.stringify() and others). Both methods give the same result, except for the cases of processing incorrect data:

 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 

To create a UTC string you can use the toGMTString() or toUTCString() methods. This will result in a value that meets the requirements of RFC-1123 .

The Date object includes toString() , toLocaleString() and their extension methods. But there is little benefit from them, since they are mainly used to return rows based on the local time zone, and the return values ​​depend on the browser and OS.

Changing the local time zone


As you can see, JS has some support for time zones. А если нужно в приложении изменить местный пояс без учёта указанного в ОС значения? Или если нужно в одном приложении отобразить одновременно несколько часовых поясов? Как я уже несколько раз говорил, JS не позволяет вручную менять местный часовой пояс. Единственным решением будет добавление или удаление смещения из даты, для которой вы уже знаете значение смещения нужного пояса. Но не огорчайтесь. Быть может, есть другой путь.

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

В этом сценарии можно использовать метод getTimeZoneOffset() . Это единственный API в JavaScript, который позволяет получить информацию о местном часовом поясе. Метод возвращает значение смещения текущего пояса в минутах:

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

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

 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() показывает корректные дату и время в соответствии с разницей часовых поясов Сеула и Нью-Йорка. Похоже, мы нашли простое решение. А можно ли преобразовать в местный часовой пояс, если мы знаем смещение нужного региона? К сожалению, нет. Я уже говорил, что данные о часовых поясах — это база данных, хранящая все исторические изменения смещений. Так что для получения корректного значения часового пояса вы должны знать смещение на нужную дату (не текущую).

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


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

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

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

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

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

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

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

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

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

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

Moment Timezone


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

Я не буду подробно рассказывать об использовании библиотеки или её структуре. Лишь покажу, как с её помощью можно легко решить вышеописанные задачи. За подробностями отправляю к документации .

Итак, воспользуемся 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, в которой аккуратно применено смещение. Получается гораздо проще приведённого мной выше решения.

Conclusion


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

Я пытался сделать всё самостоятельно, но не смог, что не удивительно. После многочисленных попыток могу дать совет: используйте библиотеку. Начиная писать статью, я ещё не знал, что сказать в заключении, а теперь знаю. Не рекомендую использовать сторонние библиотеки вслепую, не зная поддерживаемых ими возможностей JavaScript и возможных подводных камней. Как и всегда, выбирайте правильный инструмент под свою ситуацию.

useful links


Source: https://habr.com/ru/post/438286/