Elegant (Non-Standard) Time Formatting
By Phil Matarese on December 3rd, 2007
I want to show you a neat snippet of Ruby code that I wrote for specialized time formatting. I was outputting some audio information as XML for a major online music store, and I needed to encode durations in a non-standard format. This unnecessarily, ambiguously named store likes their durations to look like PT00M00S, so a duration of 12345 seconds becomes PT205M45S. Egad, how non-standard! The code to do this might get a little kludgy – not complicated, just kludgy. But secretly, I'm getting giddy because I know I'll get to write some custom code. And after all, writing code is the reason we got into this programming stuff to begin with.
But, before I show you this code, you have to promise not to use it until you've read this entire post. There are a few goodies in here that are useful for so much more than time formatting. I wouldn't want you to just cut and paste this code without understanding the magic behind it – that would be bad for all of us.
>> duration = 12345
>> format('PT%02dM%02dS', *duration.divmod(60))
=> "PT205M45S"Look, Ruby!
My favorite thing about this one line of code is the fact that it's just one line of code. I like some other things about it, too, so let's go take a look. C'mon.
Kernel.format
The first thing to notice is format, which is really Kernel.format. Basically, if you're not formatting your numbers with this you're a jerk. What's the matter with you anyway? FYI, this is an alias for sprintf which comes from C. In our example, format is being used to guarantee that minutes and seconds will be padded with zeros. The %02d means 'render an integer, taking up at least 2 spaces.' An integer like 205 will be unaltered, but 0 becomes 00, and 5 becomes 05, and so forth. If you're not familiar with this stuff, you should definitely check it out – jerk.
Numeric.divmod
As we continue on our tour, if you look to the right, you'll see a function called divmod. This function is great for two reasons: firstly its specifically useful without cluttering the language and secondly because it returns 2 values. Returns 2 values? How does that work? It's pretty simple. See. (Or try it yourself with irb.)
>> quot, rem = 22.divmod(7) # return as 2 variables
>> quot
=> 3
>> rem
=> 1
>> q_r = 22.divmod(7) # return as an array to 1 variable
=> [3, 1]Check out Pickaxe's Parallel Assignment section for more on this. But, this still leaves us with catching the results in 2 variables or 1 array. And, if we try to pass the result of divmod into format, we get a conversion error.
Splat
Here we are with our single array, yet format wants an argument list of 2 arguments (because our pattern has 2 wildcards). Uh-oh. I'm starting to feel like my quest for elegance will fall short. If only there was a way to 'unpack' the elements of an array and use them as an argument list... Well, this is Ruby, where all of your wildest dreams come true! Of course there's an 'unpack' operator, and better yet, it's called 'splat'. OMG! HOLY CRAP! WOW! (Don't act so surprised, I already showed you the code, you knew this was coming. But, OMG, nonetheless.) Simply apply the * operator to any array, and splat it's a list of arguments. What could be easier – it's just like magic. So, that explains that funky asterisk in the middle of the code, and brings our tour to an end.
Let Me Sum Up
Now, for your viewing pleasure, feast your eyes on these examples and behold the happiness of pretty code. I had to change duration to d so the ugly one would fit.
# ugly code, bad (no format, divmod, or splat)
"PT#{'0' if(d/60).floor<10}#{(d/60).floor}M#{'0' if d%60<10}#{d%60}S"
# pretty code, happy happy happy
format('PT%02dM%02dS', *d.divmod(60))
Mmm, Ruby
In case you didn't notice, time formatting is just a red herring, a clever ploy to hook you with practical applicability so I could assault you with some Ruby elegance. But now you've seen, now you know, now you're beginning to understand. You've seem a glimpse of its beauty, so sit back, take a sip of Ruby, and write some elegant one-liners.



Alex
Sun February 10, 2008 at 06:52
Now show us if you can extend that elegant 1-liner to PT00H00M00S!
Phil Matarese
Mon February 11, 2008 at 15:19
So, I wrote a quick method called divmodd that can split the number by multiple divisors:
# this is excellent for time-formatting
# and, it's a 1-liner without the definition stuff
class Fixnum
def divmodd(*nums)
nums.inject([self]){|a,n| a.shift.divmod(n) + a}
end
end
#Now I can do it!
format('PT%02dH%02dM%02dS', *d.divmodd(60, 60))
Phil Matarese
Mon March 17, 2008 at 16:24
'PT%02dM%02dS' % duration.divmod(60)