Matteo Melani
Matteo Melani

Reputation: 2726

In Rails 3 How to parse ISO8601 date string to get a TimeWithZone instance?

Here is my solution. Is there a more compact one?

> time_from_client = "2001-03-30T19:00:00-05:00"
 => "2001-03-30T19:00:00-05:00" 

> time_from_client.to_datetime
 => Fri, 30 Mar 2001 19:00:00 -0500 

> timezone_offset = time_from_client.to_datetime.offset.numerator
 => -5 

> tz = ActiveSupport::TimeZone[timezone_offset]
 => (GMT-05:00) America/New_York

> tz.class
=> ActiveSupport::TimeZone 

Upvotes: 5

Views: 5735

Answers (2)

davetapley
davetapley

Reputation: 17898

Unfortunately this isn't possible, at least without getting much more clever.

To understand why you must make a distinction between a time zone and a UTC offset:

Consider mapping from zone to offset: You must also know the time in question, as well as the zone, in order to decide whether to apply the standard or daylight offset.

Going the other way is much harder. Once again just having the offset isn't enough, because we don't know whether that offset refers to standard or daylight time. But this time we have a chicken/egg problem: Even if we have the time as well, we need to know the zone in order to see if that time was standard or daylight time. But, we don't have the zone.


Here's a worked example from yours, you're using Fri, 30 Mar 2001 19:00:00, which happens to be standard time (EST), so at first pass it looks good:

> time_from_client = "2001-03-30T19:00:00-05:00"
 => "2001-03-30T19:00:00-05:00" 


> time_from_client.to_datetime
 => Fri, 30 Mar 2001 19:00:00 -0500 

> timezone_offset = time_from_client.to_datetime.offset.numerator
 => -5 

> tz = ActiveSupport::TimeZone[timezone_offset]
 => (GMT-05:00) America/New_York

We have America/New_York.


But see what happens if we jump to summer time, let's say, 30 Jun 2001 19:00:00. The offset component of your time_from_client will now be -04:00, which is the daylight time offset for New York (EDT).

> time_from_client = "2001-03-30T19:00:00-4:00"
 => "2001-06-30T19:00:00-05:00" 


> time_from_client.to_datetime
 => Fri, 30 Jun 2001 19:00:00 -0400 

Disclaimer: The next step doesn't actually work, because numerator will round down 4/24 to 1/6, and you'll get an incorrect timezone_offset of 1. As such I've tweaked your implementation and used utc_offset.

> timezone_offset = time_from_client.to_datetime.utc_offset
 => -14400

> tz = ActiveSupport::TimeZone[timezone_offset]
 => (GMT-04:00) Atlantic Time (Canada)

The problem can now be seen, instead of getting America/New_York we get Atlantic Time (Canada). The latter is one of the zone names for the standard offset -04:00, because the implementation of ActiveSupport::TimeZone[] can only find using the standard utc_offset, and isn't aware of daylight.

If you follow this to its conclusion you end up with the following counter-intuitive parse:

> tz.parse "2001-06-30T19:00:00-04:00"
 => Sat, 30 Jun 2001 20:00:00 ADT -03:00

What I assume happens here is TimeWithZone sees this is June, and so adjusts to the Atlantic Daylight offset, -03:00.


It's worth noting that without even if you could account for daylight, and obtain the standard offset to pass to ActiveSupport::TimeZone[], you'd still not have the correct zone, because the offset to zone mapping isn't one-to-one.

As demonstrated here:

ActiveSupport::TimeZone.all.select { |z| z.utc_offset == -14400 }
=> [(GMT-04:00) Atlantic Time (Canada), (GMT-04:00) Georgetown, (GMT-04:00) La Paz, (GMT-04:00) Santiago]

This is my reason for thinking this isn't possible, unless you also happen to have location information for to the original ISO 8601 string.

Incidentally, if you pursue this approach I recommend the tzwhere Node.js library, which can uses zone geometry do do location to zone look ups.

Upvotes: 2

Jim Stewart
Jim Stewart

Reputation: 17323

Check out the documentation for ActiveSupport::TimeWithZone. Short answer: use Time.parse(time_string).in_time_zone:

[9] pry(main)> Time.parse("2001-03-30T19:00:00-05:00")
=> 2001-03-30 19:00:00 -0500
[10] pry(main)> Time.parse("2001-03-30T19:00:00-05:00").class
=> Time
[11] pry(main)> Time.parse("2001-03-30T19:00:00-05:00").in_time_zone
=> Sat, 31 Mar 2001 00:00:00 UTC +00:00
[12] pry(main)> Time.parse("2001-03-30T19:00:00-05:00").in_time_zone.class
=> ActiveSupport::TimeWithZone

If you want it in another time zone:

[13] pry(main)> Time.parse("2001-03-30T19:00:00-05:00").in_time_zone("America/Los_Angeles")
=> Fri, 30 Mar 2001 16:00:00 PST -08:00

Upvotes: 1

Related Questions