mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Partial backport of AS::Duration changes from #27610
Backports the fix to not use an epoch date when calculating durations along with additional refactoring and tests from these commits:cb9d0e4864
9ae511f550
2a5ae2b714
DOES NOT backport any changes to the length of durations.
This commit is contained in:
parent
3a3263c5ce
commit
19c450d5d9
6 changed files with 122 additions and 26 deletions
|
@ -1,3 +1,16 @@
|
||||||
|
* Fix inconsistent results when parsing large durations and constructing durations from code
|
||||||
|
|
||||||
|
ActiveSupport::Duration.parse('P3Y') == 3.years # It should be true
|
||||||
|
|
||||||
|
Duration parsing made independent from any moment of time:
|
||||||
|
Fixed length in seconds is assigned to each duration part during parsing.
|
||||||
|
|
||||||
|
Methods on `Numeric` like `2.days` now use these predefined durations
|
||||||
|
to avoid duplicating of duration constants through the codebase and
|
||||||
|
eliminate creation of intermediate durations.
|
||||||
|
|
||||||
|
*Andrey Novikov, Andrew White*
|
||||||
|
|
||||||
* In Core Extensions, make `MarshalWithAutoloading#load` pass through the second, optional
|
* In Core Extensions, make `MarshalWithAutoloading#load` pass through the second, optional
|
||||||
argument for `Marshal#load( source [, proc] )`. This way we don't have to do
|
argument for `Marshal#load( source [, proc] )`. This way we don't have to do
|
||||||
`Marshal.method(:load).super_method.call(sourse, proc)` just to be able to pass a proc.
|
`Marshal.method(:load).super_method.call(sourse, proc)` just to be able to pass a proc.
|
||||||
|
|
|
@ -18,12 +18,12 @@ class Integer
|
||||||
# # equivalent to Time.now.advance(months: 4, years: 5)
|
# # equivalent to Time.now.advance(months: 4, years: 5)
|
||||||
# (4.months + 5.years).from_now
|
# (4.months + 5.years).from_now
|
||||||
def months
|
def months
|
||||||
ActiveSupport::Duration.new(self * 30.days, [[:months, self]])
|
ActiveSupport::Duration.months(self)
|
||||||
end
|
end
|
||||||
alias :month :months
|
alias :month :months
|
||||||
|
|
||||||
def years
|
def years
|
||||||
ActiveSupport::Duration.new(self * 365.25.days.to_i, [[:years, self]])
|
ActiveSupport::Duration.years(self)
|
||||||
end
|
end
|
||||||
alias :year :years
|
alias :year :years
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,7 +19,7 @@ class Numeric
|
||||||
# # equivalent to Time.current.advance(months: 4, years: 5)
|
# # equivalent to Time.current.advance(months: 4, years: 5)
|
||||||
# (4.months + 5.years).from_now
|
# (4.months + 5.years).from_now
|
||||||
def seconds
|
def seconds
|
||||||
ActiveSupport::Duration.new(self, [[:seconds, self]])
|
ActiveSupport::Duration.seconds(self)
|
||||||
end
|
end
|
||||||
alias :second :seconds
|
alias :second :seconds
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class Numeric
|
||||||
#
|
#
|
||||||
# 2.minutes # => 2 minutes
|
# 2.minutes # => 2 minutes
|
||||||
def minutes
|
def minutes
|
||||||
ActiveSupport::Duration.new(self * 60, [[:minutes, self]])
|
ActiveSupport::Duration.minutes(self)
|
||||||
end
|
end
|
||||||
alias :minute :minutes
|
alias :minute :minutes
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class Numeric
|
||||||
#
|
#
|
||||||
# 2.hours # => 2 hours
|
# 2.hours # => 2 hours
|
||||||
def hours
|
def hours
|
||||||
ActiveSupport::Duration.new(self * 3600, [[:hours, self]])
|
ActiveSupport::Duration.hours(self)
|
||||||
end
|
end
|
||||||
alias :hour :hours
|
alias :hour :hours
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ class Numeric
|
||||||
#
|
#
|
||||||
# 2.days # => 2 days
|
# 2.days # => 2 days
|
||||||
def days
|
def days
|
||||||
ActiveSupport::Duration.new(self * 24.hours, [[:days, self]])
|
ActiveSupport::Duration.days(self)
|
||||||
end
|
end
|
||||||
alias :day :days
|
alias :day :days
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ class Numeric
|
||||||
#
|
#
|
||||||
# 2.weeks # => 2 weeks
|
# 2.weeks # => 2 weeks
|
||||||
def weeks
|
def weeks
|
||||||
ActiveSupport::Duration.new(self * 7.days, [[:weeks, self]])
|
ActiveSupport::Duration.weeks(self)
|
||||||
end
|
end
|
||||||
alias :week :weeks
|
alias :week :weeks
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class Numeric
|
||||||
#
|
#
|
||||||
# 2.fortnights # => 4 weeks
|
# 2.fortnights # => 4 weeks
|
||||||
def fortnights
|
def fortnights
|
||||||
ActiveSupport::Duration.new(self * 2.weeks, [[:weeks, self * 2]])
|
ActiveSupport::Duration.weeks(self * 2)
|
||||||
end
|
end
|
||||||
alias :fortnight :fortnights
|
alias :fortnight :fortnights
|
||||||
|
|
||||||
|
|
|
@ -7,13 +7,82 @@ module ActiveSupport
|
||||||
#
|
#
|
||||||
# 1.month.ago # equivalent to Time.now.advance(months: -1)
|
# 1.month.ago # equivalent to Time.now.advance(months: -1)
|
||||||
class Duration
|
class Duration
|
||||||
EPOCH = ::Time.utc(2000)
|
SECONDS_PER_MINUTE = 60
|
||||||
|
SECONDS_PER_HOUR = 3600
|
||||||
|
SECONDS_PER_DAY = 86400
|
||||||
|
SECONDS_PER_WEEK = 604800
|
||||||
|
SECONDS_PER_MONTH = 2592000 # 30 days
|
||||||
|
SECONDS_PER_YEAR = 31557600 # length of a julian year (365.2425 days)
|
||||||
|
|
||||||
|
PARTS_IN_SECONDS = {
|
||||||
|
seconds: 1,
|
||||||
|
minutes: SECONDS_PER_MINUTE,
|
||||||
|
hours: SECONDS_PER_HOUR,
|
||||||
|
days: SECONDS_PER_DAY,
|
||||||
|
weeks: SECONDS_PER_WEEK,
|
||||||
|
months: SECONDS_PER_MONTH,
|
||||||
|
years: SECONDS_PER_YEAR
|
||||||
|
}.freeze
|
||||||
|
|
||||||
attr_accessor :value, :parts
|
attr_accessor :value, :parts
|
||||||
|
|
||||||
autoload :ISO8601Parser, 'active_support/duration/iso8601_parser'
|
autoload :ISO8601Parser, 'active_support/duration/iso8601_parser'
|
||||||
autoload :ISO8601Serializer, 'active_support/duration/iso8601_serializer'
|
autoload :ISO8601Serializer, 'active_support/duration/iso8601_serializer'
|
||||||
|
|
||||||
|
class << self
|
||||||
|
# Creates a new Duration from string formatted according to ISO 8601 Duration.
|
||||||
|
#
|
||||||
|
# See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
|
||||||
|
# This method allows negative parts to be present in pattern.
|
||||||
|
# If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+.
|
||||||
|
def parse(iso8601duration)
|
||||||
|
parts = ISO8601Parser.new(iso8601duration).parse!
|
||||||
|
new(calculate_total_seconds(parts), parts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ===(other) #:nodoc:
|
||||||
|
other.is_a?(Duration)
|
||||||
|
rescue ::NoMethodError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def seconds(value) #:nodoc:
|
||||||
|
new(value, [[:seconds, value]])
|
||||||
|
end
|
||||||
|
|
||||||
|
def minutes(value) #:nodoc:
|
||||||
|
new(value * SECONDS_PER_MINUTE, [[:minutes, value]])
|
||||||
|
end
|
||||||
|
|
||||||
|
def hours(value) #:nodoc:
|
||||||
|
new(value * SECONDS_PER_HOUR, [[:hours, value]])
|
||||||
|
end
|
||||||
|
|
||||||
|
def days(value) #:nodoc:
|
||||||
|
new(value * SECONDS_PER_DAY, [[:days, value]])
|
||||||
|
end
|
||||||
|
|
||||||
|
def weeks(value) #:nodoc:
|
||||||
|
new(value * SECONDS_PER_WEEK, [[:weeks, value]])
|
||||||
|
end
|
||||||
|
|
||||||
|
def months(value) #:nodoc:
|
||||||
|
new(value * SECONDS_PER_MONTH, [[:months, value]])
|
||||||
|
end
|
||||||
|
|
||||||
|
def years(value) #:nodoc:
|
||||||
|
new(value * SECONDS_PER_YEAR, [[:years, value]])
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def calculate_total_seconds(parts)
|
||||||
|
parts.inject(0) do |total, (part, value)|
|
||||||
|
total + value * PARTS_IN_SECONDS[part]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(value, parts) #:nodoc:
|
def initialize(value, parts) #:nodoc:
|
||||||
@value, @parts = value, parts
|
@value, @parts = value, parts
|
||||||
end
|
end
|
||||||
|
@ -99,12 +168,6 @@ module ActiveSupport
|
||||||
@value.hash
|
@value.hash
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.===(other) #:nodoc:
|
|
||||||
other.is_a?(Duration)
|
|
||||||
rescue ::NoMethodError
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
# Calculates a new Time or Date that is as far in the future
|
# Calculates a new Time or Date that is as far in the future
|
||||||
# as this Duration represents.
|
# as this Duration represents.
|
||||||
def since(time = ::Time.current)
|
def since(time = ::Time.current)
|
||||||
|
@ -135,16 +198,6 @@ module ActiveSupport
|
||||||
@value.respond_to?(method, include_private)
|
@value.respond_to?(method, include_private)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Creates a new Duration from string formatted according to ISO 8601 Duration.
|
|
||||||
#
|
|
||||||
# See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
|
|
||||||
# This method allows negative parts to be present in pattern.
|
|
||||||
# If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+.
|
|
||||||
def self.parse(iso8601duration)
|
|
||||||
parts = ISO8601Parser.new(iso8601duration).parse!
|
|
||||||
new(EPOCH.advance(parts) - EPOCH, parts)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Build ISO 8601 Duration string for this duration.
|
# Build ISO 8601 Duration string for this duration.
|
||||||
# The +precision+ parameter can be used to limit seconds' precision of duration.
|
# The +precision+ parameter can be used to limit seconds' precision of duration.
|
||||||
def iso8601(precision: nil)
|
def iso8601(precision: nil)
|
||||||
|
|
|
@ -233,6 +233,21 @@ class DurationTest < ActiveSupport::TestCase
|
||||||
assert_equal(1, (61 <=> 1.minute))
|
assert_equal(1, (61 <=> 1.minute))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_adding_one_month_maintains_day_of_month
|
||||||
|
(1..11).each do |month|
|
||||||
|
[1, 14, 28].each do |day|
|
||||||
|
assert_equal Date.civil(2016, month + 1, day), Date.civil(2016, month, day) + 1.month
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal Date.civil(2017, 1, 1), Date.civil(2016, 12, 1) + 1.month
|
||||||
|
assert_equal Date.civil(2017, 1, 14), Date.civil(2016, 12, 14) + 1.month
|
||||||
|
assert_equal Date.civil(2017, 1, 28), Date.civil(2016, 12, 28) + 1.month
|
||||||
|
|
||||||
|
assert_equal Date.civil(2015, 2, 28), Date.civil(2015, 1, 31) + 1.month
|
||||||
|
assert_equal Date.civil(2016, 2, 29), Date.civil(2016, 1, 31) + 1.month
|
||||||
|
end
|
||||||
|
|
||||||
# ISO8601 string examples are taken from ISO8601 gem at https://github.com/arnau/ISO8601/blob/b93d466840/spec/iso8601/duration_spec.rb
|
# ISO8601 string examples are taken from ISO8601 gem at https://github.com/arnau/ISO8601/blob/b93d466840/spec/iso8601/duration_spec.rb
|
||||||
# published under the conditions of MIT license at https://github.com/arnau/ISO8601/blob/b93d466840/LICENSE
|
# published under the conditions of MIT license at https://github.com/arnau/ISO8601/blob/b93d466840/LICENSE
|
||||||
#
|
#
|
||||||
|
@ -340,6 +355,21 @@ class DurationTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_iso8601_parsing_equivalence_with_numeric_extensions_over_long_periods
|
||||||
|
with_env_tz eastern_time_zone do
|
||||||
|
with_tz_default "Eastern Time (US & Canada)" do
|
||||||
|
assert_equal 3.months, ActiveSupport::Duration.parse("P3M")
|
||||||
|
assert_equal 3.months.to_i, ActiveSupport::Duration.parse("P3M").to_i
|
||||||
|
assert_equal 10.months, ActiveSupport::Duration.parse("P10M")
|
||||||
|
assert_equal 10.months.to_i, ActiveSupport::Duration.parse("P10M").to_i
|
||||||
|
assert_equal 3.years, ActiveSupport::Duration.parse("P3Y")
|
||||||
|
assert_equal 3.years.to_i, ActiveSupport::Duration.parse("P3Y").to_i
|
||||||
|
assert_equal 10.years, ActiveSupport::Duration.parse("P10Y")
|
||||||
|
assert_equal 10.years.to_i, ActiveSupport::Duration.parse("P10Y").to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def eastern_time_zone
|
def eastern_time_zone
|
||||||
if Gem.win_platform?
|
if Gem.win_platform?
|
||||||
|
|
|
@ -683,7 +683,7 @@ Ruby instruction to be executed -- in this case, Active Support's `week` method.
|
||||||
51: #
|
51: #
|
||||||
52: # 2.weeks # => 14 days
|
52: # 2.weeks # => 14 days
|
||||||
53: def weeks
|
53: def weeks
|
||||||
=> 54: ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]])
|
=> 54: ActiveSupport::Duration.weeks(self)
|
||||||
55: end
|
55: end
|
||||||
56: alias :week :weeks
|
56: alias :week :weeks
|
||||||
57:
|
57:
|
||||||
|
|
Loading…
Reference in a new issue