Thirty days hath September,
April, June and November.
All the rest have thirty-one,
Excepting February alone,
Which hath twenty-eight days clear
And twenty-nine in each leap year.

I’m back at work today, and coincidentally found an interesting bug in our Ruby on Rails application. A test that had always worked before was suddenly failing. I happened to discover it first, but it wasn’t working for my colleagues either when they tried it. To add to the puzzle, it had worked perfectly on the continuous integration machine when the last change was checked in yesterday.

The failing component generates a report aggregating data for the last three months. In order to do this, it needs to know when the last three months begin. The obvious way is to use Rails’s time helper methods:

months = [ 
  Time.now.beginning_of_month,
  1.month.ago.beginning_of_month,
  2.months.ago.beginning_of_month
]

so that’s what I had done when writing the report. And, indeed, it works—up until the 31st of the month. Like a werewolf on the full moon, though, the bug rears its ugly head on the 31st day of any month:

[Mon May 01 00:00:00 BST 2006,
Mon May 01 00:00:00 BST 2006,
Sat Apr 01 00:00:00 BST 2006]

Why? Let’s see:

>> 1.month
=> 2592000
>> 30.days
=> 2592000

When you do 1.month.ago in Rails, you don’t get one month ago. You get thirty days ago. Maybe that’s close enough for most of the time, but it handles the edge cases remarkably poorly.

The solution in this case is quite simple:

months = [Time.now.beginning_of_month]
2.times do
  months << (months.last - 15.days).beginning_of_month
end

By subtracting fifteen days from the beginning of any month, we end up in the middle of the previous month. Taking the beginning of that month then gives us a reliable answer that works 365 days a year.

It’s obvious and easy once you realise, but it’s easy to overlook. So be careful when using month.ago in Rails. It doesn’t do what you might expect.