Time.now vs Time.current vs DateTime.now

I can never remember which of the many ways to get the time I should use and I always have to look it up. I always find the same StackOverflow answer that says it is critical that I use the correct one but I never know which one that is. Even after reading the answers.

I always want the time in UTC. Sometimes, I want to convert a timestamp to the user’s local time to write onto a page.

7 Likes

From Thoughtbot’s It's About Time (Zones)

DON’T USE

* Time.now
* Date.today
* Date.today.to_time
* Time.parse("2015-07-04 17:05:37")
* Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z")

DO USE

* Time.current
* 2.hours.ago
* Time.zone.today
* Date.current
* 1.day.from_now

It’s a shame that I can’t use Time.now to get the time and Date.today to get the date.

2 Likes

I am not sure about blanket advice here. Time.now can be fine provided your config is config.time_zone = 'UTC' and don’t need ActiveSupport::TimeWithZone . At Discourse for example everything on the server is UTC and client side is in charge of per user time zones like this control for example: 2020-02-11T23:58:00Z

We have made some progress around getting away without TimeWithZone at https://bugs.ruby-lang.org/issues/14850

@Andrew_White any more advice here?

An interesting aside is that we find databases losing fidelity to be a far more annoying issue, in fact we have a custom rubocop rule just to make sure we do time comparisons the right way https://github.com/discourse/rubocop-discourse/blob/master/lib/rubocop/cop/discourse/time_eq_matcher.rb

7 Likes

How much can be done about this at the Rails level? It feels like the current situation is an adaptation to the fact that Ruby doesn’t give us as strong a set of time primitives as we need to correctly handle timezones.

This happened just today.

I got error reports around 7.50am and started looking into them. A chunk of the application which was supposed to calculate a running number per number of records for today kept returning a fixed number despite new records being created. After a few minutes, the running number resumed incrementing.

My timezone is +0800. I don’t need to cater for users in other time zones, so the easiest approach for me to handle my local time zone is to set config.time_zone. However, I do want Postgres to continue storing timestamps in UTC, in case things change in future. This means I need to ensure that my local time is converted into UTC before sending it to the DB. config.time_zone handles this seamlessly for the most part.

Turns out the database scope responsible for this chunk uses Date.today instead of Date.current, which means when called before 8am local time, that query will return yesterday’s count. If I had looked at this issue after 8am, I wouldn’t have seen any abnormal behavior.

2 Likes

I am not sure, ENV shuffling like all the tests in this file have is not something Rails can use https://github.com//blob/6efb9fe04229c9e96205025bfd8b6cfec1b9a30f/test/ruby/test_time_tz.rb#L19-L27

But the new Time#new initializer now accepts a timezone parameter so we could possibly monkeypatch Time.now to allow for the per-block scoping Rails allows provided we only work on Ruby 2.6 and up.

Curious what Andrew thinks here.

I’ve fallen for this too. It’s a hard one to debug.

If I’m not mistaken, Rails already monkey patches Time & Date for other reasons.

Is there any good reason, in a rails server, to use local time? (genuine question - not being sarcastic or trying to make a point).

For every case, I can think of (storing, comparing, calculating, future events) except two (accepting dates from the user and writing them to a page) I want the date/time in UTC.

Can we make it super easy to follow the best practice (dates are always in UTC) by making all the common date and time functions return and accept UTC?

Then make it super easy and obvious to convert to and from the user’s local time in controllers and views.

While I am here, it would be nice if rails_ujs told us the user’s timezone.

If I’m building an application which will only be used in a single local time zone, I shouldn’t need to deal with UTC conversion in every interaction with time.

1 Like

I shouldn’t need to deal with UTC conversion in every interaction with time.

Couldn’t rails hide that away from you too?

config.i18n.server-timezone = 'UTC' 
# config.i18n.client-timezone = :use_client_time_zone
config.i18n.client-timezone = 'Americas/Los Angeles'


%h2= l Time.now
1 Like

Front end is usually relatively easy to handle. Model level stuff like querying based on date / time, exporting data to CSV with the right date / time is trickier.

(I’m not disagreeing…just exploring)

Wouldn’t that be easier with the convention that everything from the client is in local time (except the JSON API, maybe) and everything on the server is in UTC? Of course, all bets are off if you have data from outside rails that is in local time but even exporting to CSV could use a simple date.to_local_time convention if necessary.

scope :created_today, -> { 
  where('created_at > ?', Date.current.beginning_of_day).
  where('created_at < ?', Date.current.end_of_day ) 
}

A scope like this would then always require Date.current to be passed in from the front end, which would be irritating if the calls are deeply nested in some other model / service logic.

2 Likes

For most Rails apps the advice from Thoughtbot is good starting point. Rails internally uses Time.current for methods like 24.hours.ago and ‘just use .current’ is about as simple as we can make it without patching Time.now and Date.today.

The only real need to deviate from that advice is where you have performance issues and by keeping things in native Time instances and disabling the timezone conversion in Active Record you can save quite a considerable amount of processing time if you’re load large numbers of records. You can either disable the conversion globally like this:

config.active_record.time_zone_aware_attributes = false

or you can skip timezone conversion for specific attributes like this:

class Post < ApplicationRecord
  self.skip_time_zone_conversion_for_attributes = [:created_at, :updated_at]
end

If all you’re doing is outputting timestamps then this is fine. However if you’re wanting to do any kind of date/time math on the server then you’ll need to convert to a Time instance in a local time or an ActiveSupport::TimeWithZone so that DST boundaries are handled correctly.

I would certainly recommend that a server’s default timezone is UTC but Ruby’s Time.now can be switched to a different timezone by setting ENV['TZ'] dynamically. However I’d suspect there may be threading issues with that and potentially performance impacts. Trying to re-implement Active Support’s Date/Time functionality entirely using native Time instances would be a big undertaking.

@samsaffron I’ve done some initial testing with the new timezone support in 2.6+ but didn’t see any significant performance gains to be had. Will repeat the tests in 2.7 to see if anything is better.

2 Likes

If I understand correctly that setting the time zone to UTC will make Date.today & Time.now return the correct time/date in UTC then I think this takes care of all my issues.

This has bothered me for so many years that I don’t know why I didn’t discover this simple solution earlier.

Thank you. I am glad I finally asked!

1 Like

This feels like a place for :star2: improved documentation :star2: !

3 Likes

I have many opinions on this topic! Our product deals with time tracking so we think about this a lot.

In my experience, it’s easy to accidentally leak times in the wrong time zone. Sam’s method works well if all your rendering is done in JS, but not if you are rendering times in HAML/erb, or in a CSV export, or in emails triggered by background jobs, etc.

What we do is store a time_zone column on every user, and store all actual times as a timestamp without time zone in postgres. Then use an around_action to set Time.zone on each request for the current user. On background jobs, we wrap jobs in TimeZone.in(@user) {}, and are looking at adding linters to enforce this (currently it relies on people remembering… they don’t).

The biggest challenge we face is when you have two users in different time zones and need to display to user A, a time in the correct time zone for user B. For example, user A lives on the west coast but runs a factory on the east coast, and wants to see what time user B (who works in that factory) started work.

If we rendered the time in user A’s time zone, it would be wrong. If we rendered the time in user B’s time zone it’d be right, but what if we also have to display data about user C (who lives in Central time) in the same response?

So we’ve been moving away from having a per-request time zone, to making all our models be time zone aware. Basically when loading any object that has time columns we now look up the time zone to output its times in, from relevant associations you can set on the object (usually the user, but can also be their home location etc).

Our current implementation looks like https://gist.github.com/ghiculescu/d8148f60a4e8c8b0fa7ceb77df49b139

6 Likes

Indeed, but this confusion is actually caused by ActiveSupport’s monkeypatching.

We have a similar problem as described by @ghiculescu.

We received information from dataloggers, and each datalogger send us information in their way. What do I mean? Some send us in UTC, some in local time and some in local time without DST. Yay!

So we have a similar approach: timestamps are in postgresql without timezone and each equipment and user has its own timezone declared. And we make use of the timezone block and some helpers we created to convert the timezone every time a user needs to be aware of.

It is important also rt note that in this case, dates should be timezone aware sometimes too. Because if the user is in UTC+3 for eg. and equipment sends information in UTC. If you only separate the date part, and it’s near midnight, each timestamp (although the same) will report a different date.