Hi! I just added the Intended Use document in Formwork. I noticed that the time stamp is wrong (15:07 instead of 17:07). Can this be fixed? Important, because work was done out of business hours.
Hm. Weird. That difference of exactly two hours is screaming “time zone” to me. But how come is this happening? Our client is based in Germany, just like us. And our code explicitly sets the application time zone to “Europe/Berlin”. I don’t get it. Maybe he signed the document from another time zone? Or maybe our code is just not working as expected. This requires research.
I guess it’s time to learn about time zones in Rails.
Time zones in Rails
-
System time. It uses the time zone set in your server. It’s the time you get when you call Time.now and it is represented by an
Timeobject. -
Application time. It uses the time zone set by the Rails application in config.time_zone. It’s the time you get when you call
Time.zone.now,Time.currentor any expression like2.hours.from_nowand it is represented by anActiveSupport::TimeWithZoneobject. - Database time. It uses the time zone set in the database, which should always be UTC. It’s the time you get when you run a SQL query.
- (Bonus) Browser time. It uses the time zone set in the browser. It’s the time rendered by
local_time.
Time, DateTime and Date. Understandably, they all use system time. System time is OK but it’s a bit cumbersome if you suddenly want to work in a different time zone, as you’d need to change the TZ environment variable of your machine.That’s why Rails introduced its own version of Time:
ActiveSupport::TimeWithZone. It provides the same API than Time’s, making Time and TimeWithZone instances interchangeable. Well, kind of interchangeable. I found some aspects where they differ, and it’s a bit annoying. More about this later.But the good thing about TimeWithZone is that you can easily change from one time zone to another by setting
Time.zone to your favorite time zone identifier, like “America/New_York” or “Europe/Berlin”.Rails uses TimeWithZone objects all across its layers. When you run
User.first.created_at in your console, you get a TimeWithZone object, with the time offset corresponding to the time zone your set in your application. Indeed, this is what we call the application time. You can easily define the application time by tweaking config.time_zone in the config/application.rb file.Though when Rails stores those times in the database, they are first transformed to UTC, the database time. Database time is UTC by default and should be left that way.
From system time to database time
Time.now uses the system time and returns a Time object.> Time.now
=> 2023-07-10 06:19:05.0988883 +0000
> Time.now.class
=> Time
$ export TZ="America/New_York"
> Time.now
=> 2023-07-10 02:19:23.870709341 -0400
Time.zone.now, getting a TimeWithZone object.> Time.zone.now
=> Mon, 10 Jul 2023 09:19:06.363576224 CEST +02:00
> Time.zone.now.class
=> ActiveSupport::TimeWithZone
> Time.zone
=> #<ActiveSupport::TimeZone:0x00007f95b03110f8
@name="Europe/Berlin",
@tzinfo=#<TZInfo::DataTimezone: Europe/Berlin>,
@utc_offset=nil>
Time.now, always use Time.zone.now.> Time.zone.now.class
=> ActiveSupport::TimeWithZone
> Time.current.class
=> ActiveSupport::TimeWithZone
> 2.hours.from_now.class
=> ActiveSupport::TimeWithZone
> User.firt.created_at.class
=> ActiveSupport::TimeWithZone
> ale = ActiveRecord::Base.connection.execute("
SELECT * FROM users
WHERE email = '[email protected]'
").first
> database_time = ale["created_at"].to_s
=> "2023-01-16 22:09:13 UTC"
Formats: where the pain begins
Time.zone.parse.application_time = Time.zone.parse(database_time)
=> Mon, 16 Jan 2023 23:09:13.000000000 CET +01:00
We can see that if we retrieve the same value with Rails via ActiveRecord queries, the result is the same.
> User.find_by(email: "[email protected]").created_at
=> Mon, 16 Jan 2023 23:09:13.000000000 CET +01:00
Time.parse and then call the getlocal method.> system_time = Time.parse(database_time).getlocal
=> 2023-01-16 17:09:13 -0500
Time.parse maintains the same format. But TimeZone.parse did not! Hm, I don’t like that too much. It’s kind of inconsistent.Don’t panic yet, there’s still a handy way of reverting the format of our application time back to the database format: using
TimeWithZone#to_formatted_s(:db).> application_time.to_formatted_s(:db)
=> "2023-01-16 22:09:13"
OK, I sort of can understand the rationale behind it. The intention of the method is to mimic the time format in the database, which one could argue includes using the UTC timezone. Though I would say it should not be that way.
The main problem is that again this is not consistent with how Time uses
to_formatted_s(:db). For Time objects, the same method does not bring the time zone back to UTC. It stays loyal to the system’s time zone.> system_time.to_formatted_s(:db)
=> "2023-01-16 17:09:13"
Hi! I just added the Intended Use document in Formwork. I noticed that the time stamp is wrong (15:07 instead of 17:07). Can this be fixed? Important, because work was done out of business hours.
we can now see that
TimeWithZone#to_formatted_s(:db) is the one to blame here. We are displaying times in UTC without informing the user. This is not what we want: the user should see the time according to his time zone. Or otherwise be informed that the time is UTC.More users, different time zones
So our first decision here is to start using
local_time to display times in Formwork. local_time reads the browser’s time zone and updates the HTML directly on the client side using JavaScript, setting all times tagged with data-local="time" to the user’s time zone.What about exports? Should we show times in the user’s time zone? At the end, we don’t know the time zone of the person who will be reading the export. So our second decision is to show UTC times in exports while explicitly stating that they are UTC times.
This means times will be either on the user’s time zone (browser time) or they will be UTC (database time). Application time is never considered.
So… what’s the purpose of application time?
My understanding is that the main purpose of the application time is to set a default display time zone. This is really handy if
- all your users are in the same time zone because your display is correct without using JS.
- all your developers are in the same time zone because the console speaks our same time language.
Also, our preferred format in Formwork is
:db. And as we saw, that format reverts TimeWithZone objects (application time) back to UTC time. The application time is actually not displayed anywhere!Bye, bye, Miss Application Time
config.time_zone to “Europe/Berlin” as we have clients outside that time zone. Only sometimes we will display times in the browser time zone, which will be handled by local_time. But never in the application time zone.So bye bye,
config.time_zone = "Europe/Berlin".Bye bye, application time.