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


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

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

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

12 ответов:

Вы сказали "округлить вниз", так что я не уверен, если вы на самом деле ищете круглый или пол, но вот код, чтобы сделать оба. Я думаю, что что-то вроде этого читается очень хорошо, если вы добавляете 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 был необходим только для довольно

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

t.min - ( t.min % 15)

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

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

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 могут быть легко реализованы на основе этого решения.

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

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

Я написал округление драгоценного камня для обработки такого рода случаев.

округление времени становится таким же простым, как вызов 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) и это будет работать нормально. Камень использует рациональный номера, чтобы избежать округления ошибки. Вы можете округлить до ближайшего понедельника, предоставив смещение round_to(1.week, Time.parse("2015-5-4 00:00:00 UTC")). Мы используем его в производстве.

Я написал блоге это объясняет больше. Надеюсь, вы найдете это полезным.

Я нашел очень четкий решения;

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

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

вы могли бы сделать:

Time.at(t.to_i/(15*60)*(15*60))
# this is an extension of Ryan McGeary's 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

предисловие

здесь довольно много решений, и я начал задаваться вопросом об их эффективности (ты эффективность, вероятно, не самый важный аспект в этой проблеме). Я взял немного отсюда и добавил пару своих. (N. B. Хотя OP спросил о округлении до ближайших 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 может быть оправдано только потому, что .секунды там такие классные.

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

однако, так как это на самом деле не нужно вообще и делает операцию более чем в два раза дороже, чем без него я должен сказать, что мой выбор 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.at((Time.now.to_i / 60) * 60) 
t = Time.now.to_i; Time.at(t - (t % 60))

настройка для округления / ceil бенчмарки

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

результаты для округления / ceil бенчмарки

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)

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

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

t = Time.new
truncated_t = Time.at(t.to_i - t.sec - t.min % 15 * 60)
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

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

min / 15 * 15 

только усечение мин, так что

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

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

вы можете приблизить округление в плохом направлении с

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

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

( min.to_f / 15 ).round * 15