ActiveSupport::Duration 计算间隔不正确?

ActiveSupport::Duration incorrectly calculates interval?

我有一种非常奇怪的感觉,我得到的 ActiveSupport::Duration 计算的持续时间不正确。这是我的代码的本质

require 'time'
require 'active_support/duration'
require 'active_support/gem_version'
a = Time.parse('2044-11-18 01:00:00 -0600')
b = Time.parse('2045-03-05 04:00:00 -0600')
ActiveSupport::Duration.build(b - a).inspect
ActiveSupport.gem_version

这就是我得到的

[30] pry(main)> require 'time'
=> false
[31] pry(main)> require 'active_support/duration'
=> false
[32] pry(main)> require 'active_support/gem_version'
=> false
[33] pry(main)> a = Time.parse('2044-11-18 01:00:00 -0600')
=> 2044-11-18 01:00:00 -0600
[34] pry(main)> b = Time.parse('2045-03-05 04:00:00 -0600')
=> 2045-03-05 04:00:00 -0600
[35] pry(main)> ActiveSupport::Duration.build(b - a).inspect
=> "3 months, 2 weeks, 1 day, 19 hours, 32 minutes, and 42.0 seconds"
[36] pry(main)> ActiveSupport.gem_version
=> Gem::Version.new("6.0.1")

我用 PostgreSQL 交叉检查了结果

select justify_interval('2045-03-05 04:00:00 -0600'::timestamp - '2044-11-18 01:00:00 -0600'::timestamp)

并得到 3 mons 17 days 03:00:00(或 107 天零 3 小时)。还有一个web site that gives结果与PostgreSQL一致(虽然网页上说107天是3个月,15天)。

我错过了什么吗?分和秒从何而来? Ruby/Rails有没有更好的区间计算器?

更新 distance_of_time_in_words returns 4个月!

更新 2 我最终对 Wizard 的解决方案进行了稍微修改以生成文本

  def nice_duration(seconds)
    parts = duration_in_whms(seconds)
    out = []
    I18n.with_options(scope: 'datetime.distance_in_words') do |locale|
      out.push locale.t(:x_days, count: parts[:days]) if parts.key?(:days)
      out.push locale.t(:x_hours, count: parts[:hours]) if parts.key?(:hours)
      out.push locale.t(:x_minutes, count: parts[:minutes]) if parts.key?(:minutes)
    end
    out.join ' '
  end

  private

  def duration_in_whms(seconds)
    parts_and_seconds_in_part = {:days => 86400, :hours => 3600, :minutes => 60}
    result = {}
    remainder = seconds
    parts_and_seconds_in_part.each do |k, v|
      out = (remainder / v).to_i
      result[k] = out if out.positive?
      remainder -= out * v
    end
    result.merge(seconds: remainder)
  end

显然,Action View 的本地化没有几个小时没有 about。所以我还必须在我的语言环境中添加相应的翻译

en:
  datetime:
    distance_in_words:
      x_hours:
        one:   "1 hour"
        other: "%{count} hours"

ActiveSupport::Duration 使用以下常量和算法计算它的值(我在下面添加了关于它在做什么的解释,但这里是源代码的 link)。正如您在下面看到的,SECONDS_PER_YEAR 常量是 公历中的平均秒数 (然后用于定义 SECONDS_PER_MONTH)。正因为如此,SECONDS_PER_YEAR 和 SECONDS_PER_MONTH 中的 "average definition" 会出现意外的小时、分钟和秒。它被定义为平均值,因为月和年不是标准的固定时间量。

SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR   = 3600
SECONDS_PER_DAY    = 86400
SECONDS_PER_WEEK   = 604800
SECONDS_PER_MONTH  = 2629746 # This is 1/12 of a Gregorian year
SECONDS_PER_YEAR   = 31556952 # The length of a Gregorian year = 365.2425 days

# You pass ActiveSupport::Duration the number of seconds (b-a) = 9255600.0 seconds

remainder_seconds = 9255600.0

# Figure out how many years fit into the seconds using integer division.
years = (remainder_seconds/SECONDS_PER_YEAR).to_i # => 0
# Subtract the amount of years from the remaining_seconds
remainder_seconds -= years * SECONDS_PER_YEAR # => 9255600.0

months = (remainder_seconds/SECONDS_PER_MONTH).to_i # => 3
remainder_seconds -= months * SECONDS_PER_MONTH # => 1366362.0

weeks = (remainder_seconds/SECONDS_PER_WEEK).to_i # => 2
remainder_seconds -= weeks * SECONDS_PER_WEEK # => 156762.0

days = (remainder_seconds/SECONDS_PER_DAY).to_i # => 1
remainder_seconds -= days * SECONDS_PER_DAY # => 70362.0   

hours = (remainder_seconds/SECONDS_PER_HOUR).to_i # => 19
remainder_seconds -= hours * SECONDS_PER_HOUR # => 1962.0

minutes = (remainder_seconds/SECONDS_PER_MINUTE).to_i # => 32
remainder_seconds -= minutes * SECONDS_PER_MINUTE # => 42

seconds = remainder_seconds # => 42

puts "#{years} years, #{months} months, #{weeks} weeks, #{days} days, #{hours} hours, #{minutes} minutes, #{seconds} seconds"
# 0 years, 3 months, 2 weeks, 1 days, 19 hours, 32 minutes, 42.0 seconds

为了避免您遇到的问题,我建议只用周、天、小时、分钟和秒来表示时间(基本上是除月和年之外的任何时间)。

如果不使用平均值,一个月中的秒数会很复杂,因为您需要为每个单独的月份计算 28、29、30 和 31 天。同样,对于这一年,如果您不使用平均值,则需要计算 leap/non-leap。

我不确定是否有任何 gems 可以为您执行此操作,但是我可以提供一个函数来帮助您计算以下持续时间(以天、小时、分钟和秒为单位)。

def duration_in_whms(seconds)
  parts_and_seconds_in_part = {:weeks => 604800, :days => 86400, :hours => 3600, :minutes => 60}
  result = {}
  remainder = seconds
  parts_and_seconds_in_part.each do |k, v|
    result[k] = (remainder/v).to_i
    remainder -= result[k]*v
  end
  result.merge(seconds: remainder)
end

duration_in_whms(9255600) => # {:weeks=>15, :days=>2, :hours=>3, :minutes=>0, :seconds=>0.0}