Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 
like image 633
Matteo Melani Avatar asked Oct 22 '22 13:10

Matteo Melani


1 Answers

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:

  • A time zone refers to a specific region on earth, and may optionally have a period of DST.
  • A UTC offset gives only the duration by which the given time is offset from UTC.

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.

like image 58
davetapley Avatar answered Oct 24 '22 09:10

davetapley