Как округлить время до ближайших 15 минут в Ruby? - PullRequest
61 голосов
/ 16 января 2009

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

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

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

Ответы [ 12 ]

124 голосов
/ 16 января 2009

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

21 голосов
/ 16 января 2009

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

t.min - ( t.min % 15)

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

20 голосов
/ 15 ноября 2010

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

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 вместо.

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

9 голосов
/ 16 января 2009

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

t = Time.new
rounded_t = t-t.sec-t.min%15*60
8 голосов
/ 07 мая 2015

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

Округление времени становится таким же простым, как вызов 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 голосов
/ 15 июня 2013

Я нашел очень удобочитаемое решение;

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

Time.at(Time.now.to_i - (Time.now.to_i % 15.minutes))
5 голосов
/ 16 января 2009

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

Time.at(t.to_i/(15*60)*(15*60))
4 голосов
/ 14 апреля 2010
# 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
3 голосов
/ 11 июня 2014

Введение

Здесь довольно много решений, и я начал задумываться об их эффективности (эффективность, вероятно, не самый важный аспект в этой проблеме). Я взял немного отсюда и добавил пару своих. (Нет, хотя ОП спрашивал о округлении до ближайших 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) - самое медленное из всех решений на этой странице (до этого ответа) и значительно медленнее, чем два верхних решения. Использование поплавков только для того, чтобы уложить десятичные дроби, также кажется очень нелогичным. Для части округления это было бы нормально, но округление для меня звучит как "настил". Во всяком случае, аналогом для этого может быть округление вверх или «потолок», что-то вроде 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)
2 голосов
/ 30 сентября 2014

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

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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...