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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With