Optimizing Ruby Time Creation with Time.local
I have never been one to remember the number of days in a given month, and for a particular project I am working on I needed just that. I did a quick Google search to see if someone has already written a snippet of ruby code that will take care of this for me, and I found one here and here. The solution is as follows:
def DaysIn(MonthNum) (Date.new(Time.now.year,12,31).to_date<<(12-MonthNum)).day end
At first glance it seems like a fine solution — I’m getting 30s and 31s in all of the appropriate places — but then I do something that precludes this from being a reasonable solution: I benchmark it. Oh what a slippery slope it was, but I’ll get into that later. For those just looking for a ruby snippet for days in a month, I wrote an alternative solution that completes in about 2% of the time it takes the solution above:
def days_in_month(month, year)
feb = lambda do
if year % 400 == 0 || year % 4 == 0 && year % 100 != 0 then 29 else 28 end
end
day_array = [31, feb.call, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
day_array[month-1]
end
So what exactly caused the first solution to take so long? A quick benchmark exposes the culprit as Date.new. I have been doing a lot of date and time manipulations lately (naturalinputs.com is a natural language date and time parser), so I begin to worry. I decided to expand my investigation to Ruby’s Time Class, which I prefer to use over Date whenever possible; by passing appropriate strftime parameters I can accomplish most, if not all, of my needs with this class alone. In the past whenever I needed a particular date I would use Time.parse(”yyyymmdd”), which I always thought was working like a champ. Turns out it was working a lot less champ-like than I would have preferred. Two other flavors of Date creation that some of you may be using are Date.parse and Date.strptime. Then there is the DateTime class, with creation methods DateTime.civil, DateTime.parse, and DateTime.strptime. Every one of these methods is slow, horribly slow. But there is a better way, enter Time.local. Here is the benchmark (I created October 10, 2007 ten-thousand times in each case):
Date.strptime: 5.110000 0.370000 5.480000 ( 5.487217) Date.parse: 5.920000 0.420000 6.340000 ( 6.329733) Date.new: 1.010000 0.060000 1.070000 ( 1.068610) DateTime.civil: 2.740000 0.240000 2.980000 ( 2.990096) DateTime.parse: 8.310000 0.610000 8.920000 ( 8.916347) DateTime.strptime: 7.350000 0.700000 8.050000 ( 8.046435) Time.parse: 2.570000 0.110000 2.680000 ( 2.677252) Time.local: 0.110000 0.010000 0.120000 ( 0.118345)
Yeah, crazy right? Yes, the parameter format of Time.local is annoying, but even using a wrapper to Time.local that takes the same parameters as Time.parse, such as:
class Time
class << self
# Instantiate a Time object with Time.quick_parse("YYYYMMDD")
def quick_parse(date_str)
local(date_str[0..3], RFC2822_MONTH_NAME[date_str[4..5].to_i - 1], date_str[6..7])
end
end
end
The benchmark comes out as:
Time.parse: 2.570000 0.110000 2.680000 ( 2.677252) Time.local: 0.110000 0.010000 0.120000 ( 0.118345) Time.quick_parse: 0.160000 0.010000 0.170000 ( 0.159252)
And now I can still use the “yyyymmdd” format that I have sprinkled throughout my app! Congratulations Time.local, you saved the day.
1 Comment