Reputation: 355814
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)
Upvotes: 73
Views: 37429
Reputation: 42463
If you're using ActiveSupport
, then you can use some of its time helpers.
t = Time.now
t.at_beginning_of_hour + (t.min - t.min % 15).minutes
Upvotes: 0
Reputation: 1936
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 = Time.now.to_i
Time.at(time - (time % 15.minutes))
Upvotes: 20
Reputation: 240044
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.
Upvotes: 137
Reputation: 9094
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.
Upvotes: 17
Reputation: 7540
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
Upvotes: 4
Reputation: 3424
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).
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)
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).
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.
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.
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)
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))
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)
Upvotes: 8
Reputation: 1710
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.
Upvotes: 22
Reputation: 3425
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)
Upvotes: 3
Reputation: 997
# 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
Upvotes: 4
Reputation: 237080
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
Upvotes: 11
Reputation: 59215
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.
Upvotes: 23
Reputation: 57374
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
Upvotes: 1