Wednesday, October 14, 2009

Ruby's inject(): Putting the 'Fun' in 'Functional Programming' Since ... Oh, About 4:15 This Morning

Yesterday at work, I ran across the need to convert a number of seconds into its equivalent in days, hours, minutes, and seconds. The canonical imperative way to do this is something like:
seconds = 356521
days = seconds / (24 * 60 * 60)
seconds = seconds % (24 * 60 * 60)
hours = seconds / (60 * 60)
seconds = seconds % (60 * 60)
minutes = seconds / 60
seconds = seconds % 60
N.B.: This relies on Ruby's integer division -- dividing an integer by an integer results in an integer, with any fractional remainder discarded.

My pair and I ended up implementing something very much like this, but it left a bad taste in my mouth. I mean, it works and all, but it feels kind of ... non-functional. Ya know?

So, after much fretting and sleeplessness last night ... behold:
seconds = 356521

days, hours, minutes, seconds =
[1.day, 1.hour, 1.minute, 1.second].inject([]) do |acc, unit|
quotient, seconds = seconds.divmod unit
acc << quotient
end
This version needs to be run in a Rails script/console rather than irb, because it makes use of the Rails shortcut definitions of the number of seconds in various units of time. You could easily convert to plain irb-able Ruby by replacing 1.day et al above with their numeric equivalents.

The resulting code is both (more) functional, and more quintessentially Ruby-ish. divmod and multiple assignment let us figure out the quotient and the remainder of the division in one go, and inject lets us accumulate the results and ultimately multiply assign them to their respective units.

Neato.

I'm a little bummed, though, that this version has the side effect of destroying the original contents of seconds, as well as requiring seconds to be defined outside the scope of the inject. What would be really cool would be to have a version of inject that allowed for multiple accumulators (or, really, a 'decumulator' in this case) such that all side effects could be contained within the inject.