The perils of a bad date

Posted by on August 13, 2020

As you would expect from small business accounting software like FreeAgent, we deal with a lot of dates and times. Ruby on Rails has some really useful helper methods — but there are also a few unexpected quirks in the way that different date and time classes interact. One of those quirks sadly caught me out recently.

Picture the scene

I’ve written tests, the CI pipeline has passed and my pull request has been accepted. The only thing standing between my code and production is some pre-production testing (checks done by another engineer to make sure everything looks good before the code goes live). I’m feeling good!

Then I get a Slack message: Hey Lorna, why am I getting “TypeError: can’t convert Date into an exact number” when I run your code?

Say what? Ruby isn’t supposed to throw type errors! All I’m doing is subtracting one date from another…

To understand what happened, I’ll need to take you on a brief tour of Ruby and Rails and the different ways in which they treat the concept of duration. The problem isn’t unique to Ruby, or Rails, of course — you’ve probably bumped into something similar if you’ve ever tried to work with dates and times in any programming language. And let’s not even mention timezones…

Dates in Ruby

If you’re working with dates in Ruby you have several classes to choose from: Date, Time, and DateTime. Each has advantages and limitations, but one significant difference is the units in which they operate.

Date 
Units of days with no concept of hours, minutes or seconds  

Time 
Units of seconds, counting since the start of the Unix Epoch

DateTime
Units of days, but does include the concept of hours, minutes and seconds
Calendar-based, and understands the concept of calendar reform

So far, so good. I had chosen to use Date and was subtracting one date from another in order to calculate a number of months:

def self.rounded_up_number_of_months_between(first_date, second_date)
      ((second_date - first_date).to_f / 365 * 12).ceil
end

Dates in Rails

Rails ActiveSupport includes a number of helper methods to make working with duration more intuitive. You can use phrases like 1.day or 3.days.ago or 7.days.from_now, or even concepts like beginning_of_quarter.

What caught me out was the difference between date + 3.days and 3.days.from_now.  

3.days returns an ActiveSupport::Duration which is compatible with Date, Time & DateTime

And that’s where it gets interesting.  


Adding or subtracting an ActiveSupport::Duration will return the same type of object.  

Date.today - 1.day => Date

DateTime.now + 1.minute => DateTime

Time.now - 1.minute => Time

The exception is where you are modifying a Date by a number of hours, minutes or seconds when it will be coerced into ActiveSupport::TimeWithZone (which implements the same interface as Ruby Time).

Date.today - 1.minute => ActiveSupport::TimeWithZone

3.days.from_now returns an ActiveSupport::TimeWithZone which behaves like Time, rather than Date.  

This is where my error was coming from.

My colleague had passed in 90.days.from_now instead of Date.today + 90.days in order to set the end date for the calculation, and Date.today to set the start date. 

(end_date - start_date) was thus (ActiveSupport::TimeWithZone - Date)

The reason for the error is that Date and Time use different units: days for the Date and seconds for the Time.  As a result, Ruby doesn’t know how to interpret what you’ve passed in, and throws a TypeError.  In the same way, you can’t subtract a Time from a DateTime.

Date - Time => TypeError: expected numeric

DateTime - Time => TypeError: expected numeric

Time - Date => TypeError: can't convert Date into an exact number

This isn’t the only problem with mixing classes either.  Because of the differing units between DateTime, Date and Time, you might end up with a perfectly valid calculation but an answer which makes no sense in the context of your code.

Date - DateTime => Rational number of days

Time - DateTime => Float number of seconds

TL;DR  

In modern Ruby, Time and DateTime are largely indistinguishable.  So much so that Ruby offers a helpful rubric for choosing which class to use. In contrast Date and Time are significantly different, and ActiveSupport adds another layer of complexity.

References and Further Reading

https://www.rubyguides.com/2015/12/ruby-time/

https://medium.com/@muesingb/whats-the-difference-between-time-and-datetime-in-ruby-fa3cc844c9d7

https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time

https://blog.dnsimple.com/2018/03/elapsed-time-with-ruby-the-right-way/

Tagged

About the author…

Lorna is an Engineer on the Banking Team. When she's not writing code she can be found on her bicycle or experimenting with various crafts.

See all posts by Lorna

Leave a reply

Your email address will not be published. Required fields are marked *