1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Fix implicit calculations with scalars and durations

Previously calculations where the scalar is first would be converted
to a duration of seconds but this causes issues with dates being
converted to times, e.g:

    Time.zone = "Beijing"           # => Asia/Shanghai
    date = Date.civil(2017, 5, 20)  # => Mon, 20 May 2017
    2 * 1.day                       # => 172800 seconds
    date + 2 * 1.day                # => Mon, 22 May 2017 00:00:00 CST +08:00

Now the `ActiveSupport::Duration::Scalar` calculation methods will try
to maintain the part structure of the duration where possible, e.g:

    Time.zone = "Beijing"           # => Asia/Shanghai
    date = Date.civil(2017, 5, 20)  # => Mon, 20 May 2017
    2 * 1.day                       # => 2 days
    date + 2 * 1.day                # => Mon, 22 May 2017

Fixes #29160, #28970.
This commit is contained in:
Andrew White 2017-05-20 16:33:09 +01:00
parent 1c2dd9cb66
commit 28938dd64c
3 changed files with 91 additions and 6 deletions

View file

@ -1,3 +1,25 @@
* Fix implicit coercion calculations with scalars and durations
Previously calculations where the scalar is first would be converted to a duration
of seconds but this causes issues with dates being converted to times, e.g:
Time.zone = "Beijing" # => Asia/Shanghai
date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017
2 * 1.day # => 172800 seconds
date + 2 * 1.day # => Mon, 22 May 2017 00:00:00 CST +08:00
Now the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain
the part structure of the duration where possible, e.g:
Time.zone = "Beijing" # => Asia/Shanghai
date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017
2 * 1.day # => 2 days
date + 2 * 1.day # => Mon, 22 May 2017
Fixes #29160, #28970.
*Andrew White*
* Add support for versioned cache entries. This enables the cache stores to recycle cache keys, greatly saving
on storage in cases with frequent churn. Works together with the separation of `#cache_key` and `#cache_version`
in Active Record and its use in Action Pack's fragment caching.

View file

@ -37,27 +37,56 @@ module ActiveSupport
end
def +(other)
calculate(:+, other)
if Duration === other
seconds = value + other.parts[:seconds]
new_parts = other.parts.merge(seconds: seconds)
new_value = value + other.value
Duration.new(new_value, new_parts)
else
calculate(:+, other)
end
end
def -(other)
calculate(:-, other)
if Duration === other
seconds = value - other.parts[:seconds]
new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h
new_parts = new_parts.merge(seconds: seconds)
new_value = value - other.value
Duration.new(new_value, new_parts)
else
calculate(:-, other)
end
end
def *(other)
calculate(:*, other)
if Duration === other
new_parts = other.parts.map { |part, other_value| [part, value * other_value] }.to_h
new_value = value * other.value
Duration.new(new_value, new_parts)
else
calculate(:*, other)
end
end
def /(other)
calculate(:/, other)
if Duration === other
new_parts = other.parts.map { |part, other_value| [part, value / other_value] }.to_h
new_value = new_parts.inject(0) { |total, (part, value)| total + value * Duration::PARTS_IN_SECONDS[part] }
Duration.new(new_value, new_parts)
else
calculate(:/, other)
end
end
private
def calculate(op, other)
if Scalar === other
Scalar.new(value.public_send(op, other.value))
elsif Duration === other
Duration.seconds(value).public_send(op, other)
elsif Numeric === other
Scalar.new(value.public_send(op, other))
else

View file

@ -337,6 +337,13 @@ class DurationTest < ActiveSupport::TestCase
assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
end
def test_scalar_plus_parts
scalar = ActiveSupport::Duration::Scalar.new(10)
assert_equal({ days: 1, seconds: 10 }, (scalar + 1.day).parts)
assert_equal({ days: -1, seconds: 10 }, (scalar + -1.day).parts)
end
def test_scalar_minus
scalar = ActiveSupport::Duration::Scalar.new(10)
@ -349,6 +356,9 @@ class DurationTest < ActiveSupport::TestCase
assert_equal 5, scalar - 5.seconds
assert_instance_of ActiveSupport::Duration, scalar - 5.seconds
assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts)
assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts)
exception = assert_raises(TypeError) do
scalar - "foo"
end
@ -356,6 +366,13 @@ class DurationTest < ActiveSupport::TestCase
assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
end
def test_scalar_minus_parts
scalar = ActiveSupport::Duration::Scalar.new(10)
assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts)
assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts)
end
def test_scalar_multiply
scalar = ActiveSupport::Duration::Scalar.new(5)
@ -375,6 +392,14 @@ class DurationTest < ActiveSupport::TestCase
assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
end
def test_scalar_multiply_parts
scalar = ActiveSupport::Duration::Scalar.new(1)
assert_equal({ days: 2 }, (scalar * 2.days).parts)
assert_equal(172800, (scalar * 2.days).value)
assert_equal({ days: -2 }, (scalar * -2.days).parts)
assert_equal(-172800, (scalar * -2.days).value)
end
def test_scalar_divide
scalar = ActiveSupport::Duration::Scalar.new(10)
@ -394,6 +419,15 @@ class DurationTest < ActiveSupport::TestCase
assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message
end
def test_scalar_divide_parts
scalar = ActiveSupport::Duration::Scalar.new(10)
assert_equal({ days: 2 }, (scalar / 5.days).parts)
assert_equal(172800, (scalar / 5.days).value)
assert_equal({ days: -2 }, (scalar / -5.days).parts)
assert_equal(-172800, (scalar / -5.days).value)
end
def test_twelve_months_equals_one_year
assert_equal 12.months, 1.year
end