
Correct work with date and time in Ruby on Rails
- Tutorial
Hello! My name is Andrey Novikov and recently I am working on a project to develop an application that is used in different parts of our country and automates the work of people. In each specific time zone, our application needs to correctly receive, save and display time, both in the past and in the future - for example, calculate the start of a work shift and also display it correctly: count the time until the end of the shift, show how many people were traveling to the destination and determine whether they have met the norm, as well as much, much more.

For the past few years that I write in Ruby on Rails, I have not had to deal with similar problems - before that, all my applications worked in the same time zone. And then suddenly I had to sweat a lot, catching a variety of errors and trying to figure out how to work with the date and time so as to avoid them in the future.
As a result, today I have something to share with you. If you regularly encounter the fact that the time is saved or displayed incorrectly with a characteristic spread of several hours (3 hours for Moscow), some nightly recordings move to neighboring days, and the time is stubbornly displayed not as users want, and you don’t know what to do with all this - welcome under cat.
So, the first and most important thing - what is the time that we operate in everyday life and what it consists of?
In ordinary life, we operate with some local time , which operates where we live, however, it is difficult and dangerous to work with it in computer systems - due to the clock change (summer time, the State Duma, etc.) it is uneven and ambiguous ( more on this later). Therefore, it takes some universal time , which is uniform and unambiguous (a leap second bursts into the article and spoils everything, but we won’t talk about it), one value of which reflects the same moment in time anywhere in the world (physics, keep quiet! ) - a single reference point, its role is played by UTC- Coordinated universal time. And we also need time zones ( time zones in modern terminology) to convert local time to universal and vice versa.
And what is the time zone in general?
The first is the offset from UTC. That is, by how many hours and minutes our local time differs from UTC. Note that this does not have to be an integer number of hours. So, India, Nepal, Iran, New Zealand, parts of Canada and Australia and many others live with honors from UTC at X hours 30 minutes or X hours 45 minutes. Moreover, at some moments on the Earth there are already three dates - yesterday, today and tomorrow, since the difference between the extreme time zones is 26 hours.
Secondly, these are the rules for switching to daylight saving time. Among countries that have time zones with the same offset, some do not switch to daylight saving time at all, some switch to some numbers, others to others. Some in the summer, some in the winter (yes, we have the southern hemisphere). Some countries (including Russia) switched to daylight saving time earlier, but wisely abandoned this idea. And to correctly display the date and time in the past, all this must be taken into account. It is important to remember that when switching to summer time, it is the shift that changes (it was in Moscow before +3 hours in winter, it became +4 in summer).
In computers, information for working with this madness is stored in appropriate databases, all good libraries for working with time are able to take into account all these terrible features.
Windows seems to use some kind of database of its own, and in almost the entire world of the open-source world, the de facto standard is the IANA Time Zone Database , better known as tzdata . It stores the history of all time zones from the beginning of the Unix era, that is, from January 1, 1970: which time zones when they appeared, which when they disappeared (and which they poured), where and when they switched to daylight saving time, how lived on it and when it was canceled. Each time zone is designated as Region / Place, for example, the Moscow time zone is called Europe / Moscow. Tzdata is used in GNU / Linux, Java, Ruby (the tzinfo gem), PostgreSQL, MySQL, and many more.
Ruby on Rails uses the class
Of the disadvantages
Each “rail” has already encountered this class, setting the time zone in the file
In the application, you can access this time zone using the
Here we can already see that the identifier is used
So, the most interesting methods for us will be (all return objects of type
So, knowing all this, we can already add time zone support to any application:
In this example, we have a model
If this method returns no
We store the identifier in the database
Moreover, this is a specially complicated method by me, which converts the identifier of the form tzdata stored in the database
And it looks like a paired time zone setter method that saves the tzdata identifier to the database. It can accept either an object of the ActiveSupport :: TimeZone class or any of the identifiers as an input.
The main reason why I prefer to save the identifier
One feature of PostgreSQL that is important to keep in mind is that data types ending in with time zone do not store time zone information, but only convert the values inserted into them into UTC for storage and back to local time for display. Ruby on Rails in migrations creates columns with the type timestamp without time zone, which store the time as you write in them.
Ruby on Rails by default when connecting to the database sets the time zone in UTC. That is, during any work with the database, all work with time is done in UTC. Values in all columns are also written strictly in UTC, therefore, for example, when selecting records for a certain day, you should always remember this and send to SQL queries not just dates that the DBMS converts at midnight UTC, but timestamps storing midnight at desired time zone. And then no entries at your next date will leave.
The following query will not return records for the first three hours of the day for an application sharpened for Moscow time (UTC + 3, all things):
You must directly specify the point in time in the right time zone so that ActiveRecord converts it correctly:
Here is a "rake" that hit me on the forehead painfully not so long ago. In the application code, we had a place where time was generated on the client by constructing a new javascript Date object and implicitly casting it to a string. In this form, it was transmitted to the server. So a bug was discovered in the parse method of the Time class from the Ruby standard library, as a result of which time in the Novosibirsk time zone is not correctly parsed - the date turned out to be almost always in November:
Most importantly, we could not detect this bug until the first client used the application, which had the Novosibirsk time zone in the OS settings. By a good tradition, this customer turned out to be the customer. When developing in Moscow, you will never find this bug!
The advice follows: set a different time zone on your CI server than the one used by the developers. We discovered this property by accident, since our CI server was in UTC by default, and all developers have Moscow locally installed. Thus, we caught several previously unrevealed bugs, since the browser on the CI server started up with a time zone different from the default time zone of the rail application (and the time zone of test users).
This example illustrates the importance of using standardized machine-readable formats for exchanging information between subsystems. There wouldn’t be a previous bug if the developer immediately became interested in transmitting data in a machine-readable format.
An example of such a machine-readable format is ISO 8601. For example, this is the recommended format for transmitting time and date when serialized to JSON according to the Google JSON Style Guide .
Example time will look at it like this:
On the client, if you have moment.js, then you need a method
In my humble opinion, it is highly desirable to immediately expect time in this format and try to parse it with the appropriate class method
And if backward compatibility is not needed, then I would just catch the execution and return the 400 Bad Request error code with the message "you have a curve parameter and in general you are an evil pinocchio."
However, the previous method is still error prone - in case
But there is a place where everything can crash - look at the code carefully and read on!
When you transfer local time between systems (or store somewhere), be sure to transfer it along with the offset from UTC! The fact is that local time in itself (even with a time zone!) Is ambiguous in some situations. For example, when changing time from summer to winter, the same hour is repeated twice, once with one shift, another time with another. Last fall in Moscow, the same hour of the night first passed with a shift of +4 hours, and then passed again, but with a shift of +3. As you can see, each of these watches corresponds to a different clock in UTC. With a reverse transfer, one hour does not happen at all. Local time with a specified offset from UTC is always unambiguous. In the event that you “run into” at such a moment in time and you will not have a displacement, then
Here are some illustrative examples:
If you add a little Monkey-patching, you can teach how to
It may turn out that you may not have enough time zones “out of the box”. For example, Russian time zones are far from all, but at least there is one with each individual offset from UTC. By simply inserting ActiveSupport into the internal hash and adding translations to the i18n-timezones gem, this can be achieved. Do not try to send a pull request to Ruby on Rails - they will not accept it with the wording “we are not an encyclopedia of time zones here” ( I checked ). https://gist.github.com/Envek/cda8a367764dc2cacbc0
What is a modern web application without a rich frontend? Temper your ardor - not everything is so smooth! In pure javascript, you can only get the offset from UTC, which is now valid in the user's OS - and that’s all. Therefore, everyone is practically doomed to use the moment.js library along with its complementary moment timezone library , which drags the
Examples of use that you will definitely need:
In case you already have the correct and good timestamp in ISO8601 format, then just feed it to the method of
If you have a timestamp in the local time zone, then Moment Timezone needs to be informed in what time zone it is, then the analysis is carried out as follows:
If everywhere in the application you analyze time using these methods (forget about it
For a very rich frontend based on fashionable frameworks, see separate libraries for them. For example, we use angular-moment , which allows you to dynamically set the time zone for the entire application and automatically display all the time on the page in this time zone using special directives. If you use angular - wildest recommend.
General recommendations that work in 90% of cases are as follows:
If this information is not enough for someone, read the useful article on Habrahabr by Vladimir Rudnyh from Mail.ru - it tells much more about the different nuances of working with time zones and time in general, especially if it is in the future: http://habrahabr.ru / company / mailru / blog / 242645 /
There is also an interesting educational video from Tom Scott in which he talks about where all these problems with time zones came from and how much more understandable and interesting than me, but in English:
Well, of course the documentation! She is your main friend and you can learn a lot from it that is beyond the scope of this article:
PS> This article is based on my presentation at DevConf 2015. You can familiarize yourself with the slides here , and the video is posted here by the great guys from RailsClub. By the way, this year we are again sponsors of the RailsClub conference - see you soon there!

For the past few years that I write in Ruby on Rails, I have not had to deal with similar problems - before that, all my applications worked in the same time zone. And then suddenly I had to sweat a lot, catching a variety of errors and trying to figure out how to work with the date and time so as to avoid them in the future.
As a result, today I have something to share with you. If you regularly encounter the fact that the time is saved or displayed incorrectly with a characteristic spread of several hours (3 hours for Moscow), some nightly recordings move to neighboring days, and the time is stubbornly displayed not as users want, and you don’t know what to do with all this - welcome under cat.
So, the first and most important thing - what is the time that we operate in everyday life and what it consists of?
In ordinary life, we operate with some local time , which operates where we live, however, it is difficult and dangerous to work with it in computer systems - due to the clock change (summer time, the State Duma, etc.) it is uneven and ambiguous ( more on this later). Therefore, it takes some universal time , which is uniform and unambiguous (a leap second bursts into the article and spoils everything, but we won’t talk about it), one value of which reflects the same moment in time anywhere in the world (physics, keep quiet! ) - a single reference point, its role is played by UTC- Coordinated universal time. And we also need time zones ( time zones in modern terminology) to convert local time to universal and vice versa.
And what is the time zone in general?
The first is the offset from UTC. That is, by how many hours and minutes our local time differs from UTC. Note that this does not have to be an integer number of hours. So, India, Nepal, Iran, New Zealand, parts of Canada and Australia and many others live with honors from UTC at X hours 30 minutes or X hours 45 minutes. Moreover, at some moments on the Earth there are already three dates - yesterday, today and tomorrow, since the difference between the extreme time zones is 26 hours.
Secondly, these are the rules for switching to daylight saving time. Among countries that have time zones with the same offset, some do not switch to daylight saving time at all, some switch to some numbers, others to others. Some in the summer, some in the winter (yes, we have the southern hemisphere). Some countries (including Russia) switched to daylight saving time earlier, but wisely abandoned this idea. And to correctly display the date and time in the past, all this must be taken into account. It is important to remember that when switching to summer time, it is the shift that changes (it was in Moscow before +3 hours in winter, it became +4 in summer).
In computers, information for working with this madness is stored in appropriate databases, all good libraries for working with time are able to take into account all these terrible features.
Windows seems to use some kind of database of its own, and in almost the entire world of the open-source world, the de facto standard is the IANA Time Zone Database , better known as tzdata . It stores the history of all time zones from the beginning of the Unix era, that is, from January 1, 1970: which time zones when they appeared, which when they disappeared (and which they poured), where and when they switched to daylight saving time, how lived on it and when it was canceled. Each time zone is designated as Region / Place, for example, the Moscow time zone is called Europe / Moscow. Tzdata is used in GNU / Linux, Java, Ruby (the tzinfo gem), PostgreSQL, MySQL, and many more.
Ruby on Rails uses the class
ActiveSupport::TimeZone
supplied with the library to work with time zonesActiveSupport
from the standard Ruby on Rails bundle. It is a wrapper around the tzinfo gem , which in turn provides a ruby interface to tzdata . It provides methods for working with time, and is also actively used in ActiveSupport's extended Time class from the Ruby standard library to fully work with time zones. Well, in the class ActiveSupport::TimeWithZone
from Ruby on Rails, which stores in itself not only time with an offset, but also the time zone itself. Many methods in the time zone return objects ActiveSupport::TimeWithZone
, but in most cases you will not even feel it. What is the difference between these two classes is written in the documentation , and this difference is useful to know. Of the disadvantages
ActiveSupport::TimeZone
it can be noted that he uses his own “human-readable” identifiers for time zones, which sometimes creates inconvenience, and also that these identifiers are not for all time zones available in tzdata, but this is fixable. Each “rail” has already encountered this class, setting the time zone in the file
config/application.rb
after creating a new application:config.time_zone = 'Moscow'
In the application, you can access this time zone using the
zone
class method Time
. Here we can already see that the identifier is used
Moscow
instead Europe/Moscow
, but if you look at the inspect
time zone object in the method output , we will see that inside there is a mapping to the tzdata identifier : > Time.zone
=> #>
So, the most interesting methods for us will be (all return objects of type
ActiveSupport::TimeWithZone
):- A method
now
that returns the current time in a given time zone.Time.zone.now # => Sun, 16 Aug 2015 22:47:28 MSK +03:00
- A method
parse
that, like the methodparse
of a classTime
, parses a string with time into a class objectTime
, but at the same time immediately translates it into the time zone of this object. If the offset from UTC is not indicated in the line, then this method will also decide that the local time of this time zone is indicated in the line.ActiveSupport::TimeZone['Novosibirsk'].parse('2015-06-19T12:13:14') # => Fri, 19 Jun 2015 12:13:14 NOVT +06:00
- The method
at
converts a Unix timestamp (the number of seconds since January 1, 1970), which, as you know, is always in UTC, to a type objectTime
in this time zone.Time.zone.at(1234567890) #=> Sat, 14 Feb 2009 02:31:30 MSK +03:00
- And a method
local
that allows you to programmatically construct the time in the right time zone from individual components (year, month, day, hour, and so on).ActiveSupport::TimeZone['Yakutsk'].local(2015, 6, 19, 12, 13, 14) # => Fri, 19 Jun 2015 12:13:14 YAKT +09:00
ActiveSupport::TimeZone
also actively used in operations with class objects Time
and adds several useful methods to it, for example:- The class method
Time.zone
will return a class objectActiveSupport::TimeZone
representing the time zone that is currently valid throughout the application (and it can be changed). - And the class method
Time.zone_default
will return the time zone that you specified in the fileconfig/application.rb
. - The method
with_zone
allows you to temporarily change the current time zone for all code that runs in the block passed to it. - Well, the method of the object
Time#in_time_zone
allows you to change the time zone of an existing object (it will return an object of typeActiveSupport::TimeWithZone
):Time.parse('2015-06-19T12:50:00').in_time_zone('Asia/Tokyo') # => Fri, 19 Jun 2015 18:50:00 JST +09:00
Time.current
along with Date.current
and Time.now
together with Date.today
. The difference between them is that the first (those that current
) return the time or date in the time zone of the application, as an object of the type ActiveSupport::TimeWithZone
, in the same belt that currently returns the method Time.zone
and adds these Ruby on Rails methods, and the second returns the time in time zone, attention, server operating system and go to the standard Ruby library (return, respectively, simply Time
). Be careful - there may be strange bugs that cannot be played locally, so always use Time.current
and Date.current
. So, knowing all this, we can already add time zone support to any application:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
around_action :with_time_zone, if: 'current_user.try(:time_zone)'
protected
def with_time_zone(&block)
time_zone = current_user.time_zone
logger.debug "Используется часовой пояс пользователя: #{time_zone}"
Time.use_zone(time_zone, &block)
end
end
In this example, we have a model
User
with a certain method time_zone
that returns an object ActiveSupport::TimeZone
with the user's time zone. If this method returns no
nil
, then using the callback around_action
we call the class method Time.use_zone
and continue processing the request in the block passed to it. Thus, all times in all views will be automatically displayed in the user's time zone. Voila! We store the identifier in the database
tzdata
, and to convert it to an object, use this method in the file app/models/user.rb
:# Инициализирует объект класса +ActiveSupport::TimeZone+ для работы с
# часовым поясом, хранящимся в БД как идентификатор TZ database.
def time_zone
unless @time_zone
tz_id = read_attribute(:time_zone)
as_name = ActiveSupport::TimeZone::MAPPING.select do |_,v|
v == tz_id
end.sort_by do |k,v|
v.ends_with?(k) ? 0 : 1
end.first.try(:first)
value = as_name || tz_id
@time_zone = value && ActiveSupport::TimeZone[value]
end
@time_zone
end
Moreover, this is a specially complicated method by me, which converts the identifier of the form tzdata stored in the database
Europe/Moscow
into an object ActiveSupport::TimeZone
whose identifier is simple Moscow
. The reason that I stored in the id
time zone of tzdata
rather than rail, lies in interoperability - id
from tzdata
understanding everything, and id
time zone rails - only Ruby on Rails. And it looks like a paired time zone setter method that saves the tzdata identifier to the database. It can accept either an object of the ActiveSupport :: TimeZone class or any of the identifiers as an input.
# Сохраняет в базу данных идентификатор часового пояса из TZ Database,
# у объекта устанавливает часовой пояс — объект +ActiveSupport::TimeZone+
def time_zone=(value)
tz_id = value.respond_to?(:tzinfo) && value.tzinfo.name || nil
tz_id ||= TZInfo.Timezone.get(ActiveSupport::TimeZone::MAPPING[value.to_s] || value.to_s).identifier rescue nil # Неизвестный идентификатор — игнорируем
@time_zone = tz_id && ActiveSupport::TimeZone[ActiveSupport::TimeZone::MAPPING.key(tz_id) || tz_id]
write_attribute(:time_zone, tz_id)
end
The main reason why I prefer to save the identifier
tzdata
to the database is that PostgreSQL we use works well with time zones. Having an identifier in the database tzdata
, it is quite convenient to look at local time in the user's time zone and debate various problems with time zones using queries of the form:SELECT '2015-06-19T12:13:14Z' AT TIME ZONE 'Europe/Moscow';
One feature of PostgreSQL that is important to keep in mind is that data types ending in with time zone do not store time zone information, but only convert the values inserted into them into UTC for storage and back to local time for display. Ruby on Rails in migrations creates columns with the type timestamp without time zone, which store the time as you write in them.
Ruby on Rails by default when connecting to the database sets the time zone in UTC. That is, during any work with the database, all work with time is done in UTC. Values in all columns are also written strictly in UTC, therefore, for example, when selecting records for a certain day, you should always remember this and send to SQL queries not just dates that the DBMS converts at midnight UTC, but timestamps storing midnight at desired time zone. And then no entries at your next date will leave.
The following query will not return records for the first three hours of the day for an application sharpened for Moscow time (UTC + 3, all things):
News.where('published_at >= ? AND published_at <= ?', Date.today, Date.tomorrow)
You must directly specify the point in time in the right time zone so that ActiveRecord converts it correctly:
News.where('published_at >= ? AND published_at < ?', Time.current.beginning_of_day, Time.current.beginning_of_day + 1.day)
# => News Load (0.8ms) SELECT "news".* FROM "news" WHERE (published_at >= '2015-08-16 21:00:00.000000' AND published_at < '2015-08-17 21:00:00.000000') ORDER BY "news"."published_at" DESC
Serialization and transfer of date and time
Here is a "rake" that hit me on the forehead painfully not so long ago. In the application code, we had a place where time was generated on the client by constructing a new javascript Date object and implicitly casting it to a string. In this form, it was transmitted to the server. So a bug was discovered in the parse method of the Time class from the Ruby standard library, as a result of which time in the Novosibirsk time zone is not correctly parsed - the date turned out to be almost always in November:
Time.parse('Mon May 18 2015 22:16:38 GMT+0600 (NOVT)') # => 2015-11-01 22:16:38 +0600
Most importantly, we could not detect this bug until the first client used the application, which had the Novosibirsk time zone in the OS settings. By a good tradition, this customer turned out to be the customer. When developing in Moscow, you will never find this bug!
The advice follows: set a different time zone on your CI server than the one used by the developers. We discovered this property by accident, since our CI server was in UTC by default, and all developers have Moscow locally installed. Thus, we caught several previously unrevealed bugs, since the browser on the CI server started up with a time zone different from the default time zone of the rail application (and the time zone of test users).
This example illustrates the importance of using standardized machine-readable formats for exchanging information between subsystems. There wouldn’t be a previous bug if the developer immediately became interested in transmitting data in a machine-readable format.
An example of such a machine-readable format is ISO 8601. For example, this is the recommended format for transmitting time and date when serialized to JSON according to the Google JSON Style Guide .
Example time will look at it like this:
2015-05-18T22:16:38+06:00
. On the client, if you have moment.js, then you need a method
toISOString()
. And, for example, Angular.js serializes time in ISO 8601 by default (and does it right!).In my humble opinion, it is highly desirable to immediately expect time in this format and try to parse it with the appropriate class method
Time
, and parse
leave the method for backward compatibility. Like this:Time.iso8601(params[:till]) rescue Time.parse(params[:till])
And if backward compatibility is not needed, then I would just catch the execution and return the 400 Bad Request error code with the message "you have a curve parameter and in general you are an evil pinocchio."
However, the previous method is still error prone - in case
params[:till]
time is transferred without an offset from UTC, both methods ( iso8601
and parse
) will parse it as if it were local time in the server’s time zone , and not the application. So you know what time zone your server is in? I have in different. A more bulletproof time parsing method will look like this (unfortunately ActiveSupport::TimeZone
there is no method iso8601
, but sorry):Time.strptime(params[:till], "%Y-%m-%dT%H:%M:%S%z").in_time_zone rescue Time.zone.parse(params[:till])
But there is a place where everything can crash - look at the code carefully and read on!
When you transfer local time between systems (or store somewhere), be sure to transfer it along with the offset from UTC! The fact is that local time in itself (even with a time zone!) Is ambiguous in some situations. For example, when changing time from summer to winter, the same hour is repeated twice, once with one shift, another time with another. Last fall in Moscow, the same hour of the night first passed with a shift of +4 hours, and then passed again, but with a shift of +3. As you can see, each of these watches corresponds to a different clock in UTC. With a reverse transfer, one hour does not happen at all. Local time with a specified offset from UTC is always unambiguous. In the event that you “run into” at such a moment in time and you will not have a displacement, then
Time.parse
just returns you an earlier point in time, and Time.zone.parse
throws an exception TZInfo::AmbiguousTime
. Here are some illustrative examples:
Time.zone.parse("2014-10-26T01:00:00")
# TZInfo::AmbiguousTime: 2014-10-26 01:00:00 is an ambiguous local time.
Time.zone.parse("2014-10-26T01:00:00+04:00")
# => Sun, 26 Oct 2014 01:00:00 MSK +04:00
Time.zone.parse("2014-10-26T01:00:00+03:00")
# => Sun, 26 Oct 2014 01:00:00 MSK +03:00
Time.zone.parse("2014-10-26T01:00:00+04:00").utc
# => 2014-10-25 21:00:00 UTC
Time.zone.parse("2014-10-26T01:00:00+03:00").utc
# => 2014-10-25 22:00:00 UTC
Various useful tricks
If you add a little Monkey-patching, you can teach how to
timezone_select
display Russian time zones first or even unique. In the future, it will be possible to do without this - I sent a Pull Request to Ruby on Rails, but for now, unfortunately, it hangs without activity: https://github.com/rails/rails/pull/20625# config/initializers/timezones.rb
class ActiveSupport::TimeZone
@country_zones = ThreadSafe::Cache.new
def self.country_zones(country_code)
code = country_code.to_s.upcase
@country_zones[code] ||=
TZInfo::Country.get(code).zone_identifiers.select do |tz_id|
MAPPING.key(tz_id)
end.map do |tz_id|
self[MAPPING.key(tz_id)]
end
end
end
# Где-то в app/views
= f.input :time_zone, priority: ActiveSupport::TimeZone.country_zones(:ru)
It may turn out that you may not have enough time zones “out of the box”. For example, Russian time zones are far from all, but at least there is one with each individual offset from UTC. By simply inserting ActiveSupport into the internal hash and adding translations to the i18n-timezones gem, this can be achieved. Do not try to send a pull request to Ruby on Rails - they will not accept it with the wording “we are not an encyclopedia of time zones here” ( I checked ). https://gist.github.com/Envek/cda8a367764dc2cacbc0
# config/initializers/timezones.rb
ActiveSupport::TimeZone::MAPPING['Simferopol'] = 'Europe/Simferopol'
ActiveSupport::TimeZone::MAPPING['Omsk'] = 'Asia/Omsk'
ActiveSupport::TimeZone::MAPPING['Novokuznetsk'] = 'Asia/Novokuznetsk'
ActiveSupport::TimeZone::MAPPING['Chita'] = 'Asia/Chita'
ActiveSupport::TimeZone::MAPPING['Khandyga'] = 'Asia/Khandyga'
ActiveSupport::TimeZone::MAPPING['Sakhalin'] = 'Asia/Sakhalin'
ActiveSupport::TimeZone::MAPPING['Ust-Nera'] = 'Asia/Ust-Nera'
ActiveSupport::TimeZone::MAPPING['Anadyr'] = 'Asia/Anadyr'
# config/locales/ru.yml
ru:
timezones:
Simferopol: Республика Крым и Севастополь
Omsk: Омск
Novokuznetsk: Новокузнецк
Chita: Чита
Khandyga: Хандыга
Sakhalin: Сахалин
Ust-Nera: Усть-Нера
Anadyr: Анадырь
Javascript
What is a modern web application without a rich frontend? Temper your ardor - not everything is so smooth! In pure javascript, you can only get the offset from UTC, which is now valid in the user's OS - and that’s all. Therefore, everyone is practically doomed to use the moment.js library along with its complementary moment timezone library , which drags the
tzdata
user directly into the browser (yes, users will again have to download extra kilobytes). But, nevertheless, with the help of it you can do anything. Well, or almost everything. Examples of use that you will definitely need:
In case you already have the correct and good timestamp in ISO8601 format, then just feed it to the method of
parseZone
the Moment itself:moment.parseZone(ISO8601Timestamp)
If you have a timestamp in the local time zone, then Moment Timezone needs to be informed in what time zone it is, then the analysis is carried out as follows:
moment.tz(timestamp, formatString, timezoneIdentifier)
If everywhere in the application you analyze time using these methods (forget about it
new Date()
!), Then everything will be fine with you and you will soon forget about “jumping time” and it will become much calmer. For a very rich frontend based on fashionable frameworks, see separate libraries for them. For example, we use angular-moment , which allows you to dynamically set the time zone for the entire application and automatically display all the time on the page in this time zone using special directives. If you use angular - wildest recommend.
Summary
General recommendations that work in 90% of cases are as follows:
- Store and transmit time about past and ongoing right now (i.e., recorded) events in UTC.
- Over time, in the future everything is somewhat more complicated. Decide what’s more important for you, in the event of unforeseen changes in time zones, local time or UTC time.
- Ideally, you need to store three values: local time, time in UTC and the identifier of the time zone. In this case, you will be able to find that for some time "went" in advance and take any measures.
- If you still want to be able to catch the appearance of new time zones, then you can save the user's geographical coordinates.
- For the same reason that time zones change over time, and also because of the availability of summer time, it is extremely important to store the time zone identifier, and not just the offset.
- But if you do not know the time zone, then store the offset - this is better than nothing.
- It is better not to believe the time from the client, because it can be wrong - whether it is by chance, or it can be intentionally changed, the time zone or the offset from UTC can also be completely arbitrary.
- Well, the last, but important - on your servers always keep the configured NTP and the latest version of the package
tzdata
(remember that some software carries with it its own copytzdata
).
If this information is not enough for someone, read the useful article on Habrahabr by Vladimir Rudnyh from Mail.ru - it tells much more about the different nuances of working with time zones and time in general, especially if it is in the future: http://habrahabr.ru / company / mailru / blog / 242645 /
There is also an interesting educational video from Tom Scott in which he talks about where all these problems with time zones came from and how much more understandable and interesting than me, but in English:
Well, of course the documentation! She is your main friend and you can learn a lot from it that is beyond the scope of this article:
- http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html
- http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html
- http://api.rubyonrails.org/classes/Time.html
PS> This article is based on my presentation at DevConf 2015. You can familiarize yourself with the slides here , and the video is posted here by the great guys from RailsClub. By the way, this year we are again sponsors of the RailsClub conference - see you soon there!