Active Support and Time

If a list was compiled of useful Ruby libraries, Active Support would be close to the top. Based on the documentation alone, it is apparent that a very large amount of time went into thinking about the roles Active Support should play and the functionality it should support. From string manipulation to internationalization, Active Support seems to simply do it all. However, even a profoundly useful library like Active Support still has a few inconsistencies with everyone’s favorite subject: Time Calculation.

Time wounds all heals

Personally, the thing that keeps me up at night, the monster in my closet, the beast waiting to grab my foot when I accidentally let it hang over the bed while sleeping, is time calculation in programming. Regardless of the task, however simple it may seem, calculating time offsets or time zone correction continues to be a tremendous pain point. Now, to demonstrate just how crazy things can be, let’s pick on Active Support.

Active Support grants a myriad of methods for even the most impatient Software Artisan. For example, if one were to ask for this instant in time, a day ago, Active Support makes it possible.

1.day.ago
# => 2015-08-29 18:44:55 -0700

Likewise, basic date addition and subtraction is just as easy.

three_days_from_now = Time.now + 3.days
#=> 2015-09-01 18:46:34 -0700

three_days_from_now + 12.hours
# => 2015-09-03 06:48:28 -0700

Great! Very simple, seemingly arbitrary time calculation has never been easier. So what could be the catch? Obviously there isn’t one, if there were then this article would have to continue and I’m sure everyone has had quite enough.

to_i is not always easy

Had enough? Too bad. The topic of to_i still has to be discussed with relation to the Time class. Ruby’s Time class exposes a method to_i which can be used to convert a Time to an Integer. This Integeris the sum of seconds passed since Epoch. Using Epoch time can be useful for some systems that want to avoid doing timezone conversion. In fact, it might be so attractive an option that a developer’s first instinct might be to save and calculate everything in Epoch time. However, this might not always be the best idea.

Active Support follows suit with allowing to_i to be called on its convenience methods. For example, 1.day.to_i calculates the number of seconds in the day.

1.day.to_i
# => 86400

And 1.week.to_i calculates the number of seconds in a week.

1.week.to_i
# => 604800

Additionally, 1.month.to_i calculates the number of seconds in a month.

1.month.to_i
# => 2592000

Wait a second, that only accounts for 30 days. If I learned anything in kindergarten, it was that the teacher really hates it when you teach yourself to whistle during nap-time. Also, I am pretty sure that not all months consist of exactly 30 days.

Bad times

Introducing this inconsistency in time calculation can lead to unintended results. For example, note the difference between subtracting time objects and subtracting to_i calculated times:

Time.now - 3.months
# => 2015-05-30 19:07:44 -0700

Time.now - 3.months.to_i
# => 2015-06-01 19:07:59 -0700

So which is it Ruby? Was 3 months ago the 1st of June or the 30th of May? The problem persists even if everything is converted to integers first:

epoch_now = Time.now.to_i
# => 1440986921

epoch_three_months_ago = epoc_now - 3.months
# => 1433210921

Time.at(epoch_three_months_ago)
# => 2015-06-01 19:08:41 -0700

Ok, but why?

So why does this happen? Why does 1.month return the same integer regardless of which month it is. Because, how could it know? A month is not meant to exist as an abstract concept, there is no canonical “month”, they are either 30 days, 31 days, 28 days, or 29 days long (I didn’t forget about you, leap year!). This is just a small demonstration of why time calculation is never as simple as it may seem. There is no silver bullet to avoid it all. Even Epoch time conversions can lead to some real bad times.