Permalink
Browse files

Ensure duration parsing is consistent across DST changes

Previously `ActiveSupport::Duration.parse` used `Time.current` and
`Time#advance` to calculate the number of seconds in the duration
from an arbitrary collection of parts. However as `advance` tries to
be consistent across DST boundaries this meant that either the
duration was shorter or longer depending on the time of year.

This was fixed by using an absolute reference point in UTC which
isn't subject to DST transitions. An arbitrary date of Jan 1st, 2000
was chosen for no other reason that it seemed appropriate.

Additionally, duration parsing should now be marginally faster as we
are no longer creating instances of `ActiveSupport::TimeWithZone`
every time we parse a duration string.

Fixes #26941.
  • Loading branch information...
pixeltrix committed Oct 31, 2016
1 parent 3c9eb70 commit 8931916f4a1c1d8e70c06063ba63928c5c7eab1e
View
@@ -1,3 +1,23 @@
* Ensure duration parsing is consistent across DST changes
Previously `ActiveSupport::Duration.parse` used `Time.current` and
`Time#advance` to calculate the number of seconds in the duration
from an arbitrary collection of parts. However as `advance` tries to
be consistent across DST boundaries this meant that either the
duration was shorter or longer depending on the time of year.
This was fixed by using an absolute reference point in UTC which
isn't subject to DST transitions. An arbitrary date of Jan 1st, 2000
was chosen for no other reason that it seemed appropriate.
Additionally, duration parsing should now be marginally faster as we
are no longer creating instances of `ActiveSupport::TimeWithZone`
every time we parse a duration string.
Fixes #26941.
*Andrew White*
* Use `Hash#compact` and `Hash#compact!` from Ruby 2.4. Old Ruby versions
will continue to get these methods from Active Support as before.
@@ -7,6 +7,8 @@ module ActiveSupport
#
# 1.month.ago # equivalent to Time.now.advance(months: -1)
class Duration
EPOCH = ::Time.utc(2000)
attr_accessor :value, :parts
autoload :ISO8601Parser, "active_support/duration/iso8601_parser"
@@ -140,8 +142,7 @@ def respond_to_missing?(method, include_private = false) #:nodoc:
# If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+.
def self.parse(iso8601duration)
parts = ISO8601Parser.new(iso8601duration).parse!
time = ::Time.current
new(time.advance(parts) - time, parts)
new(EPOCH.advance(parts) - EPOCH, parts)
end
# Build ISO 8601 Duration string for this duration.
@@ -322,4 +322,35 @@ def test_iso8601_output_and_reparsing
assert_equal time + duration, time + ActiveSupport::Duration.parse(duration.iso8601), pattern.inspect
end
end
def test_iso8601_parsing_across_spring_dst_boundary
with_env_tz eastern_time_zone do
with_tz_default "Eastern Time (US & Canada)" do
travel_to Time.utc(2016, 3, 11) do
assert_equal 604800, ActiveSupport::Duration.parse("P7D").to_i
assert_equal 604800, ActiveSupport::Duration.parse("P1W").to_i
end
end
end
end
def test_iso8601_parsing_across_autumn_dst_boundary
with_env_tz eastern_time_zone do
with_tz_default "Eastern Time (US & Canada)" do
travel_to Time.utc(2016, 11, 4) do
assert_equal 604800, ActiveSupport::Duration.parse("P7D").to_i
assert_equal 604800, ActiveSupport::Duration.parse("P1W").to_i
end
end
end
end
private
def eastern_time_zone
if Gem.win_platform?
"EST5EDT"
else
"America/New_York"
end
end
end

0 comments on commit 8931916

Please sign in to comment.