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:

123.45.round_to(5)

Time.now.round_to(20*60)

As you may have guessed from the title of this post: we can! The Rounding gem gives us round\_to, floor\_to, and ceil\_to methods on all Numeric, Time, and 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.month or 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!

brian.hempel@collectiveidea.com

Comments

  1. April 15, 2019 at 6:26 AM

    Very helpful.