ActiveSupport :: Duration неправильно вычисляет интервал? - PullRequest
1 голос
/ 24 января 2020

У меня очень странное ощущение, что я получаю неверную продолжительность, рассчитанную 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 часа). Также существует веб-сайт , который дает результат, соответствующий PostgreSQL (хотя на веб-странице указано, что 107 дней - это 3 месяца, а 15 дни).

Я пропустил что-то? Откуда идут минуты и секунды? Есть ли лучший калькулятор интервалов для Ruby / Rails?

Обновление distance_of_time_in_words возвращает 4 месяца!

Обновление 2 У меня получилось немного модифицированное решение мастера, обернутое для получения текста

  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 не имеет часов без о . Поэтому мне также пришлось добавить соответствующий перевод в мои локали

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

1 Ответ

2 голосов
/ 24 января 2020

ActiveSupport :: Duration вычисляет свое значение, используя следующие константы и алгоритм (я добавил объяснение того, что он делает ниже, но здесь есть ссылка на источник). Как вы можете видеть ниже, константа SECONDS_PER_YEAR - это среднее число секунд в григорианском календаре (которое затем используется для определения SECONDS_PER_MONTH). Именно из-за этого «среднего определения» SECONDS_PER_YEAR и SECONDS_PER_MONTH вы получаете неожиданные часы, минуты и секунды. Он определяется как среднее, потому что месяц и год не являются стандартным фиксированным периодом времени.

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 день для каждого отдельного месяца. Точно так же за год вам нужно будет учитывать скачок / не-скачок, если вы не используете среднее значение.

Я не уверен ни в каких драгоценных камнях вокруг, которые делают это для вас, однако я могу предоставить функция, которая может помочь вам рассчитать продолжительность в днях, часах, минутах и ​​секундах ниже.

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}
...