aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgbuesing <gbuesing@gmail.com>2008-06-29 15:17:51 -0500
committergbuesing <gbuesing@gmail.com>2008-06-29 15:21:40 -0500
commit676d6a651497b56f31e00db4e7795669de0da672 (patch)
tree65e609936284d02bb3db3b335392fcbd8b8a7aef
parent01db5ded54b0e3a2ea80d28e4841d40fcec23cdf (diff)
downloadrails-676d6a651497b56f31e00db4e7795669de0da672.tar.gz
rails-676d6a651497b56f31e00db4e7795669de0da672.tar.bz2
rails-676d6a651497b56f31e00db4e7795669de0da672.zip
TimeWithZone: when crossing DST boundary, treat Durations of days, months or years as variable-length, and all other values as absolute length. A time + 24.hours will advance exactly 24 hours, but a time + 1.day will advance 23-25 hours, depending on the day. Ensure consistent behavior across all advancing methods.
-rw-r--r--activesupport/CHANGELOG2
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb45
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb168
3 files changed, 203 insertions, 12 deletions
diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG
index a540150329..f0c1a208a5 100644
--- a/activesupport/CHANGELOG
+++ b/activesupport/CHANGELOG
@@ -1,5 +1,7 @@
*Edge*
+* TimeWithZone: when crossing DST boundary, treat Durations of days, months or years as variable-length, and all other values as absolute length. A time + 24.hours will advance exactly 24 hours, but a time + 1.day will advance 23-25 hours, depending on the day. Ensure consistent behavior across all advancing methods [Geoff Buesing]
+
* Added TimeZone #=~, to support matching zones by regex in time_zone_select. #195 [Ernie Miller]
* Added Array#second through Array#tenth as aliases for Array#[1] through Array#[9] [DHH]
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index bfc5b16039..7370a45931 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -159,19 +159,24 @@ module ActiveSupport
utc == other
end
- # If wrapped +time+ is a DateTime, use DateTime#since instead of <tt>+</tt>.
- # Otherwise, just pass on to +method_missing+.
def +(other)
- result = utc.acts_like?(:date) ? utc.since(other) : utc + other rescue utc.since(other)
- result.in_time_zone(time_zone)
+ # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time,
+ # otherwise move forward from #utc, for accuracy when moving across DST boundaries
+ if duration_of_variable_length?(other)
+ method_missing(:+, other)
+ else
+ result = utc.acts_like?(:date) ? utc.since(other) : utc + other rescue utc.since(other)
+ result.in_time_zone(time_zone)
+ end
end
-
- # If a time-like object is passed in, compare it with +utc+.
- # Else if wrapped +time+ is a DateTime, use DateTime#ago instead of DateTime#-.
- # Otherwise, just pass on to +method_missing+.
+
def -(other)
+ # If we're subtracting a Duration of variable length (i.e., years, months, days), move backwards from #time,
+ # otherwise move backwards #utc, for accuracy when moving across DST boundaries
if other.acts_like?(:time)
utc - other
+ elsif duration_of_variable_length?(other)
+ method_missing(:-, other)
else
result = utc.acts_like?(:date) ? utc.ago(other) : utc - other rescue utc.ago(other)
result.in_time_zone(time_zone)
@@ -179,15 +184,27 @@ module ActiveSupport
end
def since(other)
- utc.since(other).in_time_zone(time_zone)
+ # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time,
+ # otherwise move forward from #utc, for accuracy when moving across DST boundaries
+ if duration_of_variable_length?(other)
+ method_missing(:since, other)
+ else
+ utc.since(other).in_time_zone(time_zone)
+ end
end
def ago(other)
- utc.ago(other).in_time_zone(time_zone)
+ since(-other)
end
-
+
def advance(options)
- utc.advance(options).in_time_zone(time_zone)
+ # If we're advancing a value of variable length (i.e., years, months, days), advance from #time,
+ # otherwise advance from #utc, for accuracy when moving across DST boundaries
+ if options.detect {|k,v| [:years, :months, :days].include? k}
+ method_missing(:advance, options)
+ else
+ utc.advance(options).in_time_zone(time_zone)
+ end
end
%w(year mon month day mday hour min sec).each do |method_name|
@@ -279,5 +296,9 @@ module ActiveSupport
def transfer_time_values_to_utc_constructor(time)
::Time.utc_time(time.year, time.month, time.day, time.hour, time.min, time.sec, time.respond_to?(:usec) ? time.usec : 0)
end
+
+ def duration_of_variable_length?(obj)
+ ActiveSupport::Duration === obj && obj.parts.flatten.detect {|p| [:years, :months, :days].include? p }
+ end
end
end
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index 0977cd8e50..ac52a1be0b 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -485,6 +485,7 @@ class TimeWithZoneTest < Test::Unit::TestCase
twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2004,2,29))
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.advance(:years => 1).inspect
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.years_since(1).inspect
+ assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.since(1.year).inspect
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", (twz + 1.year).inspect
end
@@ -492,6 +493,7 @@ class TimeWithZoneTest < Test::Unit::TestCase
twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2005,1,31))
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.advance(:months => 1).inspect
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.months_since(1).inspect
+ assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", twz.since(1.month).inspect
assert_equal "Mon, 28 Feb 2005 00:00:00 EST -05:00", (twz + 1.month).inspect
end
@@ -499,6 +501,7 @@ class TimeWithZoneTest < Test::Unit::TestCase
twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2000,1,31))
assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", twz.advance(:months => 1).inspect
assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", twz.months_since(1).inspect
+ assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", twz.since(1.month).inspect
assert_equal "Tue, 29 Feb 2000 00:00:00 EST -05:00", (twz + 1.month).inspect
end
@@ -506,6 +509,7 @@ class TimeWithZoneTest < Test::Unit::TestCase
twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,3,2,2))
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.advance(:months => 1).inspect
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.months_since(1).inspect
+ assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.since(1.month).inspect
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", (twz + 1.month).inspect
end
@@ -513,8 +517,172 @@ class TimeWithZoneTest < Test::Unit::TestCase
twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,1,59,59))
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.advance(:seconds => 1).inspect
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", (twz + 1).inspect
+ assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.since(1).inspect
+ assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", twz.since(1.second).inspect
assert_equal "Sun, 02 Apr 2006 03:00:00 EDT -04:00", (twz + 1.second).inspect
end
+
+ def test_advance_1_day_across_spring_dst_transition
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,1,10,30))
+ # In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long
+ # When we advance 1 day, we want to end up at the same time on the next day
+ assert_equal "Sun, 02 Apr 2006 10:30:00 EDT -04:00", twz.advance(:days => 1).inspect
+ assert_equal "Sun, 02 Apr 2006 10:30:00 EDT -04:00", twz.since(1.days).inspect
+ assert_equal "Sun, 02 Apr 2006 10:30:00 EDT -04:00", (twz + 1.days).inspect
+ assert_equal "Sun, 02 Apr 2006 10:30:01 EDT -04:00", twz.since(1.days + 1.second).inspect
+ assert_equal "Sun, 02 Apr 2006 10:30:01 EDT -04:00", (twz + 1.days + 1.second).inspect
+ end
+
+ def test_advance_1_day_across_spring_dst_transition_backwards
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,10,30))
+ # In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long
+ # When we advance back 1 day, we want to end up at the same time on the previous day
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(:days => -1).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(1.days).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 1.days).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:01 EST -05:00", twz.ago(1.days - 1.second).inspect
+ end
+
+ def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_spring_dst_transition
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,1,10,30))
+ # In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long
+ # When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", (twz + 86400).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", (twz + 86400.seconds).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.since(86400).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.since(86400.seconds).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.advance(:seconds => 86400).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", (twz + 1440.minutes).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.since(1440.minutes).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.advance(:minutes => 1440).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", (twz + 24.hours).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.since(24.hours).inspect
+ assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.advance(:hours => 24).inspect
+ end
+
+ def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_spring_dst_transition_backwards
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,2,11,30))
+ # In 2006, spring DST transition occurred Apr 2 at 2AM; this day was only 23 hours long
+ # When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 86400).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 86400.seconds).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(86400).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(86400.seconds).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(:seconds => -86400).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 1440.minutes).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(1440.minutes).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(:minutes => -1440).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 24.hours).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(24.hours).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(:hours => -24).inspect
+ end
+
+ def test_advance_1_day_across_fall_dst_transition
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30))
+ # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long
+ # When we advance 1 day, we want to end up at the same time on the next day
+ assert_equal "Sun, 29 Oct 2006 10:30:00 EST -05:00", twz.advance(:days => 1).inspect
+ assert_equal "Sun, 29 Oct 2006 10:30:00 EST -05:00", twz.since(1.days).inspect
+ assert_equal "Sun, 29 Oct 2006 10:30:00 EST -05:00", (twz + 1.days).inspect
+ assert_equal "Sun, 29 Oct 2006 10:30:01 EST -05:00", twz.since(1.days + 1.second).inspect
+ assert_equal "Sun, 29 Oct 2006 10:30:01 EST -05:00", (twz + 1.days + 1.second).inspect
+ end
+
+ def test_advance_1_day_across_fall_dst_transition_backwards
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,29,10,30))
+ # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long
+ # When we advance backwards 1 day, we want to end up at the same time on the previous day
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(:days => -1).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(1.days).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 1.days).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:01 EDT -04:00", twz.ago(1.days - 1.second).inspect
+ end
+
+ def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_fall_dst_transition
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30))
+ # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long
+ # When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", (twz + 86400).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", (twz + 86400.seconds).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.since(86400).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.since(86400.seconds).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.advance(:seconds => 86400).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", (twz + 1440.minutes).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.since(1440.minutes).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.advance(:minutes => 1440).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", (twz + 24.hours).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.since(24.hours).inspect
+ assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.advance(:hours => 24).inspect
+ end
+
+ def test_advance_1_day_expressed_as_number_of_seconds_minutes_or_hours_across_fall_dst_transition_backwards
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,29,9,30))
+ # In 2006, fall DST transition occurred Oct 29 at 2AM; this day was 25 hours long
+ # When we advance a specific number of hours, minutes or seconds, we want to advance exactly that amount
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 86400).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 86400.seconds).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(86400).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(86400.seconds).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(:seconds => -86400).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 1440.minutes).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(1440.minutes).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(:minutes => -1440).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 24.hours).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(24.hours).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(:hours => -24).inspect
+ end
+
+ def test_advance_1_month_across_spring_dst_transition
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,4,1,10,30))
+ assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", twz.advance(:months => 1).inspect
+ assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", twz.months_since(1).inspect
+ assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", twz.since(1.month).inspect
+ assert_equal "Mon, 01 May 2006 10:30:00 EDT -04:00", (twz + 1.month).inspect
+ end
+
+ def test_advance_1_month_across_spring_dst_transition_backwards
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,5,1,10,30))
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.advance(:months => -1).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.months_ago(1).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", twz.ago(1.month).inspect
+ assert_equal "Sat, 01 Apr 2006 10:30:00 EST -05:00", (twz - 1.month).inspect
+ end
+
+ def test_advance_1_month_across_fall_dst_transition
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,10,28,10,30))
+ assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", twz.advance(:months => 1).inspect
+ assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", twz.months_since(1).inspect
+ assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", twz.since(1.month).inspect
+ assert_equal "Tue, 28 Nov 2006 10:30:00 EST -05:00", (twz + 1.month).inspect
+ end
+
+ def test_advance_1_month_across_fall_dst_transition_backwards
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2006,11,28,10,30))
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.advance(:months => -1).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.months_ago(1).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", twz.ago(1.month).inspect
+ assert_equal "Sat, 28 Oct 2006 10:30:00 EDT -04:00", (twz - 1.month).inspect
+ end
+
+ def test_advance_1_year
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008,2,15,10,30))
+ assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", twz.advance(:years => 1).inspect
+ assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", twz.years_since(1).inspect
+ assert_equal "Sun, 15 Feb 2009 10:30:00 EST -05:00", (twz + 1.year).inspect
+ assert_equal "Thu, 15 Feb 2007 10:30:00 EST -05:00", twz.advance(:years => -1).inspect
+ assert_equal "Thu, 15 Feb 2007 10:30:00 EST -05:00", twz.years_ago(1).inspect
+ assert_equal "Thu, 15 Feb 2007 10:30:00 EST -05:00", (twz - 1.year).inspect
+ end
+
+ def test_advance_1_year_during_dst
+ twz = ActiveSupport::TimeWithZone.new(nil, @time_zone, Time.utc(2008,7,15,10,30))
+ assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", twz.advance(:years => 1).inspect
+ assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", twz.years_since(1).inspect
+ assert_equal "Wed, 15 Jul 2009 10:30:00 EDT -04:00", (twz + 1.year).inspect
+ assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", twz.advance(:years => -1).inspect
+ assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", twz.years_ago(1).inspect
+ assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", (twz - 1.year).inspect
+ end
end
class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase