将 BigDecimal 小时添加到 DateTime 是错误的 1 秒
Adding BigDecimal hours to DateTime is wrong by 1 second
更新到 ActiveSupport 6 时会发生这种情况
start_time = DateTime.now.beginning_of_day
start_time + BigDecimal(2).hours #=> Wed, 11 Sep 2019 01:59:59 +0000
奇怪的是,这与 Time
配合得很好
start_time = Time.now.beginning_of_day
start_time + BigDecimal(2).hours #=> 2019-09-11 02:00:00 +0000
谁能解释为什么?
差一秒,到毫秒。为什么不使用 2.hours
而不是 BigDecimal(2).hours
?
最终,它归结为 ActiveSupport 内部执行的某些数学计算中的浮点错误。
请注意,使用 Rational 而不是 BigDecimal 是可行的:
DateTime.now.beginning_of_day + Rational(2, 1).hours
# => Mon, 02 Dec 2019 02:00:00 -0800
Time.now.beginning_of_day + Rational(2, 1).hours
# => 2019-12-02 02:00:00 -0800
这是来自Time/DateTime/ActiveSupport的相关代码:
class DateTime
def since(seconds)
self + Rational(seconds, 86400)
end
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.since(self)
else
plus_without_duration(other)
end
end
end
class Time
def since(seconds)
self + seconds
rescue
to_datetime.since(seconds)
end
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.since(self)
else
plus_without_duration(other)
end
end
def advance(options)
unless options[:weeks].nil?
options[:weeks], partial_weeks = options[:weeks].divmod(1)
options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
end
unless options[:days].nil?
options[:days], partial_days = options[:days].divmod(1)
options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
end
d = to_date.gregorian.advance(options)
time_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
seconds_to_advance = \
options.fetch(:seconds, 0) +
options.fetch(:minutes, 0) * 60 +
options.fetch(:hours, 0) * 3600
if seconds_to_advance.zero?
time_advanced_by_date
else
time_advanced_by_date.since(seconds_to_advance)
end
end
end
class ActiveSupport::Duration
def since(time = ::Time.current)
sum(1, time)
end
def sum(sign, time = ::Time.current)
parts.inject(time) do |t, (type, number)|
if t.acts_like?(:time) || t.acts_like?(:date)
if type == :seconds
t.since(sign * number)
elsif type == :minutes
t.since(sign * number * 60)
elsif type == :hours
t.since(sign * number * 3600)
else
t.advance(type => sign * number)
end
else
raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
end
end
end
end
您的情况发生在 t.since(sign * number * 3600)
行,number
是 BigDecimal(2)
,DateTime.since 是 Rational(seconds, 86400)
。所以使用 DateTime 时的整个表达式是 Rational(1 * BigDecimal(2) * 3600, 86400)
.
由于将 BigDecimal 用作 Rational 的参数,结果根本不是有理数:
Rational(1 * BigDecimal(2) * 3600, 86400)
# => 0.83333333333333333e-1 # Since there's no obvious way to coerce a BigDecimal into a Rational, this returns a BigDecimal
Rational(1 * 2 * 3600, 86400)
# => (1/12) # A rational, as expected
这个值使它回到时间#advance。以下是它的计算结果:
options[:days], partial_days = options[:days].divmod(1)
# => [0.0, 0.83333333333333333e-1] # 0 days, 2 hours
options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
# => 0.1999999999999999992e1 # juuuust under 2 hours.
最后,0.1999999999999999992e1 * 3600 = 7199.9999999999999712,当它最终转换回 time/datetime.
Time 不会发生这种情况,因为 Time 永远不需要将持续时间的值传递给 Rational。
我认为这不应被视为错误,因为如果您传递的是 BigDecimal,那么您应该期望代码如何处理您的数据:作为带有小数部分的数字,而不是作为比率。也就是说,当您使用 BigDecimals 时,您会面临浮点错误。
更新到 ActiveSupport 6 时会发生这种情况
start_time = DateTime.now.beginning_of_day
start_time + BigDecimal(2).hours #=> Wed, 11 Sep 2019 01:59:59 +0000
奇怪的是,这与 Time
配合得很好start_time = Time.now.beginning_of_day
start_time + BigDecimal(2).hours #=> 2019-09-11 02:00:00 +0000
谁能解释为什么?
差一秒,到毫秒。为什么不使用 2.hours
而不是 BigDecimal(2).hours
?
最终,它归结为 ActiveSupport 内部执行的某些数学计算中的浮点错误。
请注意,使用 Rational 而不是 BigDecimal 是可行的:
DateTime.now.beginning_of_day + Rational(2, 1).hours
# => Mon, 02 Dec 2019 02:00:00 -0800
Time.now.beginning_of_day + Rational(2, 1).hours
# => 2019-12-02 02:00:00 -0800
这是来自Time/DateTime/ActiveSupport的相关代码:
class DateTime
def since(seconds)
self + Rational(seconds, 86400)
end
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.since(self)
else
plus_without_duration(other)
end
end
end
class Time
def since(seconds)
self + seconds
rescue
to_datetime.since(seconds)
end
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.since(self)
else
plus_without_duration(other)
end
end
def advance(options)
unless options[:weeks].nil?
options[:weeks], partial_weeks = options[:weeks].divmod(1)
options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
end
unless options[:days].nil?
options[:days], partial_days = options[:days].divmod(1)
options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
end
d = to_date.gregorian.advance(options)
time_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
seconds_to_advance = \
options.fetch(:seconds, 0) +
options.fetch(:minutes, 0) * 60 +
options.fetch(:hours, 0) * 3600
if seconds_to_advance.zero?
time_advanced_by_date
else
time_advanced_by_date.since(seconds_to_advance)
end
end
end
class ActiveSupport::Duration
def since(time = ::Time.current)
sum(1, time)
end
def sum(sign, time = ::Time.current)
parts.inject(time) do |t, (type, number)|
if t.acts_like?(:time) || t.acts_like?(:date)
if type == :seconds
t.since(sign * number)
elsif type == :minutes
t.since(sign * number * 60)
elsif type == :hours
t.since(sign * number * 3600)
else
t.advance(type => sign * number)
end
else
raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
end
end
end
end
您的情况发生在 t.since(sign * number * 3600)
行,number
是 BigDecimal(2)
,DateTime.since 是 Rational(seconds, 86400)
。所以使用 DateTime 时的整个表达式是 Rational(1 * BigDecimal(2) * 3600, 86400)
.
由于将 BigDecimal 用作 Rational 的参数,结果根本不是有理数:
Rational(1 * BigDecimal(2) * 3600, 86400)
# => 0.83333333333333333e-1 # Since there's no obvious way to coerce a BigDecimal into a Rational, this returns a BigDecimal
Rational(1 * 2 * 3600, 86400)
# => (1/12) # A rational, as expected
这个值使它回到时间#advance。以下是它的计算结果:
options[:days], partial_days = options[:days].divmod(1)
# => [0.0, 0.83333333333333333e-1] # 0 days, 2 hours
options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
# => 0.1999999999999999992e1 # juuuust under 2 hours.
最后,0.1999999999999999992e1 * 3600 = 7199.9999999999999712,当它最终转换回 time/datetime.
Time 不会发生这种情况,因为 Time 永远不需要将持续时间的值传递给 Rational。
我认为这不应被视为错误,因为如果您传递的是 BigDecimal,那么您应该期望代码如何处理您的数据:作为带有小数部分的数字,而不是作为比率。也就是说,当您使用 BigDecimals 时,您会面临浮点错误。