Reading Code: Drew Gourley's Countdown Script
A core step of growing as a programmer is reading code. I claim to be a fan of reading other peoples code, but I never seem to do it. Yesterday the excellent @DrewGourley tweeted about some code he deployed on the (gorgeous) new MahaMusicFestival.com.
The code behind the coundown timer on the new @mahafestival website is custom from the ground up and is absolutely gorgeous. Just saying...
— Gunnar Gourley (@DrewGourley) April 16, 2012
Since I enjoy Drew's work (the Oxide site has lots of really neat stuff), but I've never really read any of it, I thought I'd give some code reading a whirl. As a little experiment I've decided to transcribe my thoughts through the entire reading process; I'm typing it as I read.
This may turn out scattered, but it will show how I read code, and hopefully poke a little hole of insight into Drew's code.
Note: The code on Maha has since changed, and probably will in the future. For reference I've saved off a copy of the code I read, so you can follow along if you'd like. Please recognize that this is not my work, it's all Drew and the Oxide crews' - maha.js.
Also, I went back and cleaned up some grammar and spelling errors, but for the most part this is verbatim what I wrote as I was reading.
The Code
So the product I'm going to poke around in is the countdown timer on MahaMusicFestival.com - going there to check it out I find this cool piece.
And, the slick part is this great number animation, which I have captured for posterity here.
So let's find the source for it. Popping open view-source I quickly find a script tag for http://mahamusicfestival.com/wp-content/themes/maha2012/js/maha.js in the header. Since every other script tag there is for something generic (jQuery, Modernizr, etc) I'm guessing this is it. Also it looks like the site is based on Wordpress - it's amazing how flexible themes can be.
Clicking over to that source I see it's a collection of JavaScript bits for the site, wrapped in a jQuery ready event.
jQuery(document).ready(function($) {
That's a nice, and very correct way of using jQuery for page load. I'm usually lazy and just do this:
$(function(){
I should probably up my game a bit there, huh? Just in case another library has taken over $ onload of my scripts.
Scrolling down I see some relevant functions that are all prefixed with Oxide, like OxideSlider.
function OxideSlider(id) {
My first thought here was "namespace pollution!", but then I realized that this is all encapsulated in that ready closure, so it's good to go.
Lot's of stuff I could look at here, but I'm on a mission: coundown code. Browsing down I find my target. Lines 87-208 seem to be what I'm after. I'll deal with this in chunks now.
Declarations
First thing up is the variable declarations. Nice and orderly.
var $dayspan = $('#days span'),
$hourspan = $('#hours span'),
$minutespan = $('#minutes span'),
$secondspan = $('#seconds span'),
d, h, m, s, go = 0,
di = 0;
I see a few things here. First, I like the notation of $dayspan - prefixing variables with $ is a good way to visually identify jQuery objects, sort of a Hungarian Notation for jQuery.
Second, I'm a bit skittish of d, h, m, s, go and di. I can instictively identify what the first four are (day, hour, minute, second) but the last two elude me. I'm sure this will become clear as I read on, but since it feels weird to me I guess that means I like my variable names a bit more descriptive. Learning about myself through others!
Core Function
Up next appears to be the core function of which this all revolves around, OxideCountdown. And it's a doosie, so I'm going to try and chunk it out in pieces.
First up are some variable assingments. Interestingly d, h, m, s are not integers, that's what I was expecting. This function might hold some surprises for me!
function OxideCountdown() {
d = new Array(), h = new Array(), m = new Array(), s = new Array(), go = 0;
So, this next chunk seems to be pulling the current values out of the spans, one at a time.
$dayspan.each(function(i, e) {
n = parseFloat($(e).text());
d.push(n);
go = go + n;
});
$hourspan.each(function(i, e) {
n = parseFloat($(e).text());
h.push(n);
go = go + n;
});
$minutespan.each(function(i, e) {
n = parseFloat($(e).text());
m.push(n);
go = go + n;
});
$secondspan.each(function(i, e) {
n = parseFloat($(e).text());
s.push(n);
go = go + n;
});
Interesting use of parseFloat here, as I would have expected parseInt. Could this be related to the bugs in interpretation by parseInt?
So, it works it's way through each time span, grabbing the value of the each digit in order and pushing it onto the corresponding array, so that each array would read out the same as the visual version, i.e. today d = [ 1, 1, 6 ];
Also, it's interesting to see that go seems to be an accumulator, but I've not figured out what for yet. It's just incrementing with all the values of all the digits, which doesn't seem useful to me. i.e today go would equal 8 (1+1+6) after the $dayspan.each. My curiosity is piqued!
Okay, on to the next chunk!
Math!
So here's the next block, I warn you it's a bit hairy!
di = d.length;
s[1]--;
if (s[1] < 0) {
s[1] = 9;
s[0] = s[0] - 1;
}
if (s[0] < 0) {
s[0] = 5;
m[1] = m[1] - 1;
}
if (m[1] < 0) {
m[1] = 9;
m[0] = m[0] - 1;
}
if (m[0] < 0) {
m[0] = 5;
h[1] = h[1] - 1;
}
if (h[1] < 0) {
h[1] = 9;
h[0] = h[0] - 1;
}
if (h[0] < 0) {
h[0] = 5;
d[di - 1] = d[di - 1] - 1;
}
So I'm now seeing that di is how many digit's will be displayed in the days area? I'm lost now, I was operating on the assumption that each section has a fixed number of elements in it. Let's jump to the HTML real quick...
47
Seconds
57
Minutes
17
Hours
116
Days
So yeah, I suppose it's possible that days might not have all those spans in it, but that seems true of all of the blocks if we are going down that route. Hopefully reading more will shed some light. But first, let's double back and look at the math a piece at a time.
s[1]--;
Looks like this will be called once a second, and will decrement the right-most seconds column by one second (the reverse indexes here could get confusing).
It seems that the rest of this section is just balancing the carry for that subtraction. Here's the seconds code, but I stuck in comments where I heard them in my head. The pattern repeats up until we hit tens of hours.
if (s[1] < 0) {
// Underrun on seconds, take away one from tens of seconds!
s[1] = 9;
s[0] = s[0] - 1;
}
if (s[0] < 0) {
// Underrun on tens of seconds, take away a minute!
s[0] = 5;
m[1] = m[1] - 1;
}
Now that we have an underrun on tens of hours we have to tinker with days. So there is what di is for! Since the number of spans for days is (apparently) flexible, we are going to use the length of the days array to access the largest element. Makes you wish JavaScript was allowed negative indicies on arrays (d[-1]), but that would of course break [] object notation for the key "-1", which as we all know is super useful ;-p
The loop that follows just does the same carry algorithm on all the days blocks.
if (h[0] < 0) {
h[0] = 5;
d[di - 1] = d[di - 1] - 1;
}
while (di--) {
if (d[di] < 0) {
d[di] = 9;
if (di > 0) {
d[di - 1] = d[di - 1] - 1;
}
}
}
Bug?
At this point I noticed something I thought might be a bug. In the tens of hours decrement block I saw that h[0] is set to "5". I'm not sure that is possible, since there are only 24 hours in the day, so it seems that at midnight a tick would mess up the count. I'm set up a quick test script for this, since I don't want to stay awake until midnight watching :-)
// Preset the arrays in an expected failure condition
var d = [1,1,6],
h = [0,0],
m = [0,0],
s = [0,0],
di = 0;
// Code from http://mahamusicfestival.com/wp-content/themes/maha2012/js/maha.js?ver=3.3.1 ( 2012-04-16 18:44 Central)
di = d.length;
s[1]--;
if (s[1] < 0) {
s[1] = 9;
s[0] = s[0] - 1;
}
if (s[0] < 0) {
s[0] = 5;
m[1] = m[1] - 1;
}
if (m[1] < 0) {
m[1] = 9;
m[0] = m[0] - 1;
}
if (m[0] < 0) {
m[0] = 5;
h[1] = h[1] - 1;
}
if (h[1] < 0) {
h[1] = 9;
h[0] = h[0] - 1;
}
if (h[0] < 0) {
h[0] = 5;
d[di - 1] = d[di - 1] - 1;
}
console.log( "d:", d );
console.log( "h:", h );
console.log( "m:", m );
console.log( "s:", s );
After running it, I got bad output, sure enough.
$ node test.js
d: [ 1, 1, 5 ]
h: [ 5, 9 ]
m: [ 5, 9 ]
s: [ 5, 9 ]
Corrected code would be this, which actually reaches backwards into h[1] to fix it.
if (h[0] < 0) {
h[0] = 2;
h[1] = 3;
d[di - 1] = d[di - 1] - 1;
}
Update: I shot Drew an email and he fixed it. He even gave me credit!
Moving On
Ah ha! Now I finally see what go is for!
if (go == 0) {
clearInterval(timer);
It's an accumulator to know when to stop this whole thing! Evidently there is a timeout set on timer, which we haven't seen yet.
When all the digits in the countdown are 0, the accumulator is also 0. Tricky!
I would usually implement it as a boolean, but I like it this way. It's trickier, but it cleans up a lot of potential if statements. Neat!
Now we are back to animate in all the various numbers, if they need animating. Again we iterate over the digits in each span, and then apply a really cool custom animation on them if they have changed.
The animation takes advantage of hidden overflow in the CSS to animate the digit down and thus out of view, then moves it up above the visible area and animates in back down and into view again. It's a great combination of CSS and jQuery for a nice effect.
Since the bulk of this is stuff we've seen, I'll just let show the code (though I am curious what's going on in that "Messy" comment, crazy chaining!
} else {
$dayspan.each(function(i, e) {
if (parseFloat($(e).text()) !== d[i]) {
$(e).stop(true).animate({
'top': '200px'
}, 100, function() {
$(e).css({
'top': '-200px'
}).text(d[i]).animate({
'top': '0'
}, 100);
});
}
});
//Messy: if ( $($dayspan[0]).text() == 0 ) { test1 = $($dayspan[0]).parent().parent().width(); $($dayspan[0]).fadeOut(function() { test2 = $(this).parent().parent().width(); $(this).parent().parent().css({width:test1+'px'}).animate({width:test2+'px'}); }); }
$hourspan.each(function(i, e) {
if (parseFloat($(e).text()) !== h[i]) {
$(e).stop(true).animate({
'top': '200px'
}, 100, function() {
$(e).css({
'top': '-200px'
}).text(h[i]).animate({
'top': '0'
}, 100);
});
}
});
$minutespan.each(function(i, e) {
if (parseFloat($(e).text()) !== m[i]) {
$(e).stop(true).animate({
'top': '200px'
}, 100, function() {
$(e).css({
'top': '-200px'
}).text(m[i]).animate({
'top': '0'
}, 100);
});
}
});
$secondspan.each(function(i, e) {
if (parseFloat($(e).text()) !== s[i]) {
$(e).stop(true).animate({
'top': '200px'
}, 100, function() {
$(e).css({
'top': '-200px'
}).text(s[i]).animate({
'top': '0'
}, 100);
});
}
});
}
}
Lastly we declare and kick off the timer for the count down. I would probably have moved this up, it's a bit weird to see timer on line 151 before it's declared. It's a little thing, and just my preference.
var timer = setInterval(OxideCountdown, 1000);
Conclusions
So that was neat!
Since I found a bug Drew has fixed that and went back and tweaked the code a bit more. Most notably he pulled out and boiled down the countdown animation into this function.
function OxideNumberDrop(elem, array, index) {
$(elem).stop(true).animate({
'top': '200px'
}, 100, function() {
$(elem).css({
'top': '-200px'
}).text(array[index]).animate({
'top': '0'
}, 100);
});
}
This is a really great effect, I can't emphasize how cool it looks.
Reading another persons code exposed a few things about my own coding style that I wasn't conscious of. What surprised me most is that I wasn't bothered by any formatting issues. I'm rather opinionated on formatting, for instance I claim to despise cuddled else's (no newline between a closing curly brace and an else), but Drew does that and I breezed right over them.
It was also interesting to see a totally different approach to this problem. I implemented a countdown in the middle of last year, but it was day only and not JavaScript. My natural approach would be handling time pieces as plain integers, so it was cool and kind of mind bending to see this approach of partitioning digits into arrays.
I think I will try to make a habit of this process, and I like the brain dump while reading, I think it helped it stick better to express it in writing while fresh in my mind.
Does anyone have any suggestions for what to read next? A favorite piece of code? Preferably something I can do in one sitting.