Fix timezone issue with database in Rails on Heroku - ruby-on-rails

My Rails app is hosted on Heroku with a Postgress database. For a certain model I'm asking the user to chose a day and a time, which I combine to a date before saving the model:
def create_timestamp
self.date = day.to_datetime + time.seconds_since_midnight.seconds
end
When I chose for instance today # 20:50:00 and store it in the database, my record looks like this:
<Report id: 1, account_id: 1, date: "2016-11-05 20:50:00", description: "test", created_at: "2016-11-05 19:50:57", updated_at: "2016-11-05 19:50:57", deleted_at: nil, user_id: 1, report_category_id: 2, time: "2000-01-01 20:50:00", day: "2016-11-05">
As you might notice, the created_at date is different, because it's in a different timezone. While the created_at is stored in UTC +0000, my custom date, which uses local timezone is CET +0100.
So when I type in console: Report.find(1).date, it returns 2016-11-05 21:50:00 +0100.
How can I store the correct date in the initial set, or make the database return the correct timezone when querying?
Thanks

What you're doing currently is basically this:
>> date = Date.new(2016, 11, 6)
=> Sun, 06 Nov 2016
>> time = Time.new(2000, 1, 1, 20, 50, 0)
=> 2000-01-01 20:50:00 +0100
>> date.to_datetime + time.seconds_since_midnight.seconds
=> Sun, 06 Nov 2016 20:50:00 +0000
to_datetime converts the time-zone-less Date into a DateTime representing midnight UTC on that date, and then you add 20 hours and 50 minutes.
Instead of midnight UTC, you want midnight in your local time zone as your starting point. So you could do this, for example:
>> date.in_time_zone + time.seconds_since_midnight.seconds
=> Sun, 06 Nov 2016 20:50:00 CET +01:00
Rails should then be smart enough to convert it to UTC when storing in the database and back to CET when retrieving from the database.
To be clear about the difference, compare:
>> date
=> Sun, 06 Nov 2016
>> date.to_datetime
=> Sun, 06 Nov 2016 00:00:00 +0000
>> date.in_time_zone
=> Sun, 06 Nov 2016 00:00:00 CET +01:00

You should use
Time.zone.now
# or
Time.current
Which will return the time in time zone

Related

How can I change the offset of a timestamp and then store the change in postgresql?

I have a form on my rails app that allows you to create a campaign object where the user can set a start_date, end_date, and timezone for which the dates will officially start and end.
What I would like to do is to apply the offset of the selected timezone to both the start_date and end_date. I've tried a few combinations but I can't seem to make it work. For starters, when I select my start and end dates for 11:00 PM and select Central Time Zone CDT with offset of -0500, the end result in postgresql is the following timestamp.
# No offset is applied
Wed, 08 Jun 2022 23:00:00.000000000 UTC +00:00
In order to try to apply the offset, I've tried a few combinations in my rails code. Here are some examples in a before_validation callback. It's pseudocode but this is the gist of it.
Example #1
date_tmp = send("start_at") # Wed, 08 Jun 2022 23:00:00.000000000 UTC +00:00
date_string = date_tmp.to_datetime.change(offset: '-0500').to_s
assign_attributes("start_at" => date_string) # "2022-06-08T22:00:00-05:00"
The result is no change for example 1.
Example #2
date_tmp = send("start_at") # Wed, 08 Jun 2022 23:00:00.000000000 UTC +00:00
date_string = date_tmp.to_datetime.change(offset: '-0500').to_s
dt = Chronic.parse(date_string.to_time.to_s)
assign_attributes("start_at" => dt) # "2022-06-08 22:00:00 -0500"
None of these variations work. I've also tried to save to postgresql timestamps that look like this:
2022-06-08T22:00:00-05:00
2022-06-08 22:00:00 -0500
Wed, 08 Jun 2022 22:00:00 -0500
I don't understand why rails and postgresql can't save the timestamp with the adjusted utc_offset that I want. What can I be doing wrong?
Try new_offset method on your DateTime instance.
It duplicates datetime object and resets its offset.
Some examples:
date = DateTime.now
date # => Thu, 16 Jun 2022 20:14:58 +0000
date.new_offset('+03:00') # => Thu, 16 Jun 2022 23:14:58 +0300
date.new_offset('-03:00') # => Thu, 16 Jun 2022 17:14:58 -0300

strftime(%Z) returns wrong result

I have this date
date = Mon, 15 Aug 2016 13:00:00 UTC +00:00
which is ActiveSupport::TimeWithZone class
Then, I need to get the time in time zone "Fiji"
start_in_time_zone = date.in_time_zone("Fiji")
This returns Tue, 16 Aug 2016 01:00:00 +12 +12:00
Then, I need to present the date with the name of the time zone, so
time_zone_abbr = start_in_time_zone.strftime("%Z")
It should return "FJT"
but returns "+12"
Any idea why?
I am using ruby 2.3.7 and rails 4.2.7
UPDATE
If I do
start_in_time_zone = date.in_time_zone("Madrid")
it returns
"CEST"
UPDATE 2
I have tried to see where the problem is by setting different time.
date=Time.utc(2018, 07, 25, 20, 30, 45)
date.class #=> Time
date.in_time_zone("Madrid") #=> Wed, 25 Jul 2018 22:30:45 CEST +02:00
date.in_time_zone("Fiji") #=> Thu, 26 Jul 2018 08:30:45 +12 +12:00
date.in_time_zone("EST") #=> Wed, 25 Jul 2018 15:30:45 EST -05:00
Sadly, it seems there is no 'FJT' abbreviation assigned to 'Fiji' in timezone data used by Rails. Also, support for those abbreviations seems patchy regarding Pacific timezones.
irb(main):002:0> DateTime.now.in_time_zone('Samoa').strftime('%Z')
=> "+13"
irb(main):003:0> DateTime.now.in_time_zone('Midway Island').strftime('%Z')
=> "SST"
irb(main):004:0> DateTime.now.in_time_zone('Samoa').strftime('%Z')
=> "+13"
irb(main):005:0> DateTime.now.in_time_zone('Tokelau Is.').strftime('%Z')
=> "+13"
irb(main):006:0> DateTime.now.in_time_zone('Wellington').strftime('%Z')
=> "NZST"
UTC offset is displayed as fallback. If it's any help, remember that full name and additional information can be retrieved with .time_zone.tzinfo on ActiveSupport::TimeWithZone objects. 'FJ' code is recognized by TZInfo::Country.
irb(main):056:0> TZInfo::Country.get('FJ')
=> #<TZInfo::Country: FJ>
irb(main):057:0> TZInfo::Country.get('FJ').zone_info
=> [#<TZInfo::CountryTimezone: Pacific/Fiji>]

1.year.from_now is also one day ahead

Today's date is 2016-09-19. I add one year to it, and I expect the result to be 2017-09-19. Instead, I get 2017-09-20. One year plus one day ahead. Is this behavior as intended?
$ rails c
2.3.1 :001 > Time.now.to_date.iso8601
=> "2016-09-19"
2.3.1 :002 > 1.year.from_now.to_date.iso8601
=> "2017-09-20"
If you want get ecactly date with time zones, you can use Time.current
1.year.from_now
#=> Wed, 20 Sep 2017 05:38:50 UTC +00:00
Time.current
#=> Tue, 20 Sep 2016 05:39:08 UTC +00:00
since or its alias from_now calculate the offset based on Time.current which is equivalent to Time.zone.now – both return a ActiveSupport::TimeWithZone instance:
Time.current #=> Mon, 19 Sep 2016 19:56:34 SST -11:00
Time.zone.now #=> Mon, 19 Sep 2016 19:56:35 SST -11:00
Time.now on the other hand returns an ordinary Time instance in your system's timezone, which can differ from the Rails timezone:
Time.now #=> 2016-09-20 08:56:36 +0200
To avoid confusion, you should always use Time.current / Time.zone.now when working within Rails. You can however pass another "base time" to since:
1.year.since(Time.now) #=> 2017-09-20 08:56:37 +0200
Or, because you are working with dates:
1.year.since(Date.today) #=> Wed, 20 Sep 2017
There is also Date.current which is equivalent to Time.zone.today:
1.year.since(Date.current) #=> Wed, 19 Sep 2017
Turns out it's a timezone thing. I'm UTC -7 hours.
Time.now returns the time in my timezone.
1.year.from_now returns the time in UTC+0, 7 hours ahead of where I am.
It's 10pm here, so it's the next day at UTC+0.

Rails: get #beginning_of_day in time zone

I have a default time zone setup for the rails application.
And an instance of the Date object.
How can I get make Date#beginning_of_day to return the beginning of the day in the specified time zone, but not my local timezone.
Is there any other method to get beginning of the day time in the specified timezone for the given date?
date = Date.new(2014,10,29)
zone = ActiveSupport::TimeZone.new('CET')
date.foo(zone) # should return "Wed, 29 Oct 2014 00:00:00 CET +01:00"
zone = ActiveSupport::TimeZone.new('UTC')
date.foo(zone) # should return "Wed, 29 Oct 2014 00:00:00 UTC +00:00"
DateTime.now.in_time_zone(Time.zone).beginning_of_day
time_zone = Time.zone # any time zone really
time_zone.local(date.year, date.month, date.day)
Problem is, Date.beginning_of_day does not honor Time.zone in ActiveSupport 2.3
Compare https://github.com/rails/rails/blob/v2.3.11/activesupport/lib/active_support/core_ext/date/calculations.rb#L64 (AS 2.3)
to https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/date/calculations.rb#L74
and
https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/date/zones.rb#L7
(AS 3)
Date#beginning_of_day will always return 00:00.
But as I understand you want to know time in other time zone while in current time zone is beginning of the day.
So. Let's find out beginning of the day in your current place. Imagine it is Paris, France:
bd = DateTime.now.in_time_zone('Paris').beginning_of_day
# or just
bd = DateTime.now.in_time_zone(1).beginning_of_day
#=> Thu, 24 Mar 2011 00:00:00 WET +01:00
Now lets found out what time is in Moscow:
moscow_time = bd.in_time_zone("Moscow") # or in_time_zone(3)
#=> Thu, 24 Mar 2011 02:00:00 AST +03:00
london_time = bd.in_time_zone("London")
#=> Wed, 23 Mar 2011 23:00:00 GMT +00:00
kyiv_time = bd.in_time_zone("Kyiv")
#=> Thu, 24 Mar 2011 01:00:00 EET +02:00
For different form now day:
# You even shouldn't call now, because it by default will be 00:00
date = DateTime(2011, 1, 3).in_time_zone("-10")
# or
date = DateTime.new(2011,1,3,0,0,0,"-10")
# and same way as above
moscow_time = date.in_time_zone("Moscow") # or in_time_zone(3)
and converting Date to DateTime
date = Date.new(2011,1,3).to_datetime.change(:offset => "EST")
Going off Peder's answer, here's what I did for the PST time zone:
DateTime.now.in_time_zone("Pacific Time (US & Canada)").beginning_of_day
I know this post is old, but what about:
Time.zone.parse("12am")
may2 = Date.new(2012,5,2)
midnight = Time.zone.local(may2.year,may2.month,may2.day).beginning_of_day
Wed, 02 May 2012 00:00:00 UTC +00:00
midnight = Time.zone.local(may2.year,may2.month,may2.day).in_time_zone("Moscow").beginning_of_day
=> Wed, 02 May 2012 00:00:00 MSK +04:00
midnight = Time.zone.local(may2.year,may2.month,may2.day).in_time_zone("Alaska").beginning_of_day
=> Tue, 01 May 2012 00:00:00 AKDT -08:00
NOTICE, it's the WRONG DAY.
What is needed is a way to construct a TimeWithZone in the correct timezone.
Time.zone="Alaska"
midnight = Time.zone.local(may2.year,may2.month,may2.day)
I really dislike this, because as far as I can see, I've just changed the system notion of what zone I'm in. The idea is to change my database searches to match the zone of the client...
So, I have to save zone and restore it:
foo = Time.zone; Time.zone="Alaska"; midnight = Time.zone.local(may2.year,may2.month,may2.day); Time.zone = foo
It seems like I ought be able to call TimeWithZone.new(), but I didn't figure out how.
ActiveSupport::TimeZone['Europe/London'].parse('30.07.2013') # 2013-07-29 23:00:00 UTC
ActiveSupport::TimeZone['Asia/Magadan'].parse('30.07.2013') # 2013-07-29 12:00:00 UTC
As Leonid Shevtsov mentioned, Date.beginning_of_day does not honor Time.zone in ActiveSupport 2.3
An alternative I used, if your stuck using Rails 4.0 or ActiveSupport 2.3, and you need to use a custom date:
date = Date.new(2014,10,29)
date.to_time.change(hour: 0, min: 0, sec: 0).in_time_zone #.beginning_of_day
date.to_time.change(hour: 23, min: 59, sec: 59).in_time_zone #.end_of_day
Results:
2.0.0-p247 :001 > date = Date.new(2014,10,29)
=> Wed, 29 Oct 2014
2.0.0-p247 :002 > date.to_time.change(hour: 0, min: 0, sec: 0)
=> 2014-10-29 00:00:00 -0500
2.0.0-p247 :003 > date.to_time.change(hour: 0, min: 0, sec: 0).in_time_zone
=> Wed, 29 Oct 2014 05:00:00 UTC +00:00
2.0.0-p247 :004 > date.to_time.change(hour: 23, min: 59, sec: 59)
=> 2014-10-29 23:59:59 -0500
2.0.0-p247 :005 > date.to_time.change(hour: 23, min: 59, sec: 59).in_time_zone
=> Thu, 30 Oct 2014 04:59:59 UTC +00:00
My original failed model scope using .beginning_of_day to .end_of_day failed to work:
scope :on_day, ->(date) { where( created_at: date.beginning_of_day..date.end_of_day ) }
And, this is what fixed it, since I could not upgrade to Rails 4.0
scope :on_day, ->(date) { where( created_at: date.to_time.change(hour: 0, min: 0, sec: 0).in_time_zone..date.to_time.change(hour: 23, min: 59, sec: 59).in_time_zone ) }
Generate new date in a specific time zone
If you would like to get beginning of the day for a specific date (and you know the exact date), then you have to use:
Time.use_zone('London'){ Time.zone.local(2022, 1, 1) }
# => Sat, 01 Jan 2022 00:00:00.000000000 GMT +00:00
It respects seasonal time change:
Time.use_zone('London'){ Time.zone.local(2022, 6, 1) }
# => Wed, 01 Jun 2022 00:00:00.000000000 BST +01:00
Although it is already referred by accepted answer I'd like to add that personally I prefer block notation.
Convert existing and find out what the day it is in another time zone
If you would like to know what day that would be for a specific timestamp/epoch/date that you already have you need to convert your existing date to the proper time zone, which is already referred by Peder's and Tim's answers:
Time.current.in_time_zone('London').beginning_of_day
Be aware, when converting existing date you may end up in another day (1st became 2nd):
Time.use_zone('London'){ Time.zone.local(2022, 6, 1, 23, 0, 0) }.in_time_zone('Tokyo').beginning_of_day
# => Thu, 02 Jun 2022 00:00:00.000000000 JST +09:00
If you are going to convert time, then Time.now or Time.current is indifferent, but I prefer using Time.current as a rule:
Time.now
# => 2022-09-30 15:29:16 +0000
Time.current
=> Fri, 30 Sep 2022 16:29:36.006315710 BST +01:00

converting Date object to TimeWithZone

I need to convert a Date object into a TimeWithZone object representing the beginning of that day in a given time zone.
The following approach works, but seems too convoluted as it requires me to convert the date to a string:
?> date = Date.parse("2010-02-17")
=> Wed, 17 Feb 2010
>> ActiveSupport::TimeZone['Eastern Time (US & Canada)'].parse(date.to_s)
=> Wed, 17 Feb 2010 00:00:00 EST -05:00
>> ActiveSupport::TimeZone['UTC'].parse(date.to_s)
=> Wed, 17 Feb 2010 00:00:00 UTC 00:00
Is there a better way I'm missing?
Edit:
People are suggesting variations of:
?> date.to_datetime.in_time_zone('Eastern Time (US & Canada)').beginning_of_day
=> Tue, 16 Feb 2010 00:00:00 EST -05:00
As you can see, this isn't an equivalent conversion since it leaves me at the start of Feb. 16th EST, instead of the start of Feb. 17th EST.
I'm late to the party, but this is still a great question. ActiveSupport's in_time_zone was introduced since the O.P., but it does exactly what you want without parsing a string (slow) or setting Time.zone (risky):
>> date = Date.parse("2010-02-17")
=> Wed, 17 Feb 2010
>> date.in_time_zone('Eastern Time (US & Canada)')
=> Wed, 17 Feb 2010 00:00:00 EST -05:00
Of course if you want the beginning of day expressed at utc, you can do this:
>> date.in_time_zone('Eastern Time (US & Canada)').utc
=> 2010-02-17 05:00:00 UTC
If you have Time.zone set in Rails then you can call Date#at_beginning_of_day (see http://api.rubyonrails.org/classes/Date.html#method-i-at_beginning_of_day). Contrast this with Date#to_datetime:
Time.zone
=> #<ActiveSupport::TimeZone:0x10cf10858 #tzinfo=#<TZInfo::TimezoneProxy: Etc/UTC>, #utc_offset=nil, #current_period=nil, #name="UTC">
date = Date.today
=> Thu, 31 May 2012
date.to_datetime
=> Thu, 31 May 2012 00:00:00 +0000
date.at_beginning_of_day
=> Thu, 31 May 2012 00:00:00 UTC +00:00
Time.zone = 'America/Chicago'
=> "America/Chicago"
date.to_datetime
=> Thu, 31 May 2012 00:00:00 +0000
date.at_beginning_of_day
=> Thu, 31 May 2012 00:00:00 CDT -05:00
I strongly recommend against any solution that converts the date to a time using to_datetime or to_time because those methods are unaware of the zone, and tacking in_time_zone onto the result, as some answers suggest, won't retroactively fix the mistake. Also, don't try to build your own daylight saving time math using UTC offsets. You're bound to get it wrong, and you're doing work unnecessarily.
Use the TimeZone itself which has this logic built in.
Given a zone and a date, you can get a TimeWithZone for the beginning of the day like this:
time = zone.local(date.year, date.month, date.day)
If you want a specific time of day other than the beginning, you can pass the hour, minute, and second as the 4th, 5th, and 6th arguments to #local.
If zone is actually your system's local time zone (Time.zone), then ActiveSupport will let you shorten the above to this:
time = date.to_time_in_current_zone
All of the above handle daylight saving time correctly. Let's verify that by looking at the UTC offsets for two times, one that's outside DST and one that's within DST:
irb(main):009:0> zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
=> (GMT-05:00) Eastern Time (US & Canada)
irb(main):010:0> t1 = zone.local(2013, 1, 1)
=> Tue, 01 Jan 2013 00:00:00 EST -05:00
irb(main):011:0> t2 = zone.local(2013, 5, 1)
=> Wed, 01 May 2013 00:00:00 EDT -04:00
irb(main):012:0> t1.utc_offset
=> -18000
irb(main):013:0> t2.utc_offset
=> -14400
Would something like this work for you?
'2010-04-01'.to_time.in_time_zone('Eastern Time (US & Canada)').beginning_of_day
Subtract utc_offset:
d = Date.today
Time.zone.class.all.map(&:name).map { |tz| dt = d.to_datetime.in_time_zone(tz); dt -= dt.utc_offset }
Using ActiveSupport::TimeZone[tz] doesn't take daylight savings time into account.
Time.zone.class.all.map(&:name).map { |tz| o = d.to_datetime.in_time_zone(tz).utc_offset - ActiveSupport::TimeZone[tz].utc_offset }

Resources