Yesterday, I posted a tip about iterating through time with Ruby and Rails/ActiveSupport.
Originally, I posted this code. This code is actually not valid in Ruby 1.9 anymore.
# Steps day-by-day from today until 5 days from now Range.new(Time.now, 5.days.from_now).step(1.day) do |time| puts time end
This code looks amazingly compact and incredibly idiomatic … right?
Well, not so “fast”. Take a look at the API documentation for Range#step:
Iterates over rng, passing each nth element to the block. If the range contains numbers, n is added for each iteration. Otherwise step invokes succ to iterate through range elements.
A Time instance is not a number, so Range#step is stuck calling Time#succ 86400 times (the number of seconds in a day) between steps. To add insult to injury, Time#succ creates a new Time instance: we create 86399 useless objects between steps!
While it’s often reasonable to sacrifice a bit of performance for idiomatic, readable code, in this case, the price is certainly too high.
Lesson learned: Peel back at least one level of abstraction when writing any code that looks a bit magical.
In the end, I recommend this code:
now = DateTime.now # Second argument defaults to 1, but shown for clarity now.step(now + 5, 1) do |time| puts time end
DateTime has its own #step instance method which can be much more specific about its implementation than Range#step which must operate correctly on nearly every type of enumerable object.
Final thought: Sometimes abstractions and duck typing are great things, but be especially careful with methods that accept any object that implements a very limited public API (in this case, Range with objects that respond to #succ). In many cases, using only this limited API results in very bad inefficiencies.