-
Notifications
You must be signed in to change notification settings - Fork 0
TimeEdgeCasesAndZones
Edge cases of time; time zones in Ruby and ROR
Discusses the time edge cases (relating to daylight/standard time changes); handling of time zones in Ruby and Ruby on Rails. (This is mostly for my own benefit, as others are likely to be acquainted with most of this.)
There are two time edge cases. These are nicely illustrated at http://mad.ly/2008/04/03/time-zone-visualizations/
The cases exist (if and) when time changes from standard (S) to daylight (D) and when it changes back.
For instance in Portland OR, time switched from S to D on March 9, 2008. Time will switch from D to S on November 2, 2008.
At the instant that would be 0200 S, clocks were moved ahead to 0300 D. The problem is that there is no valid time (either S or D) >= 0200 AND < 0300.
Ruby handles this without creating an exception, but the handling is imperfect. If you give Ruby an invalid time on this day (>= 0200 AND < 0300) Ruby arbitrarily converts it to a time between 0100 and 0200 S. For example, March 9, 2008, 2:15 PST is not a valid time.
> spring_invalid_time = Time.local(2008, 3, 9, 2, 15)
=> Sun Mar 09 01:15:00 -0800 2008
> spring_invalid_time.zone
=> "Pacific Standard Time"
At the instant that would be 0200 D, clocks will be moved back to 0100 S. The problem is that (unless S or D is specified), times >=0100 AND < 0200 are ambiguous -- they could be either S or D.
Ruby likewise handles this without creating an exception, but the handling is imperfect. If you give Ruby an ambiguous time on this day (>= 0200 AND < 0300) Ruby assumes you mean S. For example, on November 11, 2008, times between 0100 and 0200 can be either PDT or PST, but Ruby handles them all as PST:
> fall_ambiguous_time = Time.local(2008, 11, 2, 1, 15) => Sun Nov 02 01:15:00 -0800 2008
> fall_ambiguous_time.zone => "Pacific Standard Time"
Thus to give Ruby a time equal to between 0100 Daylight and 0200 Daylight, local time, you must resort to something else. For example, this could include specifying the zone, or using UTC.
Calagator time inputs currently have exactly the same vulnerabilities (as of r918). For instance, (1) if you create an event starting March 9 at 02:15, it will be turned into 01:15 Standard time; (2) if you create an event starting November 2 at 01:15, Calagator assume that this is 01:15 standard time -- it's impossible to enter a time on November 2 between 01:00 and 02:00 Daylight time.
We currently store start_time and duration, as opposed to start_time and end_time. But we don't ask the user for duration: it's calculated from the End Time that the user supplies to the form.
If we asked the user for duration, we could handle the the edge cases better. But once you've calculated the end_time, I'm not understanding why it makes any difference (for the edge cases) whether you store end_time or duration. All times are stored as UTC, which does not use daylight/standard. UTC unambiguously converts to local time. They are then displayed in local time.
(By local time, I mean the server's local time. If we want to get or display times in the user's local time, then we need to track that also.)
Stored internally as the fractional number of seconds since the epoch (Jan 1 1970). "It is a thin layer over the system date and time functionality provided by the operating system." Flanagan & Matsumoto, The Ruby Programming Language at 325 (1st ed. 2008). Some operating systems can have negative systems, and thus represent Times before the epoch. Some operating systems on or after January 19, 2038.
Times that display the same may be unequal because of small microsecond differences. Can represent time in one of two zones: (1) UTC, (2) the server's local zone. Time is aware of both zones and whether the zone uses daylight time. Time uses simplifying interpretations in the edge cases. It always returns a valid time, even if given an invalid time or an ambiguous time.
Invalid time arbitrarily interpreted as valid standard time:
> spring_invalid_time = Time.local(2008, 3, 9, 2, 15)
=> Sun Mar 09 01:15:00 -0800 2008
> spring_invalid_time.zone
=> "Pacific Standard Time"
Ambiguous time arbitrarily interpreted as standard time.
> fall_ambiguous_time = Time.local(2008, 11, 2, 1, 15) => Sun Nov 02 01:15:00 -0800 2008
> fall_ambiguous_time.zone => "Pacific Standard Time"
Another with this class is that setting the ENV['TZ'] variable supposedly doesn't work in Windows, so you can't change the server's local time zone. The TZinfo gem was developed to deal with these problems. See http://article.gmane.org/gmane.comp.lang.ruby.rails/93924/match=75790
Stored internally as the fractional number of days since 4712 BCE. Is aware of time zone offset, but not of daylight time. See http://www.ruby-doc.org/stdlib/libdoc/date/rdoc/classes/DateTime.html
This includes a TimeZone class that handles Daylight time and can be dropped in as a replacement for the Ruby class of the same name. One must install both the tzino gem and the tzinfo_timezone plug in. Correctly handles all edge cases, including complaining about ambiguous or invalid times. See http://article.gmane.org/gmane.comp.lang.ruby.rails/93924/match=75790
irb(main):005:0> TZInfo::Timezone.get('US/Pacific').local_to_utc(DateTime.new(2008, 3, 9, 2, 15))
TZInfo::PeriodNotFound: TZInfo::PeriodNotFound
irb(main):002:0> TZInfo::Timezone.get('US/Pacific').local_to_utc(DateTime.new(2008, 11, 2, 1, 30))
TZInfo::AmbiguousTime: DateTime: 2008-11-02T01:30:00+00:00 is an ambiguous local time.
But it runs very slowly:
"TZInfo (version 0.3.1 on Ruby 1.8.5) is about 9 times slower than libc converting from UTC to local and about 7 times slower converting from local to UTC"
See http://www.ruby-forum.com/topic/79431
Rails ActiveSupport::TimeWithZone is a custom implementation of TZinfo. Bundles the TZInfo gem into Rails. Can represent a time in any time zone. Wraps the Ruby Time class with a zone.
>> Time.zone = 'Pacific Time (US & Canada)'
=> "Pacific Time (US & Canada)"
>> Time.now
=> Tue Jul 08 16:21:38 -0700 2008
>> Time.now.in_time_zone
=> Tue, 08 Jul 2008 16:22:01 PDT -07:00
But ActiveSupport::TimeWithZone does not throw exceptions for invalid or ambiguous times. Instead, it uses arbitary simplifying interpretations. Those interpretations are different from those of Time.
>> Time.zone = 'US/Pacific'
=> "US/Pacific"
>> spring_invalid = Time.zone.local(2008, 3, 9, 2, 15)
=> Sun, 09 Mar 2008 03:15:00 PDT -07:00
>> fall_ambiguous = Time.zone.local(2008, 11, 2, 1, 15)
=> Sun, 02 Nov 2008 01:15:00 PDT -07:00
Some general discussions: http://ryandaigle.com/articles/2008/1/25/what-s-new-in-edge-rails-easier-timezones
"Set the Time.zone variable to the local timezone. All further date manipulations will automatically reflect this local time while being saved to the database in UTC."
Thorough discussion, including upgrade info at: Rails 2.1 Time Zone Support: An Overview
INITIAL REVIEW NEEDED