/*global window */
|
/**
|
* @license countdown.js v2.5.3 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 );
|
}
|
|
/**
|
* Applies the Timespan to the given date.
|
*
|
* @private
|
* @param {Timespan} ts
|
* @param {Date=} date
|
* @return {Date}
|
*/
|
function addToDate(ts, date) {
|
date = (date instanceof Date) || ((date !== null) && isFinite(date)) ? new Date(+date) : new Date();
|
if (!ts) {
|
return date;
|
}
|
|
// if there is a value field, use it directly
|
var value = +ts.value || 0;
|
if (value) {
|
date.setTime(date.getTime() + value);
|
return date;
|
}
|
|
value = +ts.milliseconds || 0;
|
if (value) {
|
date.setMilliseconds(date.getMilliseconds() + value);
|
}
|
|
value = +ts.seconds || 0;
|
if (value) {
|
date.setSeconds(date.getSeconds() + value);
|
}
|
|
value = +ts.minutes || 0;
|
if (value) {
|
date.setMinutes(date.getMinutes() + value);
|
}
|
|
value = +ts.hours || 0;
|
if (value) {
|
date.setHours(date.getHours() + value);
|
}
|
|
value = +ts.weeks || 0;
|
if (value) {
|
value *= DAYS_PER_WEEK;
|
}
|
|
value += +ts.days || 0;
|
if (value) {
|
date.setDate(date.getDate() + value);
|
}
|
|
value = +ts.months || 0;
|
if (value) {
|
date.setMonth(date.getMonth() + value);
|
}
|
|
value = +ts.millennia || 0;
|
if (value) {
|
value *= CENTURIES_PER_MILLENNIUM;
|
}
|
|
value += +ts.centuries || 0;
|
if (value) {
|
value *= DECADES_PER_CENTURY;
|
}
|
|
value += +ts.decades || 0;
|
if (value) {
|
value *= YEARS_PER_DECADE;
|
}
|
|
value += +ts.years || 0;
|
if (value) {
|
date.setFullYear(date.getFullYear() + value);
|
}
|
|
return date;
|
}
|
|
/**
|
* @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
|
* @type {string}
|
*/
|
var LABEL_NOW;
|
|
/**
|
* Formats a number as a string
|
*
|
* @private
|
* @param {number} value
|
* @return {string}
|
*/
|
var formatNumber;
|
|
/**
|
* @private
|
* @param {number} value
|
* @param {number} unit unit index into label list
|
* @return {string}
|
*/
|
function plurality(value, unit) {
|
return formatNumber(value)+((value === 1) ? LABELS_SINGLUAR[unit] : LABELS_PLURAL[unit]);
|
}
|
|
/**
|
* Formats the entries with singular or plural 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 sentence
|
*
|
* @param {string=} emptyLabel the string to use when no values returned
|
* @return {string}
|
*/
|
Timespan.prototype.toString = function(emptyLabel) {
|
var label = formatList(this);
|
|
var count = label.length;
|
if (!count) {
|
return emptyLabel ? ''+emptyLabel : LABEL_NOW;
|
}
|
if (count === 1) {
|
return label[0];
|
}
|
|
var last = LABEL_LAST+label.pop();
|
return label.join(LABEL_DELIM)+last;
|
};
|
|
/**
|
* Formats the Timespan as a sentence in HTML
|
*
|
* @param {string=} tag HTML tag name to wrap each value
|
* @param {string=} emptyLabel the string to use when no values returned
|
* @return {string}
|
*/
|
Timespan.prototype.toHTML = function(tag, emptyLabel) {
|
tag = tag || 'span';
|
var label = formatList(this);
|
|
var count = label.length;
|
if (!count) {
|
emptyLabel = emptyLabel || LABEL_NOW;
|
return emptyLabel ? '<'+tag+'>'+emptyLabel+'</'+tag+'>' : emptyLabel;
|
}
|
for (var i=0; i<count; i++) {
|
// wrap each unit in tag
|
label[i] = '<'+tag+'>'+label[i]+'</'+tag+'>';
|
}
|
if (count === 1) {
|
return label[0];
|
}
|
|
var last = LABEL_LAST+label.pop();
|
return label.join(LABEL_DELIM)+last;
|
};
|
|
/**
|
* Applies the Timespan to the given date
|
*
|
* @param {Date=} date the date to which the timespan is added.
|
* @return {Date}
|
*/
|
Timespan.prototype.addTo = function(date) {
|
return addToDate(this, date);
|
};
|
|
/**
|
* 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|Timespan|null|function(Timespan,number)} start the starting date
|
* @param {Date|number|Timespan|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
|
var startTS = null;
|
if ('function' === typeof start) {
|
callback = start;
|
start = null;
|
|
} else if (!(start instanceof Date)) {
|
if ((start !== null) && isFinite(start)) {
|
start = new Date(+start);
|
} else {
|
if ('object' === typeof startTS) {
|
startTS = /** @type{Timespan} */(start);
|
}
|
start = null;
|
}
|
}
|
|
// ensure end date
|
var endTS = null;
|
if ('function' === typeof end) {
|
callback = end;
|
end = null;
|
|
} else if (!(end instanceof Date)) {
|
if ((end !== null) && isFinite(end)) {
|
end = new Date(+end);
|
} else {
|
if ('object' === typeof end) {
|
endTS = /** @type{Timespan} */(end);
|
}
|
end = null;
|
}
|
}
|
|
// must wait to interpret timespans until after resolving dates
|
if (startTS) {
|
start = addToDate(startTS, end);
|
}
|
if (endTS) {
|
end = addToDate(endTS, start);
|
}
|
|
if (!start && !end) {
|
// used for unit testing
|
return new Timespan();
|
}
|
|
if (!callback) {
|
return populate(new Timespan(), /** @type{Date} */(start), /** @type{Date} */(end), /** @type{number} */(units), /** @type{number} */(max), /** @type{number} */(digits));
|
}
|
|
// base delay off units
|
var delay = getDelay(units),
|
timerId,
|
fn = function() {
|
callback(
|
populate(new Timespan(), /** @type{Date} */(start), /** @type{Date} */(end), /** @type{number} */(units), /** @type{number} */(max), /** @type{number} */(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 delimiter before the last unit (default: ' and ')
|
* @param {string=} delim a delimiter to use between all other units (default: ', ')
|
* @param {string=} empty a label to use when all units are zero (default: '')
|
* @param {function(number):string=} formatter a function which formats numbers as a string
|
*/
|
countdown.setLabels = function(singular, plural, last, delim, empty, formatter) {
|
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;
|
LABEL_NOW = ('string' === typeof empty) ? empty : LABEL_NOW;
|
formatNumber = ('function' === typeof formatter) ? formatter : formatNumber;
|
};
|
|
/**
|
* 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 = ', ';
|
LABEL_NOW = '';
|
formatNumber = function(value) { return value; };
|
};
|
|
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);
|