Better Ruby Rounding
Black and White Clock is licensed under Creative Commons Zero
Ruby’s built-in rounding methods are great for the basic cases like rounding to a whole number or rounding to a certain decimal place.
123.45.floor # => 123 123.45.ceil # => 124 123.45.round # => 123 # Round to the nearest tenth. 123.45.round(1) # => 123.5 # Round to the nearest hundred. 123.45.round(-2) # => 100
But what happens when you need to round a time? Or you need to round to the nearest 5? I need to do this sort of thing on occasion. Ruby’s rounding methods can’t help us. We could write some ugly manual code…
# Round to the nearest multiple of five. (123.45 / 5).round * 5 # => 5 # Round to the nearest twenty minutes. time = Time.now step = 20*60 Time.at((time.to_r / step).round * step) # => 2015-04-30 14:20:00 -0400
…but that’s gross. It would be great if we could write:
As you may have guessed from the title of this post: we can! The Rounding gem gives us
ceil\_to methods on all
DateTime objects. Add it to your Gemfile and run
bundle install, or just install the gem directly with
gem install rounding. Now, we can do all sorts of useful things:
require “rounding” # Round to the nearest five. 123.45.round_to(5) # => 125 # Round down to the previous 2.5. 123.45.floor_to(2.5) # => 122.5 # Round up to the next 50. 123.45.ceil_to(50) # => 150 # Round to the nearest 20 minutes. Time.now.round_to(20*60) # => 2015-04-30 14:20:00 -0400 # Round down to the previous 15 minute mark. Time.now.floor_to(15*60) # => 2015-04-30 14:15:00 -0400 # Round up to the next quarter-day. Time.now.ceil_to(6*60*60) # => 2015-04-30 18:00:00 -0400
If we have ActiveSupport loaded, we can use its duration sugar to make the code even clearer:
Time.now.round_to(20.minutes) Time.now.floor_to(15.minutes) Time.now.ceil_to(6.hours)
The Rounding gem also has another trick. What if we want to round to the nearest odd number? Or what if we want to round forward to the next Monday morning midnight? If we provide a center for rounding, then it just works.
# Round to 1, 3, 5, 7, 9 etc. # Use a step of 2, starting at 1. 32.1.round_to(2, 1) # => 33 # Round to the next Monday morning midnight, in UTC. # Use a step of 1 week, starting at any Monday. monday = Time.utc(2014, 4, 27, 0, 0) Time.now.utc.ceil_to(1.week, monday) # => 2015-05-03 00:00:00 UTC
The Rounding gem is designed to be nice to use. When rounding times, it uses Rational numbers internally to avoid floating-point rounding errors. Time zones are honored and preserved.
The Rounding gem is also battle-tested. We use it determine schedules in critical parts of Dead Man’s Snitch, our periodic process monitoring service.
The only caveat is that you can only round by fix-width steps. Months and years are not fixed-width, so if you try to round to
1.year you may not get the results you expect. As the author of the gem, I’m open to adding support for month and year rounding. Chime in with your use case if you think you need month or year rounding.
I wrote the Rounding gem to make special rounding cases—particularly time rounding—clean and reliable. I hope you find it useful as well. Happy rounding!