Как округлить время до ближайших 15 минут в Ruby?

Есть ли простой способ округлить время до ближайших 15 минут?

Это то, что я сейчас делаю. Есть ли более простой способ сделать это?

t = Time.new
rounded_t = Time.local(t.year, t.month, t.day, t.hour, t.min/15*15)

Ответ 1

Вы сказали "закруглить", поэтому я не уверен, действительно ли вы ищете раунд или пол, но здесь код для обоих. Я думаю, что что-то вроде этого хорошо читается, если вы добавите методы round_off и floor в класс Time. Дополнительным преимуществом является то, что вы можете более легко округлить любой раздел времени.

require 'active_support/core_ext/numeric' # from gem 'activesupport'

class Time
  # Time#round already exists with different meaning in Ruby 1.9
  def round_off(seconds = 60)
    Time.at((self.to_f / seconds).round * seconds).utc
  end

  def floor(seconds = 60)
    Time.at((self.to_f / seconds).floor * seconds).utc
  end
end

t = Time.now                    # => Thu Jan 15 21:26:36 -0500 2009
t.round_off(15.minutes)         # => Thu Jan 15 21:30:00 -0500 2009
t.floor(15.minutes)             # => Thu Jan 15 21:15:00 -0500 2009

Примечание. ActiveSupport был необходим только для симпатичного аргумента 15.minutes. Если вы не хотите эту зависимость, используйте 15 * 60 вместо этого.

Ответ 2

Я не очень хорошо знаком с синтаксисом ruby, но вы можете округлить до ближайших 15 минут, используя modulo. (т.е. x - (x по модулю 15)). Я бы предположил, что синтаксис будет похож на

t.min - ( t.min % 15)

Это сделает ваш набор возможных значений 0, 15, 30 и 45. Предполагая, что 0 <= t.min <= 59.

Ответ 3

Я думал, что отправлю еще одно решение, которое обеспечивает округление вверх и вниз до ближайшего количества секунд. О, и это не изменяет часовой пояс, как некоторые из других решений.

class Time
  def round(sec=1)
    down = self - (self.to_i % sec)
    up = down + sec

    difference_down = self - down
    difference_up = up - self

    if (difference_down < difference_up)
      return down
    else
      return up
    end
  end
end

t = Time.now                             # => Mon Nov 15 10:18:29 +0200 2010
t.round(15.minutes)                      # => Mon Nov 15 10:15:00 +0200 2010
t.round(20.minutes)                      # => Mon Nov 15 10:20:00 +0200 2010
t.round(60.minutes)                      # => Mon Nov 15 10:00:00 +0200 2010

ActiveSupport использовался в примерах для функции x.minutes. Вместо этого вы можете использовать 15 * 60.

Способы floor и ceil могут быть легко реализованы на основе этого решения.

Ответ 4

Так как Ruby разрешает арифметику (в секундах) в Times, вы можете просто сделать это:

t = Time.new
rounded_t = t-t.sec-t.min%15*60

Ответ 5

Я нашел очень читаемое решение,

Это будет округлять ваше время до последних округленных 15 минут. Вы можете изменить значение 15.minutes на каждый возможный временной интервал.

Time.at(Time.now.to_i - (Time.now.to_i % 15.minutes))

Ответ 6

Я написал Rounding gem для обработки таких случаев.

Округление времени становится таким же простым, как вызов floor_to по времени. Поддерживаются округление и округление до ближайшего (ceil_to и round_to).

require "rounding"

Time.current.floor_to(15.minutes) # => Thu, 07 May 2015 16:45:00 UTC +00:00
Time.current.ceil_to(15.minutes)  # => Thu, 07 May 2015 17:00:00 UTC +00:00
Time.current.round_to(15.minutes) # => Thu, 07 May 2015 16:45:00 UTC +00:00

Временной пояс исходного времени сохраняется (UTC в этом случае). Вам не нужно загружать ActiveSupport - вы можете написать floor_to(15*60), и он будет работать нормально. Драгоценный камень использует Rational, чтобы избежать ошибок округления. Вы можете округлить до ближайшего понедельника, предоставив смещение round_to(1.week, Time.parse("2015-5-4 00:00:00 UTC")). Мы используем его в производстве.

Я написал сообщение , которое объясняет больше. Надеюсь, вам понравится.

Ответ 7

Вы можете сделать:

Time.at(t.to_i/(15*60)*(15*60))

Ответ 8

# this is an extension of Ryan McGeary solution, specifically for Rails.
# Note the use of utc, which is necessary to keep Rails time zone stuff happy.
# put this in config/initializers/time_extensions

require 'rubygems'
require 'active_support'

module TimeExtensions
  %w[ round floor ceil ].each do |_method|
    define_method _method do |*args|
      seconds = args.first || 60
      Time.at((self.to_f / seconds).send(_method) * seconds).utc
    end
  end
end

Time.send :include, TimeExtensions

Ответ 9

Введение

Здесь довольно много решений, и я начал задумываться об их эффективности (эффективность - это, вероятно, не самый важный аспект этой проблемы). Я взял немного отсюда и бросил пару своих. (Н.Б., хотя ОП попросил округлить до ближайших 15 минут, я сделал свои сравнения и образцы всего за 1 минуту /60 секунд для более простых образцов).

Настройка

Benchmark.bmbm do |x|
  x.report("to_f, /, floor, * and Time.at") { 1_000_000.times { Time.at((Time.now.to_f / 60).floor * 60) } }
  x.report("to_i, /, * and Time.at") { 1_000_000.times { Time.at((Time.now.to_i / 60) * 60) } }
  x.report("to_i, %, - and Time.at") { 1_000_000.times { t = Time.now.to_i; Time.at(t - (t % 60)) } }
  x.report("to_i, %, seconds and -") { 1_000_000.times { t = Time.now; t - (t.to_i % 60).seconds } }
  x.report("to_i, % and -") { 1_000_000.times { t = Time.now; t - (t.to_i % 60) } }
end

Результаты

Rehearsal -----------------------------------------------------------------
to_f, /, floor, * and Time.at   4.380000   0.010000   4.390000 (  4.393235)
to_i, /, * and Time.at          3.270000   0.010000   3.280000 (  3.277615)
to_i, %, - and Time.at          3.220000   0.020000   3.240000 (  3.233176)
to_i, %, seconds and -         10.860000   0.020000  10.880000 ( 10.893103)
to_i, % and -                   4.450000   0.010000   4.460000 (  4.460001)
------------------------------------------------------- total: 26.250000sec

                                    user     system      total        real
to_f, /, floor, * and Time.at   4.400000   0.020000   4.420000 (  4.419075)
to_i, /, * and Time.at          3.220000   0.000000   3.220000 (  3.226546)
to_i, %, - and Time.at          3.270000   0.020000   3.290000 (  3.275769)
to_i, %, seconds and -         10.910000   0.010000  10.920000 ( 10.924287)
to_i, % and -                   4.500000   0.010000   4.510000 (  4.513809)

Анализ результатов

Что с этим делать? Хорошо, что на вашем оборудовании может работать быстрее или медленнее, поэтому не принимайте за него слово для компьютеров. Как вы можете видеть, еще одна вещь заключается в том, что, если мы не выполним эти операции в масштабе миллионов операций, не будет иметь большого значения какой метод вы используете в отношении мощности обработки (хотя обратите внимание, что, например, большинство решений для облачных вычислений обеспечивают очень мало вычислительной мощности, и, следовательно, миллионы могут составлять сотни или десятки тысяч в таких средах).

Самое медленное, но, вероятно, наиболее читаемое решение

В этом смысле, используя ясно, что самый медленный из них t = Time.now; t - (t.to_i % 60).seconds может быть оправдан только потому, что .seconds там так круто.

Не так медленно и почти как читаемое решение

Однако, поскольку он фактически не нужен вообще и делает операцию в два раза дороже, чем без нее, я должен сказать, что мой выбор - это t = Time.now; t - (t.to_i % 60). По-моему, это достаточно быстро и в миллион раз больше читаемых, чем любые другие представленные здесь решения. Именно поэтому я считаю, что это лучшее решение для ваших случайных напольных покрытий, хотя оно значительно медленнее, чем три других.

Неловко и не особенно медленно или быстро

Самое голосованое решение на этой странице Time.at((Time.now.to_f / 60).floor * 60) - это самый медленный из всех решений на этой странице (до этого ответа) и значительно медленнее, чем лучшие 2 решения. Использование поплавков просто для того, чтобы укладывать десятичные знаки, также кажется очень нелогичным. Для округлой части, которая была бы в порядке, но округление звучит как "пол" для меня. Если что-то похожее на него может быть округлено или "потолок", что бы было похоже на t = Time.now; t - (60 - t.to_i % 60) % 60 или Time.at((Time.now.to_f / 60).ceil * 60). Двойной модуль, в котором нуждается решение to_i, выглядит немного неприятно, поэтому, хотя он значительно быстрее, здесь я бы предпочел метод ceil. (Контрольные показатели прилагаются в самом конце этого сообщения)

Для тех, кто нуждается в скорости

Связанный (различия настолько незначительны, что вы не можете действительно объявить победителя) топ-два исполнителя в тесте, где два варианта to_i, которые используют немного другую комбинацию операций, а затем преобразуют целочисленное число обратно в объект Time. Если вы спешите, это те, которые вы должны использовать:

Time.at((Time.now.to_i / 60) * 60) 
t = Time.now.to_i; Time.at(t - (t % 60))

Настройка для округления/сравнения уровней

Benchmark.bmbm do |x|
  x.report("to_f, /, ceil, * and Time.at") { 1_000_000.times { Time.at((Time.now.to_f / 60).ceil * 60) } }
  x.report("to_i, %, -, %, + and Time.at") { 1_000_000.times { t = Time.now; t + (60 - t.to_i % 60) % 60 } }
end

Результаты для тестов округления/сравнения уровня

Rehearsal ----------------------------------------------------------------
to_f, /, ceil, * and Time.at   4.410000   0.040000   4.450000 (  4.446320)
to_i, %, -, %, + and Time.at   3.910000   0.020000   3.930000 (  3.939048)
------------------------------------------------------- total: 8.380000sec

                                   user     system      total        real
to_f, /, ceil, * and Time.at   4.420000   0.030000   4.450000 (  4.454173)
to_i, %, -, %, + and Time.at   3.860000   0.010000   3.870000 (  3.884866)

Ответ 10

Ответ Чака, в то время как изящный, вызовет у вас проблемы, если вы попытаетесь сравнить значения, полученные таким образом; usecs не обнуляются.

Ответ Shalmanese позаботится об этом, или Чак может быть изменен как:

t = Time.new
truncated_t = Time.at(t.to_i - t.sec - t.min % 15 * 60)

Ответ 11

Решение Райана МакГери не работало для часовых поясов, которые не были на полчаса. Например, Катманду составляет +5: 45, поэтому округление до 30.minutes приводит к неправильным результатам. Это должно работать:

class ActiveSupport::TimeWithZone
  def floor(seconds = 60)
    return self if seconds.zero?
    Time.at(((self - self.utc_offset).to_f / seconds).floor * seconds).in_time_zone + self.utc_offset
  end

  def ceil(seconds = 60)
    return self if seconds.zero?
    Time.at(((self - self.utc_offset).to_f / seconds).ceil * seconds).in_time_zone + self.utc_offset
  end

  # returns whichever (out of #floor and #ceil) is closer to the current time
  def closest(seconds = 60)
    down, up = floor(seconds), ceil(seconds)
    ((self - down).abs > (self - up).abs) ? up : down
  end
end

И тесты:

class TimeHelperTest < ActionDispatch::IntegrationTest
  test "floor" do
    t = Time.now.change(min: 14)
    assert_equal Time.now.change(min: 10), t.floor(5.minutes)
    assert_equal Time.now.change(min: 0), t.floor(30.minutes)
  end

  test "ceil" do
    t = Time.now.change(min: 16)
    assert_equal Time.now.change(min: 20), t.ceil(5.minutes)
    assert_equal Time.now.change(min: 30), t.ceil(30.minutes)
  end

  test "closest" do
    t = Time.now.change(min: 18)
    assert_equal Time.now.change(min: 20), t.closest(5.minutes)
    assert_equal Time.now.change(min: 30), t.closest(30.minutes)
    assert_equal Time.now.change(min: 0), t.closest(60.minutes)
  end

  test "works in time zones that are off the half hour" do
    Time.zone = "Kathmandu"
#2.1.0p0 :028 > Time.zone.now
# => Tue, 30 Sep 2014 06:46:12 NPT +05:45 # doing .round(30.minutes) here would give 06:45 under the old method

    t = Time.zone.now.change(min: 30)
    assert_equal Time.zone.now.change(min: 30), t.closest(30.minutes)

    t = Time.zone.now.change(min: 0)
    assert_equal Time.zone.now.change(min: 0), t.closest(30.minutes)
  end
end

Ответ 12

Ваша текущая оценка с помощью

min / 15 * 15 

только усекает min, поэтому

15 => 15
16 => 15
..
29 => 15
30 => 30 

Это не "округление".

Вы можете аппроксимировать округление с ошибкой

(( min + 7.5 ) / 15).to_i * 15 

Или, используя внутренние элементы:

( min.to_f / 15 ).round * 15