Specify UTC Offset instead of timezone to ActiveSupport::TimeWithZone - ruby-on-rails

With ActiveSupport::TimeWithZone I'm able to do the following and get back a TimeWithZone object in the correct timezone.
Time.current.in_time_zone('Alaska')
:> Thu, 19 Oct 2017 08:45:08 AKDT -08:00
Is there an equivalent method to in_time_zone where I can pass it the UTC Offset in seconds and get back a TimeWithZone object with the specified offset?
offset = -25200 # -25200 seconds == -08:00
Time.current.in_utc_offset(offset)
:> Thu, 19 Oct 2017 08:45:08 -08:00
Thanks in advance!
Using Rails 5.1.2 & Ruby 2.4.1

Yes it is in vanilla Ruby: #getlocal(sec). It is not going to give you an ActiveSupport::TimeWithZone but it will give you the Time which you can format or do whatever you want with it, including using ActiveSupport extensions.
2.4.1 :016 > Time.now.getlocal(3600)
=> 2017-10-19 22:56:45 +0100
2.4.1 :017 > Time.now.getlocal(-3600)
=> 2017-10-19 20:56:48 -0100
PS: -25200 is 7 hours in seconds :) -8:00 would be 28800

Related

Parse Time in Ruby

I'm trying to parse this date Wed, 17 Feb 2021 13:00:00 +0100 into this 2021-02-17 13:00:00.000000000 +0100.
And I've tried using this Time.strptime(current_time.to_s, '%Q'), (where current_time it's the date above) but I get 1970-01-01 01:00:02.021 +0100
But I don't understand why I get another date, could you help me? Thanks!
I'm trying to parse this date Wed, 17 Feb 2021 13:00:00 +0100 [...]
You seem to already have an instance of Time: (or ActiveSupport::TimeWithZone which is Rails' drop-in replacement with better timezone support)
current_time = Time.current
#=> Thu, 19 May 2022 10:09:58.702560000 CEST +02:00
In this case, there's nothing to parse. You just have to format it via strftime the way you like:
current_time.strftime('%F %T.%N %z')
#=> "2022-05-19 10:09:58.702560000 +0200"
Parsing is only needed when you have a string representation that you want to turn into a Time object, e.g.: (using Rails' Time.zone.parse variant)
time_string = 'Thu, 19 May 2022 10:09:58.702560000 CEST +02:00'
time_obj = Time.zone.parse(time_string)
#=> Thu, 19 May 2022 10:09:58.702560000 CEST +02:00
time_obj.class
#=> ActiveSupport::TimeWithZone

Should ActiveSupport::TimeWithZone (and Time.zone)'s methods respect DST changes?

Look at this code:
[4, 5, 6, 7].each do |x|
start_at = Time.zone.parse("2014.10.2#{x} 08:00")
puts "#{start_at.inspect} / #{start_at.seconds_since_midnight/60}"
end
Output:
Fri, 24 Oct 2014 08:00:00 CEST +02:00 / 480.0
Sat, 25 Oct 2014 08:00:00 CEST +02:00 / 480.0
Sun, 26 Oct 2014 08:00:00 CET +01:00 / 480.0
Mon, 27 Oct 2014 08:00:00 CET +01:00 / 480.0
We are in Germany, and on October 26 the DST is reset so 08:00 is not actually 8*60 minutes since midnight but 9*60.
IMHO, this should be respected by the seconds_since_midnight method. Am I missing something or is this actually a Ruby bug?
Using Ruby 1.9.3, Rails 3.0.20 on Mac OS X 10.9.5 (MacPorts).
(Yes, Rails 3.0.20 is old. Legacy project. If it's fixed in Rails 4, all the better.)
According to the Rails docs, it's implemented as such:
# File activesupport/lib/active_support/core_ext/date_time/calculations.rb, line 14
def seconds_since_midnight
sec + (min * 60) + (hour * 3600)
end
Thus, this function is not time zone aware. It simply looks at the local time value. It does not consider that the offset of midnight might not be the same as the offset of the value you provided. Nor does it consider that midnight might not exist in the local time zone (such as with Brazil's spring-forward transition).
Is this a bug? Perhaps. But one might also say the method is just poorly named. Either way, it's an issue with Rails, not with Ruby.

Effect of leaving space after '-' in Ruby expression

Today I was trying something out in my Rails console and this happened,
2.0.0p247 :009 > Date.today -29.days
=> Fri, 07 Feb 2014
2.0.0p247 :010 > Date.today - 29.days
=> Thu, 09 Jan 2014
I am pretty baffled. I can see that I am missing something basic. But it just inst striking my mind! Could anyone explain why this is happening?
What actually happens is this:
Date.today(-29.days) # => Fri, 07 Feb 2014
today has an optional parameter called start, which defaults to Date::ITALY.
An optional argument the day of calendar reform (start) as a Julian
day number, which should be 2298874 to 2426355 or -/+oo. The default
value is Date::ITALY (2299161=1582-10-15).
Passing -29.days to today apparently has no effect.
Whereas:
Date.today + -29.days # => Thu, 09 Jan 2014
Which is the same as:
Date.today - 29.days # => Thu, 09 Jan 2014
The Fixnum#days method in Ruby is defined in the ActiveSupport library in Rails. More specifically, it's defined in the 'active_support/core_ext/numeric/time` module
Just loading ActiveSupport inside a console
> require 'active_support'
true
> require 'active_support/core_ext'
true
> 29.days
=> 2505600
> -29.days
=> -2505600
The code for the method days looks like this:
def days
ActiveSupport::Duration.new(self * 24.hours, [[:days, self]])
end
The self object in this case is 29 or -29. That gets multiplied by 2505600 which is a legitimate calculation and will return the amount of seconds that the number 29 represents.
With that in mind, in the first calculation, you're just passing two objects to the console input, the first one being a Date object and the second one being a number. Similar to sending the following:
> puts "test"
That is, the -29.days is passed as an argument to the object returned byDate.today. And Date.today accepts a parameter which specifies the start day of the calendar. Refer to this answer to know the accepted days. If any non-recognized parameter is passed, the default start date is used (which is Date::GREGORIAN)
> Date.today
=> Thu, 09 Feb 2014
> Date.today Date::JULIAN
=> Fri, 25 Jan 2014
So Ruby checks if you passed in a valid start date constant or not and the decide on the start date. For this reason, you still get today's date as the answer when running the first command.
The second command is simply the subtraction of one Date object with another object that is understood by Ruby as the number of seconds you want to subtract from today's date. 29.days is internally converted to the number of seconds in both cases.
Hope that helps.

Time.use_zone is not working as expected

So right now it is 2:54 PM PST in San Francisco. For some reason, this code block is not returning 12:54 PM HST in Hawaii. Am I missing something here? I would expect this code to return me the current time in Hawaii
Time.use_zone('Hawaii') do
Time.now
end
# => 2012-01-03 14:54:54 -0800
This should work ok:
Time.use_zone('Hawaii') do
p Time.zone.now
end
Try using Time.now.in_time_zone inside your block instead.
> Time.use_zone('Hawaii') do
> Time.now.in_time_zone
> end
=> Tue, 03 Jan 2012 13:07:06 HST -10:00
Use Time.current if you want now with timezone support. Time.now is dangerous when working in a timezone aware application, as a rule of thumb I never use Time.now, only Time.current. Rails time helpers like 2.hours.ago and 4.days.from_now are based off of Time.current as well.
# Time.current will use Time.zone when needed (when Time.zone is present)
def current
::Time.zone ? ::Time.zone.now : ::Time.now
end
Also, this is a great article with a great cheat sheet at the bottom: https://www.varvet.com/blog/working-with-time-zones-in-ruby-on-rails/
DOs
code
result
2.hours.ago
Thu, 27 Aug 2015 14:39:36 AFT +04:30
1.day.from_now
Fri, 28 Aug 2015 16:39:36 AFT +04:30
Time.zone.parse("2015-08-27T12:09:36Z")
Thu, 27 Aug 2015 16:39:36 AFT +04:30
Time.current
Thu, 27 Aug 2015 16:39:36 AFT +04:30
When supliyng an API
Time.current.utc.iso8601
2015-08-27T12:09:36Z
If you can’t use Time.zone.parse
Time.strptime("2015-08-27T12:09:36Z", "%Y-%m-%dT%H:%M:%S%z").in_time_zone
Thu, 27 Aug 2015 16:39:36 AFT +04:30
If you really can’t have a Time or DateTime for some reason
Date.current
Thu, 27 Aug 2015
If you have a date and want to make the best out of it
Date.current.in_time_zone
Thu, 27 Aug 2015 00:00:00 AFT +04:30
DON’Ts
code
result
Returns system time and ignores your configured time zone.
Time.now
2015-08-27 14:09:36 +0200
Will assume time string given is in the system’s time zone.
Time.parse("2015-08-27T12:09:36Z")
2015-08-27 12:09:36 UTC
Same problem as with Time.parse.
Time.strptime("2015-08-27T12:09:36Z", "%Y-%m-%dT%H:%M:%S%z")
2015-08-27 12:09:36 UTC
This could be yesterday or tomorrow depending on the machine’s time zone, see issue 1 for more info.
Date.today
Thu, 27 Aug 2015
Time.now - using server time
Time.zone.now - using rails application time (in config: config.time_zone)
Time.use_zone - using 'your' timezone for given block
This example is wrong, because Time.now get time in your server timezone and with method in_time_zone translate time into an equivalent time in Hawaii timezone. But it's no current Time in Hawaii! It's your server time with utc offset for Hawaii.
Time.use_zone('Hawaii') do
Time.now.in_time_zone
end
=> Wed, 14 Aug 2013 10:33:18 HST -10:00
Time.now.in_time_zone
=> Thu, 15 Aug 2013 00:32:30 MSK +04:00
For getting time in Hawaii timezone you must use
Time.use_zone('Hawaii') do
Time.zone.now
end
Don't use Time.now this is using your local time zone instead use Time.current
Time.use_zone('Hawaii') do
p Time.current
end

How do I change the zone offset for a time in Ruby on Rails?

I have a variable foo that contains a time, lets say 4pm today, but the zone offset is wrong, i.e. it is in the wrong time zone. How do I change the time zone?
When I print it I get
Fri Jun 26 07:00:00 UTC 2009
So there is no offset, and I would like to set the offset to -4 or Eastern Standard Time.
I would expect to be able to just set the offset as a property of the Time object, but that doesn't seem to be available?
You don't explicitly say how you get the actual variable but since you mention the Time class so I'll assume you got the time using that and I'll refer to that in my answer
The timezone is actually part of the Time class (in your case the timezone is shown as UTC). Time.now will return the offset from UTC as part of the Time.now response.
>> local = Time.now
=> 2012-08-13 08:36:50 +0000
>> local.hour
=> 8
>> local.min
=> 36
>>
... in this case I happen to be in the same timezone as GMT
Converting between timezones
The easiest way that I've found is to change the offset using '+/-HH:MM' format to the getlocal method. Let's pretend I want to convert between the time in Dublin and the time in New York
?> dublin = Time.now
=> 2012-08-13 08:36:50 +0000
>> new_york = dublin + Time.zone_offset('EST')
=> 2012-08-13 08:36:50 +0000
>> dublin.hour
=> 8
>> new_york.hour
=> 3
Assuming that 'EST' is the name of the Timezone for New York, as Dan points out sometimes 'EDT' is the correct TZ.
This takes advantage of the fact that Time#asctime doesn't include the zone.
Given a time:
>> time = Time.now
=> 2013-03-13 13:01:48 -0500
Force it to another zone (this returns an ActiveSupport::TimeWithZone):
>> ActiveSupport::TimeZone['US/Pacific'].parse(time.asctime)
=> Wed, 13 Mar 2013 13:01:48 PDT -07:00
Note that the original zone is ignored completely. If I convert the original time to utc, the result will be different:
>> ActiveSupport::TimeZone['US/Pacific'].parse(time.getutc.asctime)
=> Wed, 13 Mar 2013 18:01:48 PDT -07:00
You can use to_time or to_datetime on the result to get a corresponding Time or DateTime.
This question uses an interesting approach with DateTime#change to set the tz offset. (Remember that ActiveSupport makes it easy to convert between Time and DateTime.) The downside is that there's no DST detection; you have to do that manually by using TZInfo's current_period.
If given:
2011-10-25 07:21:35 -700
you want:
2011-10-25 07:21:35 UTC
then do:
Time.parse(Time.now.strftime('%Y-%m-%d %H:%M:%S UTC')).to_s
...
>> Time.at(Time.now.utc + Time.zone_offset('PST'))
=> Mon Jun 07 22:46:22 UTC 2010
>> Time.at(Time.now.utc + Time.zone_offset('PDT'))
=> Mon Jun 07 23:46:26 UTC 2010
>> Time.at(Time.now.utc + Time.zone_offset('CST'))
=> Tue Jun 08 00:46:32 UTC 2010
One note: make sure that the current time object is set to UTC time first, otherwise Ruby will try and convert the Time object to your local timezone, thus throwing the calculation. You can always get the adjusted time by applying ".utc" to the end of the above statements in that case.
For those that came across this while looking for a non-rails solution (as I did), TZInfo solved it for me...
require 'tzinfo'
def adjust_time time, time_zone="America/Los_Angeles"
return TZInfo::Timezone.get(time_zone).utc_to_local(time.utc)
end
puts adjust_time(Time.now)
#=> PST or PDT
puts adjust_time(Time.now, "America/New_York")
#=> EST or EDT
This also handles DST, which is what I needed that wasn't handled above.
See: http://tzinfo.rubyforge.org/
in you environment.rb search for the following line.
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names.
config.time_zone = 'UTC'
Keep in mind ActiveRecord and Rails always handle Time as UTC internally.
I'm using Rails 2.0 before they added the code that makes weppos solution work. Here's what I did
# Silly hack, because sometimes the input_date is in the wrong timezone
temp = input_date.to_time.to_a
temp[8] = true
temp[9] = "Eastern Daylight Time"
input_date = Time.local(*temp)
I break the time down into a 10 element array, change the timezone and then convert the array back into a time.
Here is what worked for me...
def convert_zones(to_zone)
to_zone_time = to_zone.localtime
end
# have your time set as time
time = convert_zones(time)
time.strftime("%b #{day}, %Y (%a) #{hour}:%M %p %Z")
This is what I did, as I am not using Rails and don't want to use any non-core gems.
t = Time.now # my local time - which is GMT
zone_offset = 3600 # offset for CET - which is my target time zone
zone_offset += 3600 if t.dst? # an extra hour offset in summer
time_cet = Time.mktime(t.sec, t.min, t.hour, t.mday, t.mon, t.year, nil, nil, t.dst?, zone_offset)
Option 1
Use date_time_attribute gem:
my_date_time = DateTimeAttribute::Container.new(Time.zone.now)
my_date_time.date_time # => 2001-02-03 22:00:00 KRAT +0700
my_date_time.time_zone = 'Moscow'
my_date_time.date_time # => 2001-02-03 22:00:00 MSK +0400
Option 2
If time is used as an attribute, you can use the same date_time_attribute gem:
class Task
include DateTimeAttribute
date_time_attribute :due_at
end
task = Task.new
task.due_at_time_zone = 'Moscow'
task.due_at # => Mon, 03 Feb 2013 22:00:00 MSK +04:00
task.due_at_time_zone = 'London'
task.due_at # => Mon, 03 Feb 2013 22:00:00 GMT +00:00
It's probably a good idea to store the time as UTC and then show it in a specific time zone when it is displayed. Here's an easy way to do that (works in Rails 4, unsure about earlier versions).
t = Time.now.utc
=> 2016-04-19 20:18:33 UTC
t.in_time_zone("EST")
=> Tue, 19 Apr 2016 15:18:33 EST -05:00
But if you really want to store it in a specific timezone, you can just set the initial Time object to itself.in_time_zone like this:
t = t.in_time_zone("EST")
When Parsing a Time
I'd be interested to hear how you're setting the variable foo to begin with.
If you're parsing a time string that doesn't have a time zone (what I was doing during a data import) then you can use String#in_time_zone to force the time zone during the parsing:
"Fri Jun 26 2019 07:00:00".in_time_zone( "Eastern Time (US & Canada)" )
# => Wed, 26 Jun 2019 07:00:00 EDT -04:00
Works like a charm and is super clean.
You can do:
DateTime.parse('Fri Jun 26 07:00:00 UTC 2009').change(offset: '-0400')
Which returns:
Fri, 26 Jun 2009 07:00:00 -0400

Resources