How to round a time down to the nearest 15 minutes in Ruby?

Ruby

Ruby Problem Overview


Is there an easy way to round a Time down to the nearest 15 minutes?

This is what I'm currently doing. Is there an easier way to do it?

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

Ruby Solutions


Solution 1 - Ruby

You said "round down", so I'm not sure if you're actually looking for the round or the floor, but here's the code to do both. I think something like this reads really well if you add round_off and floor methods to the Time class. The added benefit is that you can more easily round by any time partition.

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

Note: ActiveSupport was only necessary for the pretty 15.minutes argument. If you don't want that dependency, use 15 * 60 instead.

Solution 2 - Ruby

I am not very familiar with the syntax of ruby but you can round down to the nearest 15 minutes using modulo. (i.e. x - (x modulo 15)). I would guess the syntax would be something like

t.min - ( t.min % 15)

This will make your set of possible values 0, 15, 30, and 45. Assuming 0 <= t.min <= 59.

Solution 3 - Ruby

I thought I would post another solution that provides rounding up and down to the nearest number of seconds given. Oh, and this does not change the time zone like some of the other solutions.

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 was used in the examples for the x.minutes feature. You can use 15 * 60 instead.

Methods floor and ceil can be easily implemented based on this solution.

Solution 4 - Ruby

I found a very readable solution;

This will round your time to the last rounded 15 minutes. You can change the 15.minutes to every timescale possible.

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

Solution 5 - Ruby

I wrote the Rounding gem to handle these sort of cases.

Rounding down a time becomes as simple as calling floor_to on the time. Rounding up and rounding to the nearest are also supported (ceil_to and 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

The time zone of the original time is preserved (UTC in this case). You don't need ActiveSupport loaded—you can write floor_to(15*60) and it will work fine. The gem uses Rational numbers to avoid rounding errors. You can round to the nearest Monday by providing an offset round_to(1.week, Time.parse("2015-5-4 00:00:00 UTC")). We use it in production.

I wrote a blog post that explains more. Hope you find it helpful.

Solution 6 - Ruby

Since Ruby allows arithmetic (in seconds) on Times, you can just do this:

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

Solution 7 - Ruby

#Preface

There's quite a few solutions here and I began to wonder about their efficiency (thou efficiency is probably not the most important aspect in this problem). I took some from here and threw in a couple of my own. (N.B. though the OP asked about rounding down to closest 15 minutes, I've done my comparitions and samples with just 1 minute / 60s for the sake of more simple samples).

#Setup

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

#Results

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)

#Analysing the results

What to make of it? Well thing's might work faster or slower on your hardware, so don't take my computers word for it. As you can see, another thing is that, unless we do these operations on the scale of millions operations it's not going to make much difference which method you use as far as processing power goes (though, do note that for instance most cloud computing solutions provide very little processing power and thus the millions might be hundreds or tens of thousands in such environments).

Slowest, but probably the most readable solution

In that sense using clearly the slowest of them t = Time.now; t - (t.to_i % 60).seconds can be justified just because the .seconds is so cool in there.

Not so slow and almost as readable solution

However, since it's actually not needed at all and makes the operation over twice as expensive as without it I have to say that my choice is the t = Time.now; t - (t.to_i % 60). In my opinion it is fast enough and million times more readable than any of the other solutions presented here. That is why I think it's the best solution for you casual flooring needs, though it is a significantly slower than the three other ones.

Awkward and not particularly slow or fast

The most voted solution on this page Time.at((Time.now.to_f / 60).floor * 60) is the slowest of all solutions on this page (before this answer) and significantly slower than the top 2 solutions. Using floats just to be able to floor the decimals away also seems very illogical. For the rounding part that would be ok, but rounding down sounds like "flooring" to me. If anything the counterpart for it might be rounding up or "ceiling", which would be somethig like t = Time.now; t - (60 - t.to_i % 60) % 60 or Time.at((Time.now.to_f / 60).ceil * 60). The the double modulo that the to_i solution needs here is a bit nasty looking, so even though it is significantly faster, here I'd prefer the ceil method. (Benchmarks appended at the very end of this post)

For those in need for speed

The tied (the differences are so insignificant that you can't really declare a winner) top two performers in the test where two to_i variants that use slightly different combination of operations and then convert integer back to Time object. If your in a hurry these are the ones you should use:

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

###Setup for rounding up / ceil benchmarks

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

###Results for rounding up / ceil benchmarks

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)

Solution 8 - Ruby

You could do:

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

Solution 9 - Ruby

# 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

Solution 10 - Ruby

Chuck's answer, while elegant, will run you into trouble if you try to compare values derived in this way; the usecs are not zeroed out.

Shalmanese' answer takes care of that, or Chuck's can be modified as:

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

Solution 11 - Ruby

Ryan McGeary's solution didn't work for time zones that were not on the half hour. For example, Kathmandu is +5:45, so rounding to 30.minutes was getting the wrong results. This should work:

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

And tests:

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

Solution 12 - Ruby

Your current evaluation using

min / 15 * 15 

is only truncating the min, so

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

Which is not 'rounding'.

You can approximate rounding in a bad-way with

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

Or, using internals:

( min.to_f / 15 ).round * 15

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionreadonlyView Question on Stackoverflow
Solution 1 - RubyRyan McGearyView Answer on Stackoverflow
Solution 2 - RubycmsjrView Answer on Stackoverflow
Solution 3 - RubyJarno LambergView Answer on Stackoverflow
Solution 4 - RubyjewilmeerView Answer on Stackoverflow
Solution 5 - RubyBrian HempelView Answer on Stackoverflow
Solution 6 - RubyChuckView Answer on Stackoverflow
Solution 7 - RubyTimoView Answer on Stackoverflow
Solution 8 - RubyShalmaneseView Answer on Stackoverflow
Solution 9 - RubyDavid LowenfelsView Answer on Stackoverflow
Solution 10 - RubydenishaskinView Answer on Stackoverflow
Solution 11 - RubyAlex GhiculescuView Answer on Stackoverflow
Solution 12 - RubyKent FredricView Answer on Stackoverflow