Active Support and Time
30 Aug 2015If 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 Integer
is 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.