APIs

Show:
/*global window */
/**
 * @license countdown.js v2.4.1 http://countdownjs.org
 * Copyright (c)2006-2014 Stephen M. McKamey.
 * Licensed under The MIT License.
 */
/*jshint bitwise:false */

/**
 * @public
 * @type {Object|null}
 */
var module;

/**
 * API entry
 * @public
 * @param {function(Object)|Date|number} start the starting date
 * @param {function(Object)|Date|number} end the ending date
 * @param {number} units the units to populate
 * @return {Object|number}
 */
var countdown = (/**
 * @param {Object} module CommonJS Module
 */
    function (module) {
    /*jshint smarttabs:true */

    'use strict';

    /**
     * @private
     * @const
     * @type {number}
     */
    var MILLISECONDS = 0x001;

    /**
     * @private
     * @const
     * @type {number}
     */
    var SECONDS = 0x002;

    /**
     * @private
     * @const
     * @type {number}
     */
    var MINUTES = 0x004;

    /**
     * @private
     * @const
     * @type {number}
     */
    var HOURS = 0x008;

    /**
     * @private
     * @const
     * @type {number}
     */
    var DAYS = 0x010;

    /**
     * @private
     * @const
     * @type {number}
     */
    var WEEKS = 0x020;

    /**
     * @private
     * @const
     * @type {number}
     */
    var MONTHS = 0x040;

    /**
     * @private
     * @const
     * @type {number}
     */
    var YEARS = 0x080;

    /**
     * @private
     * @const
     * @type {number}
     */
    var DECADES = 0x100;

    /**
     * @private
     * @const
     * @type {number}
     */
    var CENTURIES = 0x200;

    /**
     * @private
     * @const
     * @type {number}
     */
    var MILLENNIA = 0x400;

    /**
     * @private
     * @const
     * @type {number}
     */
    var DEFAULTS = YEARS | MONTHS | DAYS | HOURS | MINUTES | SECONDS;

    /**
     * @private
     * @const
     * @type {number}
     */
    var MILLISECONDS_PER_SECOND = 1000;

    /**
     * @private
     * @const
     * @type {number}
     */
    var SECONDS_PER_MINUTE = 60;

    /**
     * @private
     * @const
     * @type {number}
     */
    var MINUTES_PER_HOUR = 60;

    /**
     * @private
     * @const
     * @type {number}
     */
    var HOURS_PER_DAY = 24;

    /**
     * @private
     * @const
     * @type {number}
     */
    var MILLISECONDS_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;

    /**
     * @private
     * @const
     * @type {number}
     */
    var DAYS_PER_WEEK = 7;

    /**
     * @private
     * @const
     * @type {number}
     */
    var MONTHS_PER_YEAR = 12;

    /**
     * @private
     * @const
     * @type {number}
     */
    var YEARS_PER_DECADE = 10;

    /**
     * @private
     * @const
     * @type {number}
     */
    var DECADES_PER_CENTURY = 10;

    /**
     * @private
     * @const
     * @type {number}
     */
    var CENTURIES_PER_MILLENNIUM = 10;

    /**
     * @private
     * @param {number} x number
     * @return {number}
     */
    var ceil = Math.ceil;

    /**
     * @private
     * @param {number} x number
     * @return {number}
     */
    var floor = Math.floor;

    /**
     * @private
     * @param {Date} ref reference date
     * @param {number} shift number of months to shift
     * @return {number} number of days shifted
     */
    function borrowMonths(ref, shift) {
        var prevTime = ref.getTime();

        // increment month by shift
        ref.setMonth(ref.getMonth() + shift);

        // this is the trickiest since months vary in length
        return Math.round((ref.getTime() - prevTime) / MILLISECONDS_PER_DAY);
    }

    /**
     * @private
     * @param {Date} ref reference date
     * @return {number} number of days
     */
    function daysPerMonth(ref) {
        var a = ref.getTime();

        // increment month by 1
        var b = new Date(a);
        b.setMonth(ref.getMonth() + 1);

        // this is the trickiest since months vary in length
        return Math.round((b.getTime() - a) / MILLISECONDS_PER_DAY);
    }

    /**
     * @private
     * @param {Date} ref reference date
     * @return {number} number of days
     */
    function daysPerYear(ref) {
        var a = ref.getTime();

        // increment year by 1
        var b = new Date(a);
        b.setFullYear(ref.getFullYear() + 1);

        // this is the trickiest since years (periodically) vary in length
        return Math.round((b.getTime() - a) / MILLISECONDS_PER_DAY);
    }

    /**
     * @private
     * @const
     * @type {number}
     */
    var LABEL_MILLISECONDS = 0;

    /**
     * @private
     * @const
     * @type {number}
     */
    var LABEL_SECONDS = 1;

    /**
     * @private
     * @const
     * @type {number}
     */
    var LABEL_MINUTES = 2;

    /**
     * @private
     * @const
     * @type {number}
     */
    var LABEL_HOURS = 3;

    /**
     * @private
     * @const
     * @type {number}
     */
    var LABEL_DAYS = 4;

    /**
     * @private
     * @const
     * @type {number}
     */
    var LABEL_WEEKS = 5;

    /**
     * @private
     * @const
     * @type {number}
     */
    var LABEL_MONTHS = 6;

    /**
     * @private
     * @const
     * @type {number}
     */
    var LABEL_YEARS = 7;

    /**
     * @private
     * @const
     * @type {number}
     */
    var LABEL_DECADES = 8;

    /**
     * @private
     * @const
     * @type {number}
     */
    var LABEL_CENTURIES = 9;

    /**
     * @private
     * @const
     * @type {number}
     */
    var LABEL_MILLENNIA = 10;

    /**
     * @private
     * @type {Array}
     */
    var LABELS_SINGLUAR;

    /**
     * @private
     * @type {Array}
     */
    var LABELS_PLURAL;

    /**
     * @private
     * @type {string}
     */
    var LABEL_LAST;

    /**
     * @private
     * @type {string}
     */
    var LABEL_DELIM;

    /**
     * @private
     * @param {number} value
     * @param {number} unit unit index into label list
     * @return {string}
     */
    function plurality(value, unit) {
        return value + ' ' + ((value === 1) ? LABELS_SINGLUAR[unit] : LABELS_PLURAL[unit]);
    }

    /**
     * Formats the entries as English labels
     *
     * @private
     * @param {Timespan} ts
     * @return {Array}
     */
    var formatList;

    /**
     * Timespan representation of a duration of time
     *
     * @private
     * @this {Timespan}
     * @constructor
     */
    function Timespan() {
    }

    /**
     * Formats the Timespan as a sentance
     *
     * @private
     * @return {string}
     */
    Timespan.prototype.toString = function () {
        var label = formatList(this);

        var count = label.length;
        if (!count) {
            return '';
        }
        if (count > 1) {
            label[count - 1] = LABEL_LAST + label[count - 1];
        }
        return label.join(LABEL_DELIM);
    };

    /**
     * Formats the Timespan as HTML
     *
     * @private
     * @param {string} tag HTML tag name to wrap each value
     * @return {string}
     */
    Timespan.prototype.toHTML = function (tag) {
        tag = tag || 'span';
        var label = formatList(this);

        var count = label.length;
        if (!count) {
            return '';
        }
        for (var i = 0; i < count; i++) {
            // wrap each unit in tag
            label[i] = '<' + tag + '>' + label[i] + '</' + tag + '>';
        }
        if (--count) {
            label[count] = LABEL_LAST + label[count];
        }
        return label.join(LABEL_DELIM);
    };

    /**
     * Formats the entries as English labels
     *
     * @private
     * @param {Timespan} ts
     * @return {Array}
     */
    formatList = function (ts) {
        var list = [];

        var value = ts.millennia;
        if (value) {
            list.push(plurality(value, LABEL_MILLENNIA));
        }

        value = ts.centuries;
        if (value) {
            list.push(plurality(value, LABEL_CENTURIES));
        }

        value = ts.decades;
        if (value) {
            list.push(plurality(value, LABEL_DECADES));
        }

        value = ts.years;
        if (value) {
            list.push(plurality(value, LABEL_YEARS));
        }

        value = ts.months;
        if (value) {
            list.push(plurality(value, LABEL_MONTHS));
        }

        value = ts.weeks;
        if (value) {
            list.push(plurality(value, LABEL_WEEKS));
        }

        value = ts.days;
        if (value) {
            list.push(plurality(value, LABEL_DAYS));
        }

        value = ts.hours;
        if (value) {
            list.push(plurality(value, LABEL_HOURS));
        }

        value = ts.minutes;
        if (value) {
            list.push(plurality(value, LABEL_MINUTES));
        }

        value = ts.seconds;
        if (value) {
            list.push(plurality(value, LABEL_SECONDS));
        }

        value = ts.milliseconds;
        if (value) {
            list.push(plurality(value, LABEL_MILLISECONDS));
        }

        return list;
    };

    /**
     * Borrow any underflow units, carry any overflow units
     *
     * @private
     * @param {Timespan} ts
     * @param {string} toUnit
     */
    function rippleRounded(ts, toUnit) {
        switch (toUnit) {
            case 'seconds':
                if (ts.seconds !== SECONDS_PER_MINUTE || isNaN(ts.minutes)) {
                    return;
                }
                // ripple seconds up to minutes
                ts.minutes++;
                ts.seconds = 0;

            /* falls through */
            case 'minutes':
                if (ts.minutes !== MINUTES_PER_HOUR || isNaN(ts.hours)) {
                    return;
                }
                // ripple minutes up to hours
                ts.hours++;
                ts.minutes = 0;

            /* falls through */
            case 'hours':
                if (ts.hours !== HOURS_PER_DAY || isNaN(ts.days)) {
                    return;
                }
                // ripple hours up to days
                ts.days++;
                ts.hours = 0;

            /* falls through */
            case 'days':
                if (ts.days !== DAYS_PER_WEEK || isNaN(ts.weeks)) {
                    return;
                }
                // ripple days up to weeks
                ts.weeks++;
                ts.days = 0;

            /* falls through */
            case 'weeks':
                if (ts.weeks !== daysPerMonth(ts.refMonth) / DAYS_PER_WEEK || isNaN(ts.months)) {
                    return;
                }
                // ripple weeks up to months
                ts.months++;
                ts.weeks = 0;

            /* falls through */
            case 'months':
                if (ts.months !== MONTHS_PER_YEAR || isNaN(ts.years)) {
                    return;
                }
                // ripple months up to years
                ts.years++;
                ts.months = 0;

            /* falls through */
            case 'years':
                if (ts.years !== YEARS_PER_DECADE || isNaN(ts.decades)) {
                    return;
                }
                // ripple years up to decades
                ts.decades++;
                ts.years = 0;

            /* falls through */
            case 'decades':
                if (ts.decades !== DECADES_PER_CENTURY || isNaN(ts.centuries)) {
                    return;
                }
                // ripple decades up to centuries
                ts.centuries++;
                ts.decades = 0;

            /* falls through */
            case 'centuries':
                if (ts.centuries !== CENTURIES_PER_MILLENNIUM || isNaN(ts.millennia)) {
                    return;
                }
                // ripple centuries up to millennia
                ts.millennia++;
                ts.centuries = 0;
            /* falls through */
        }
    }

    /**
     * Ripple up partial units one place
     *
     * @private
     * @param {Timespan} ts timespan
     * @param {number} frac accumulated fractional value
     * @param {string} fromUnit source unit name
     * @param {string} toUnit target unit name
     * @param {number} conversion multiplier between units
     * @param {number} digits max number of decimal digits to output
     * @return {number} new fractional value
     */
    function fraction(ts, frac, fromUnit, toUnit, conversion, digits) {
        if (ts[fromUnit] >= 0) {
            frac += ts[fromUnit];
            delete ts[fromUnit];
        }

        frac /= conversion;
        if (frac + 1 <= 1) {
            // drop if below machine epsilon
            return 0;
        }

        if (ts[toUnit] >= 0) {
            // ensure does not have more than specified number of digits
            ts[toUnit] = +(ts[toUnit] + frac).toFixed(digits);
            rippleRounded(ts, toUnit);
            return 0;
        }

        return frac;
    }

    /**
     * Ripple up partial units to next existing
     *
     * @private
     * @param {Timespan} ts
     * @param {number} digits max number of decimal digits to output
     */
    function fractional(ts, digits) {
        var frac = fraction(ts, 0, 'milliseconds', 'seconds', MILLISECONDS_PER_SECOND, digits);
        if (!frac) {
            return;
        }

        frac = fraction(ts, frac, 'seconds', 'minutes', SECONDS_PER_MINUTE, digits);
        if (!frac) {
            return;
        }

        frac = fraction(ts, frac, 'minutes', 'hours', MINUTES_PER_HOUR, digits);
        if (!frac) {
            return;
        }

        frac = fraction(ts, frac, 'hours', 'days', HOURS_PER_DAY, digits);
        if (!frac) {
            return;
        }

        frac = fraction(ts, frac, 'days', 'weeks', DAYS_PER_WEEK, digits);
        if (!frac) {
            return;
        }

        frac = fraction(ts, frac, 'weeks', 'months', daysPerMonth(ts.refMonth) / DAYS_PER_WEEK, digits);
        if (!frac) {
            return;
        }

        frac = fraction(ts, frac, 'months', 'years', daysPerYear(ts.refMonth) / daysPerMonth(ts.refMonth), digits);
        if (!frac) {
            return;
        }

        frac = fraction(ts, frac, 'years', 'decades', YEARS_PER_DECADE, digits);
        if (!frac) {
            return;
        }

        frac = fraction(ts, frac, 'decades', 'centuries', DECADES_PER_CENTURY, digits);
        if (!frac) {
            return;
        }

        frac = fraction(ts, frac, 'centuries', 'millennia', CENTURIES_PER_MILLENNIUM, digits);

        // should never reach this with remaining fractional value
        if (frac) {
            throw new Error('Fractional unit overflow');
        }
    }

    /**
     * Borrow any underflow units, carry any overflow units
     *
     * @private
     * @param {Timespan} ts
     */
    function ripple(ts) {
        var x;

        if (ts.milliseconds < 0) {
            // ripple seconds down to milliseconds
            x = ceil(-ts.milliseconds / MILLISECONDS_PER_SECOND);
            ts.seconds -= x;
            ts.milliseconds += x * MILLISECONDS_PER_SECOND;

        } else if (ts.milliseconds >= MILLISECONDS_PER_SECOND) {
            // ripple milliseconds up to seconds
            ts.seconds += floor(ts.milliseconds / MILLISECONDS_PER_SECOND);
            ts.milliseconds %= MILLISECONDS_PER_SECOND;
        }

        if (ts.seconds < 0) {
            // ripple minutes down to seconds
            x = ceil(-ts.seconds / SECONDS_PER_MINUTE);
            ts.minutes -= x;
            ts.seconds += x * SECONDS_PER_MINUTE;

        } else if (ts.seconds >= SECONDS_PER_MINUTE) {
            // ripple seconds up to minutes
            ts.minutes += floor(ts.seconds / SECONDS_PER_MINUTE);
            ts.seconds %= SECONDS_PER_MINUTE;
        }

        if (ts.minutes < 0) {
            // ripple hours down to minutes
            x = ceil(-ts.minutes / MINUTES_PER_HOUR);
            ts.hours -= x;
            ts.minutes += x * MINUTES_PER_HOUR;

        } else if (ts.minutes >= MINUTES_PER_HOUR) {
            // ripple minutes up to hours
            ts.hours += floor(ts.minutes / MINUTES_PER_HOUR);
            ts.minutes %= MINUTES_PER_HOUR;
        }

        if (ts.hours < 0) {
            // ripple days down to hours
            x = ceil(-ts.hours / HOURS_PER_DAY);
            ts.days -= x;
            ts.hours += x * HOURS_PER_DAY;

        } else if (ts.hours >= HOURS_PER_DAY) {
            // ripple hours up to days
            ts.days += floor(ts.hours / HOURS_PER_DAY);
            ts.hours %= HOURS_PER_DAY;
        }

        while (ts.days < 0) {
            // NOTE: never actually seen this loop more than once

            // ripple months down to days
            ts.months--;
            ts.days += borrowMonths(ts.refMonth, 1);
        }

        // weeks is always zero here

        if (ts.days >= DAYS_PER_WEEK) {
            // ripple days up to weeks
            ts.weeks += floor(ts.days / DAYS_PER_WEEK);
            ts.days %= DAYS_PER_WEEK;
        }

        if (ts.months < 0) {
            // ripple years down to months
            x = ceil(-ts.months / MONTHS_PER_YEAR);
            ts.years -= x;
            ts.months += x * MONTHS_PER_YEAR;

        } else if (ts.months >= MONTHS_PER_YEAR) {
            // ripple months up to years
            ts.years += floor(ts.months / MONTHS_PER_YEAR);
            ts.months %= MONTHS_PER_YEAR;
        }

        // years is always non-negative here
        // decades, centuries and millennia are always zero here

        if (ts.years >= YEARS_PER_DECADE) {
            // ripple years up to decades
            ts.decades += floor(ts.years / YEARS_PER_DECADE);
            ts.years %= YEARS_PER_DECADE;

            if (ts.decades >= DECADES_PER_CENTURY) {
                // ripple decades up to centuries
                ts.centuries += floor(ts.decades / DECADES_PER_CENTURY);
                ts.decades %= DECADES_PER_CENTURY;

                if (ts.centuries >= CENTURIES_PER_MILLENNIUM) {
                    // ripple centuries up to millennia
                    ts.millennia += floor(ts.centuries / CENTURIES_PER_MILLENNIUM);
                    ts.centuries %= CENTURIES_PER_MILLENNIUM;
                }
            }
        }
    }

    /**
     * Remove any units not requested
     *
     * @private
     * @param {Timespan} ts
     * @param {number} units the units to populate
     * @param {number} max number of labels to output
     * @param {number} digits max number of decimal digits to output
     */
    function pruneUnits(ts, units, max, digits) {
        var count = 0;

        // Calc from largest unit to smallest to prevent underflow
        if (!(units & MILLENNIA) || (count >= max)) {
            // ripple millennia down to centuries
            ts.centuries += ts.millennia * CENTURIES_PER_MILLENNIUM;
            delete ts.millennia;

        } else if (ts.millennia) {
            count++;
        }

        if (!(units & CENTURIES) || (count >= max)) {
            // ripple centuries down to decades
            ts.decades += ts.centuries * DECADES_PER_CENTURY;
            delete ts.centuries;

        } else if (ts.centuries) {
            count++;
        }

        if (!(units & DECADES) || (count >= max)) {
            // ripple decades down to years
            ts.years += ts.decades * YEARS_PER_DECADE;
            delete ts.decades;

        } else if (ts.decades) {
            count++;
        }

        if (!(units & YEARS) || (count >= max)) {
            // ripple years down to months
            ts.months += ts.years * MONTHS_PER_YEAR;
            delete ts.years;

        } else if (ts.years) {
            count++;
        }

        if (!(units & MONTHS) || (count >= max)) {
            // ripple months down to days
            if (ts.months) {
                ts.days += borrowMonths(ts.refMonth, ts.months);
            }
            delete ts.months;

            if (ts.days >= DAYS_PER_WEEK) {
                // ripple day overflow back up to weeks
                ts.weeks += floor(ts.days / DAYS_PER_WEEK);
                ts.days %= DAYS_PER_WEEK;
            }

        } else if (ts.months) {
            count++;
        }

        if (!(units & WEEKS) || (count >= max)) {
            // ripple weeks down to days
            ts.days += ts.weeks * DAYS_PER_WEEK;
            delete ts.weeks;

        } else if (ts.weeks) {
            count++;
        }

        if (!(units & DAYS) || (count >= max)) {
            //ripple days down to hours
            ts.hours += ts.days * HOURS_PER_DAY;
            delete ts.days;

        } else if (ts.days) {
            count++;
        }

        if (!(units & HOURS) || (count >= max)) {
            // ripple hours down to minutes
            ts.minutes += ts.hours * MINUTES_PER_HOUR;
            delete ts.hours;

        } else if (ts.hours) {
            count++;
        }

        if (!(units & MINUTES) || (count >= max)) {
            // ripple minutes down to seconds
            ts.seconds += ts.minutes * SECONDS_PER_MINUTE;
            delete ts.minutes;

        } else if (ts.minutes) {
            count++;
        }

        if (!(units & SECONDS) || (count >= max)) {
            // ripple seconds down to milliseconds
            ts.milliseconds += ts.seconds * MILLISECONDS_PER_SECOND;
            delete ts.seconds;

        } else if (ts.seconds) {
            count++;
        }

        // nothing to ripple milliseconds down to
        // so ripple back up to smallest existing unit as a fractional value
        if (!(units & MILLISECONDS) || (count >= max)) {
            fractional(ts, digits);
        }
    }

    /**
     * Populates the Timespan object
     *
     * @private
     * @param {Timespan} ts
     * @param {?Date} start the starting date
     * @param {?Date} end the ending date
     * @param {number} units the units to populate
     * @param {number} max number of labels to output
     * @param {number} digits max number of decimal digits to output
     */
    function populate(ts, start, end, units, max, digits) {
        var now = new Date();

        ts.start = start = start || now;
        ts.end = end = end || now;
        ts.units = units;

        ts.value = end.getTime() - start.getTime();
        if (ts.value < 0) {
            // swap if reversed
            var tmp = end;
            end = start;
            start = tmp;
        }

        // reference month for determining days in month
        ts.refMonth = new Date(start.getFullYear(), start.getMonth(), 15, 12, 0, 0);
        try {
            // reset to initial deltas
            ts.millennia = 0;
            ts.centuries = 0;
            ts.decades = 0;
            ts.years = end.getFullYear() - start.getFullYear();
            ts.months = end.getMonth() - start.getMonth();
            ts.weeks = 0;
            ts.days = end.getDate() - start.getDate();
            ts.hours = end.getHours() - start.getHours();
            ts.minutes = end.getMinutes() - start.getMinutes();
            ts.seconds = end.getSeconds() - start.getSeconds();
            ts.milliseconds = end.getMilliseconds() - start.getMilliseconds();

            ripple(ts);
            pruneUnits(ts, units, max, digits);

        } finally {
            delete ts.refMonth;
        }

        return ts;
    }

    /**
     * Determine an appropriate refresh rate based upon units
     *
     * @private
     * @param {number} units the units to populate
     * @return {number} milliseconds to delay
     */
    function getDelay(units) {
        if (units & MILLISECONDS) {
            // refresh very quickly
            return MILLISECONDS_PER_SECOND / 30; //30Hz
        }

        if (units & SECONDS) {
            // refresh every second
            return MILLISECONDS_PER_SECOND; //1Hz
        }

        if (units & MINUTES) {
            // refresh every minute
            return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE;
        }

        if (units & HOURS) {
            // refresh hourly
            return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
        }

        if (units & DAYS) {
            // refresh daily
            return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY;
        }

        // refresh the rest weekly
        return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY * DAYS_PER_WEEK;
    }

    /**
     * API entry point
     *
     * @public
     * @param {Date|number|null|function(Timespan,number)} start the starting date
     * @param {Date|number|null|function(Timespan,number)} end the ending date
     * @param {number} units the units to populate
     * @param {number} max number of labels to output
     * @param {number} digits max number of decimal digits to output
     * @return {Timespan|number}
     */
    function countdown(start, end, units, max, digits) {
        var callback;

        // ensure some units or use defaults
        units = +units || DEFAULTS;
        // max must be positive
        max = (max > 0) ? max : NaN;
        // clamp digits to an integer between [0, 20]
        digits = (digits > 0) ? (digits < 20) ? Math.round(digits) : 20 : 0;

        // ensure start date
        if ('function' === typeof start) {
            callback = start;
            start = null;

        } else if (!(start instanceof Date)) {
            start = (start !== null && isFinite(start)) ? new Date(start) : null;
        }

        // ensure end date
        if ('function' === typeof end) {
            callback = end;
            end = null;

        } else if (!(end instanceof Date)) {
            end = (end !== null && isFinite(end)) ? new Date(end) : null;
        }

        if (!start && !end) {
            // used for unit testing
            return new Timespan();
        }

        if (!callback) {
            return populate(new Timespan(), /** @type{?Date} */(start), /** @type{?Date} */(end), units, max, digits);
        }

        // base delay off units
        var delay = getDelay(units),
            timerId,
            fn = function () {
                callback(
                    populate(new Timespan(), /** @type{?Date} */(start), /** @type{?Date} */(end), units, max, digits),
                    timerId
                );
            };

        fn();
        return (timerId = setInterval(fn, delay));
    }

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.MILLISECONDS = MILLISECONDS;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.SECONDS = SECONDS;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.MINUTES = MINUTES;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.HOURS = HOURS;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.DAYS = DAYS;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.WEEKS = WEEKS;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.MONTHS = MONTHS;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.YEARS = YEARS;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.DECADES = DECADES;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.CENTURIES = CENTURIES;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.MILLENNIA = MILLENNIA;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.DEFAULTS = DEFAULTS;

    /**
     * @public
     * @const
     * @type {number}
     */
    countdown.ALL = MILLENNIA | CENTURIES | DECADES | YEARS | MONTHS | WEEKS | DAYS | HOURS | MINUTES | SECONDS | MILLISECONDS;

    /**
     * Override the unit labels
     * @public
     * @param {string|Array} singular a pipe ('|') delimited list of singular unit name overrides
     * @param {string|Array} plural a pipe ('|') delimited list of plural unit name overrides
     * @param {string} last a prefix for the last unit if more than one (default: 'and ')
     * @param {string} delim a delimiter to use between units (default: ', ')
     */
    var setLabels = countdown.setLabels = function (singular, plural, last, delim) {
        singular = singular || [];
        if (singular.split) {
            singular = singular.split('|');
        }
        plural = plural || [];
        if (plural.split) {
            plural = plural.split('|');
        }

        for (var i = LABEL_MILLISECONDS; i <= LABEL_MILLENNIA; i++) {
            // override any specified units
            LABELS_SINGLUAR[i] = singular[i] || LABELS_SINGLUAR[i];
            LABELS_PLURAL[i] = plural[i] || LABELS_PLURAL[i];
        }

        LABEL_LAST = ('string' === typeof last) ? last : LABEL_LAST;
        LABEL_DELIM = ('string' === typeof delim) ? delim : LABEL_DELIM;
    };

    /**
     * Revert to the default unit labels
     * @public
     */
    var resetLabels = countdown.resetLabels = function () {
        LABELS_SINGLUAR = 'millisecond|second|minute|hour|day|week|month|year|decade|century|millennium'.split('|');
        LABELS_PLURAL = 'milliseconds|seconds|minutes|hours|days|weeks|months|years|decades|centuries|millennia'.split('|');
        LABEL_LAST = 'and ';
        LABEL_DELIM = ', ';
    };

    resetLabels();

    if (module && module.exports) {
        module.exports = countdown;

    } else if (typeof window.define === 'function' && typeof window.define.amd !== 'undefined') {
        window.define('countdown', [], function () {
            return countdown;
        });
    }

    return countdown;

})(module);