DateTime.now or Time.now? - ruby-on-rails

Which of the two should I be using in my Rails application:
DateTime.now or Time.now
Is there any harm in using both in the application?
Can there ever be any differences between the two in case of the above (now) example? (On my current system they are both showing the same time)

Use (Date)Time.current instead of (Date)Time.now
Rails extends the Time and DateTime objects, and includes the current property for retrieving the time the Rails environment is set to (default = UTC), as opposed to the server time (Could be anything).
This is critical- You should always be working in UTC time except when converting between timezones for user input or display- but many production systems are not UTC by default. (e.g. Heroku is set to PST (GMT -8))
See article here

In reference to Time.now (not DateTime.now):
The object created will be created using the resolution available on your system clock, and so may include fractional seconds.
a = Time.new #=> Wed Apr 09 08:56:03 CDT 2003
b = Time.new #=> Wed Apr 09 08:56:03 CDT 2003
a == b #=> false
"%.6f" % a.to_f #=> "1049896563.230740"
"%.6f" % b.to_f #=> "1049896563.231466"

If you want to get the time in the application's time zone, you'll need to call Time.zone.now, so that's what I generally use.
Time.now and DateTime.now will both return a time in the system's time, which often is set to UTC.

Related

Why does Date.yesterday() return the day before yesterday in rails?

I am a little bit confused by the Date.yesterday(), for example:
$ rails c
Loading development environment (Rails 6.0.3.2)
irb(main):001:0> Date.today
=> Fri, 10 Jul 2020
irb(main):002:0> Date.yesterday
=> Wed, 08 Jul 2020
irb(main):003:0> Time.now
=> 2020-07-10 03:54:46.02207138 +0530
irb(main):004:0>
But if I am not wrong, if today is Friday, which is true, the previous day should be Thursday as I learnt in my primary school..
What's going on in here?
tl;dr: Use Date.current and Time.current instead of Date.today and Time.now.
Your application has its own time zone, Time.zone. Ruby is not aware of time zones, but Rails is. Rails partially updates Time and Date to be aware of time zones, but not completely. Any methods Rails adds will be time zone aware. Date.yesterday and Date.tomorrow, for example. Built-in Ruby methods it leaves alone, like Date.today. This causes some confusion.
Date.today is giving today according to your local time zone, +0530. Date.yesterday is giving yesterday according to your application's time zone which I'm guessing is +0000 (UTC). 2020-07-10 03:54:46 +0530 is 2020-07-09 22:24:46 UTC so Date.yesterday is 2020-07-08.
Use Date.current instead of Date.today. Date.yesterday is a thin wrapper around Date.current.yesterday. Similarly, use Time.current instead of Time.now.
The ThoughtBot article It's About Time (Zones) discusses Rails time zones in detail and has simple DOs and DON'Ts to avoid time zone confusion.
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
Time.zone.parse("2015-07-04 17:05:37")
Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z").in_time_zone
Make sure to check the location of the server where the application is running. The value of Date.yesterday will likely depend on the location of the server with respect to the international date line. For example, while it is still Thursday, 07/09/2020 in New York City, it is Friday, 07/10/2020 in New Zealand.
In your Rails app, you may have a value set for Time.zone or in the configuration for config.time_zone.
If one of these values is configured, then Rails uses this as follows:
def yesterday
::Date.current.yesterday
end
def current
::Time.zone ? ::Time.zone.today : ::Date.today
end
These are ActiveSupport Date helpers
So it's indeed a result of wrong Timezone, in my case Time.zone.name returned "UTC". The correct zone for me is Asia/Kolkata, to check the presence of the timezone:
ActiveSupport::TimeZone::MAPPING.select { |x| x[/^kol/i] }
Which returned {"Kolkata"=>"Asia/Kolkata"} in my case.
For it to take effect temporarily, I set Time.zone = 'Kolkata' in the rails console.
For a permanent effect, added config.time_zone = 'Kolkata' to {project_root}/config/application.rb.
This fixed the problem, and Date.yesterday(), Date.today(), and Date.tomorrow() are working as expected.

Correcting time for daylight savings in rails DateTime.parse

Just adding to the million questions about time zone and DST issues out there.
I have a form with separate date and time fields that I combine to create a DateTime like so
start_time = DateTime.parse("#{parse_date(form_date)} #{form_start_time} #{Time.zone}")
If I fill out my form with 21 Aug 2012 and 15:00, then these are the values that I see when I reload my form. If I then look at my start_time attribute in my model it is correctly set to Tue, 21 Aug 2012 15:00:00 EST +10:00.
The problem I am having occurs if I use a date later this year once daylight savings kicks in (I am in Australia). If I use 21 Dec 2012 and 15:00 then check start_time I see Fri, 21 Dec 2012 16:00:00 EST +11:00.
My interpretation of the problem is that the date is being saved in my current time zone (+10:00) as this is what I have told DateTime.parse to do. However when the value is returned, Rails is looking at the date and saying 'hey, it's daylight savings time in December' and returning the time in the +11:00 time zone.
What I want to do is tell DateTime.parse to save the time in the +11:00 time zone if DST is in effect. Clearly passing Time.zone into my string doesn't achieve this. Is there a simple way of doing this? I can see ways of doing it using Time#dst? but I suspect that this is going to create some really ugly convoluted code. I thought there might be a built in way that I'm missing.
(Answer for Rails 4.2.4, didn't check for older or newer versions)
Instead of using fixed shift +01:00, +02:00, etc, I recommend to use the in_time_zone String method with time zone name as argument :
Summer time :
ruby :001 > "2016-07-02 00:00:00".in_time_zone('Paris')
=> Sat, 02 Jul 2016 00:00:00 CEST +02:00
Winter time :
ruby :002 > "2016-11-02 00:00:00".in_time_zone('Paris')
=> Wed, 02 Nov 2016 00:00:00 CET +01:00
String#in_time_zone is the equivalent of :
ruby :003 > Time.find_zone!("Paris").parse("2016-07-02 00:00:00")
=> Sat, 02 Jul 2016 00:00:00 CEST +02:00
ruby :004 > Time.find_zone!("Paris").parse("2016-11-02 00:00:00")
=> Wed, 02 Nov 2016 00:00:00 CET +01:00
You can get the time zone names by :
$ rake time:zones:all
Or in rails console :
ruby :001 > ActiveSupport::TimeZone.all.map(&:name)
Or build collection for select tag :
ActiveSupport::TimeZone.all.map do |timezone|
formatted_offset = Time.now.in_time_zone(timezone.name).formatted_offset
[ "(GMT#{formatted_offset}) #{timezone.name}", timezone.name ]
end
And store the time zone name instead of the shift.
Note : don't confuse String#in_time_zone method and the Time#in_time_zone method.
consider the time zone for my system is 'Paris'.
ruby :001 > Time.parse("2016-07-02 00:00:00")
=> 2016-07-02 00:00:00 +0200
ruby :002 > Time.parse("2016-07-02 00:00:00").in_time_zone("Nuku'alofa")
=> Sat, 02 Jul 2016 11:00:00 TOT +13:00
Here's my solution so far. I'm hoping someone has a better one.
start_time = DateTime.parse "#{date} #{(form_start_time || start_time)} #{Time.zone}"
start_time = start_time - 1.hour if start_time.dst? && !Time.now.dst?
start_time = start_time + 1.hour if Time.now.dst? && start_time.dst?
It seems to work but I haven't rigorously tested it. I suspect it could be prettied up and shortened but I think this is readable and understandable. Any improvements?
I ran into this exact issue. My app allows users to see upcoming events. In the US we fall of DST on November 2nd and all events on and after that date were showing times an hour early.
We require the opportunity to have the timezone selected and stored to its own field. Before I was using the following to store my datetime:
timezone_offset = Time.now.in_time_zone(params[:opportunity][:time_zone]).strftime("%z") #-0700
DateTime.parse("#{params[:opportunity][:start_datetime]} #{timezone_offset}")
To fix the issue I have changed to:
start_datetime = Time.zone.parse(params[:opportunity][:start_datetime])
To display the correct times we use:
#opportunity.start_datetime.in_time_zone(#opportunity.time_zone)
I wouuld try and use
Australian Eastern Standard Time (AEST) (UTC +10).
Australian Central Standard Time (ACST) (UTC +9 ½).
Australian Western Standard Time (AWST) (UTC +8).
which adjust for Daylight Savings.
With Rails, we can use ActiveSupport::TimeZone for this:
tz = ActiveSupport::TimeZone.new 'Pacific Time (US & Canada)'
tz.parse(date_str_without_zone).to_datetime
I use TZip to get TimeZone strings (e.g. "Pacific Time (US & Canada)") from zip codes.
In case you have a custom date/time format, different than the supported by String#in_time_zone, you could also use (since rails 5) strptime like:
Time.find_zone!('Auckland').strptime('2021-02-02 08.00.00', '%Y-%m-%d %H.%M.%S')

Rails: how to create Time object in specific time zone

My app is working in "Moscow" (+04:00) timezone. But sometimes I need to create time object by only local time (for example "01 may 2012 13:45") and name of ActiveSupport::TimeZone object (for example "Berlin": +02:00 in Summer Time and +01:00 otherwise).
For example if I get "01 may 2012 13:45" and "Berlin" as input I want to yield "2012-05-01 13:45:00 +0200" or "2012-05-01 11:45:00 +0000". I create following function:
def from_local_datetime(local_datetime, time_zone)
offset = Time.now.in_time_zone(time_zone).formatted_offset
datetime = case local_datetime
when String
DateTime.parse(local_datetime)
else
DateTime.new(local_datetime)
end.change(:offset => offset)
return datetime
end
And at the first look it works as I expected. But is it a best practice for this kind of task? May be in some situation It works with errors. I'm not definitely sure.
I would be greatful to any comments.
UPD: I think bug may occur about time when DST changing the time. For example 26 march 2011 was GMT+1 in Berlin time zone and Time.now.in_time_zone("Berlin").formatted_offset returns "GMT+1", but it would be GMT+2 in 27 march 2011. So if I call from_local_datetime("28 march 2011", "Berlin") before 27 march it returns 28 march 2011 00:00:00 +0100, but If I call it after changing the time my function returns 28 march 2011 00:00:00 +0200 :(
Your conversion method is the right approach.
With web sites, you should make sure times are stored as UTC in the database. If you can get the UTC value out of the database, instead of the local time (or maybe you can set your web server's time zone to UTC) it won't have to convert the time from UTC to local time, when you are going to then convert it to the user's timezone anyway.
And, of course, you will have to store the user's time zone preference.
TZInfo::Timezone.get('Europe/London')
Find the time zone
http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html

Rails timestamps don't use the right timezone

I'm a bit confused about timezones in rails. I want my rails app to use British Summer Time (like daylight savings in the US) for the timestamps set in updated_at and created_at in my models. I changed my environment.rb to say
config.time_zone = 'London'
The ubuntu server my app is on seems to use BST for it's time: in the command line, for example, if i type 'date' i get the current time (not offset by an hour). In the rails console, i see the following:
>> time = Time.now
=> Wed Oct 27 16:29:17 +0100 2010
>> time.zone
=> "BST"
All fine. However, if i make a new AR model object and save it, the timestamps are from an hour ago. So, it looks like this is using UTC. Now, i can see the logic in this: since the timestamps might be used in the model logic, you want them to be based on an unvarying yardstick time, ie UTC. But, this is a weird bit of behaviour that i don't understand:
#change a record and save it
>> someobj.save
=> true
#object's updated_at is one hour ago
>> someobj.updated_at
=> Wed, 27 Oct 2010 15:34:22 UTC +00:00
>> Time.now
=> Wed Oct 27 16:34:31 +0100 2010
#however, Time.now - object's updated at is just a few seconds.
>> Time.now - someobj.updated_at
=> 15.305549
So, before doing the subtraction, updated_at is converted into the current time zone.
The reason i want to show the date in the current time zone is just for status reports etc in the views: if someone updates something i want them to see that it was updated '1 minute ago' not 'one hour ago'.
Can anyone unconfuse me? cheers, max
EDIT: My immediate problem, of showing the right time in the status, is solved by using the 'time_ago_in_words' helper, which adjusts for time zone. I'd still like someone to explain what's going on with the timestamps though :)
Timestamps are stored in UTC by default, and this is probably the best way to do it. If you move from one server environment to another, you don't want all of your times shifting around just because you switched time zones.
If you want to know what the timestamp is in your local time zone, you just have to ask for it that way:
someobj.updated_at.localtime
Note the offset listed at the end of the times -- the first offset is 0, the second is 1. When the time calculation occurs, the offset is included automatically, so that the subtraction gives you the correct result. someobj.updated_at and Time.now each displays its value in a different time zone, so they are really only 9 seconds apart, not 1 hour and 9 seconds.

Why does this rails query behave differently depending on timezone?

I have a rails time-based query which has some odd timezone sensitive behaviour, even though as far as I know I'm using UTC. In a nutshell, these queries give different answers:
>> Model.find(:all,:conditions=>['created_at<=?',(Time.now-1.hours).gmtime]).length
=> 279
>> Model.find(:all,:conditions=>['created_at<=?',(Time.now-1.hours)]).length
=> 280
Where the DB actually does contain one model created in the last hour, and the total number of models is 280. So only the first query is correct.
However, in environment.rb I have:
config.time_zone = 'UTC'
The system time zone (as reported by 'date') is BST (which is GMT+1) - so somehow this winds up getting treated as UTC and breaking queries.
This is causing me all sorts of problems as I need to parameterise the query passing in different times to an action (which are then converted using Time.parse()), and even though I send in UTC times, this 'off by one hour' DST issue crops a lot. Even using '.gmtime()' doesn't always seem to fix it.
Obviously the difference is caused somehow by an implicit conversion somewhere resulting in BST being incorrectly treated as UTC, but why? Doesn't rails store the timestamps in UTC? Isn't the Time class timezone aware? I am using Rails 2.2.2
So what is going on here - and what is the safe way to program around it?
edit, some additional info to show what the DB and Time class are doing:
>> Model.find(:last).created_at
=> Tue, 11 Aug 2009 20:31:07 UTC +00:00
>> Time.now
=> Tue Aug 11 22:00:18 +0100 2009
>> Time.now.gmtime
=> Tue Aug 11 21:00:22 UTC 2009
The Time class isn't directly aware of your configured timezone. Rails 2.1 added a bunch of timezone support, but Time will still act upon your local timezone. This is why Time.now returns a BST time.
What you likely want is to interact with Time.zone. You can call methods on this like you would the Time class itself but it will return it in the specified time zone.
Time.zone.now # => Tue, 11 Aug 2009 21:31:45 UTC +00:00
Time.zone.parse("2:30 PM Aug 23, 2009") # => Sun, 23 Aug 2009 14:30:00 UTC +00:00
Another thing you have to be careful with is if you ever do queries on the database where you are comparing times, but sure to use the UTC time (even if you have a different time zone specified) because Rails always stores UTC in the database.
Item.all(:conditions => ["published_at <= ?", Time.now.utc])
Also, instead of Time.now-1.hour do 1.hour.ago. It is easier to read and Rails will automatically use the configured timezone.
The TimeZone you need to set is UK, this will automatically handle BST
Time.zone = 'UK'
Time.zone.now
=> Sun, 17 Oct 2010 02:09:54 BST +01:00
start_date_format = DateTime.strptime(#start_date, date_format)
start_date_format_with_hour =
DateTime.strptime((start_date_format.to_i + timezone_offset*60*60).to_s,'%s').strftime(date_format)
end_date_format = DateTime.strptime(#end_date, date_format)
end_date_format_with_hour = DateTime.strptime((end_date_format.to_i + timezone_offset*60*60).to_s,'%s').strftime(date_format)
#filters_date = "invoices.created_at >= ? AND invoices.created_at < ?", start_date_format_with_hour, end_date_format_with_hour

Resources