summaryrefslogtreecommitdiff
path: root/js/lib
diff options
context:
space:
mode:
authorJonas Smedegaard <dr@jones.dk>2017-03-26 18:37:53 +0200
committerJonas Smedegaard <dr@jones.dk>2017-03-26 18:37:53 +0200
commit7fed15b0fdbc32500cd601be5de275e6ee5abb20 (patch)
treee8b87744ebf7d3760135006573d0712a0cbf484b /js/lib
parentedb317740843720c9bb979e030c12dc346b3c6a5 (diff)
Add countdown.js.
Diffstat (limited to 'js/lib')
-rw-r--r--js/lib/countdown.js1366
1 files changed, 1366 insertions, 0 deletions
diff --git a/js/lib/countdown.js b/js/lib/countdown.js
new file mode 100644
index 0000000..c03a032
--- /dev/null
+++ b/js/lib/countdown.js
@@ -0,0 +1,1366 @@
+/*global window */
+/**
+ * @license countdown.js v2.6.0 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 & unit as a string
+ *
+ * @param {number} value
+ * @param {number} unit
+ * @return {string}
+ */
+ var formatter;
+
+ /**
+ * 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(formatter(value, LABEL_MILLENNIA));
+ }
+
+ value = ts.centuries;
+ if (value) {
+ list.push(formatter(value, LABEL_CENTURIES));
+ }
+
+ value = ts.decades;
+ if (value) {
+ list.push(formatter(value, LABEL_DECADES));
+ }
+
+ value = ts.years;
+ if (value) {
+ list.push(formatter(value, LABEL_YEARS));
+ }
+
+ value = ts.months;
+ if (value) {
+ list.push(formatter(value, LABEL_MONTHS));
+ }
+
+ value = ts.weeks;
+ if (value) {
+ list.push(formatter(value, LABEL_WEEKS));
+ }
+
+ value = ts.days;
+ if (value) {
+ list.push(formatter(value, LABEL_DAYS));
+ }
+
+ value = ts.hours;
+ if (value) {
+ list.push(formatter(value, LABEL_HOURS));
+ }
+
+ value = ts.minutes;
+ if (value) {
+ list.push(formatter(value, LABEL_MINUTES));
+ }
+
+ value = ts.seconds;
+ if (value) {
+ list.push(formatter(value, LABEL_SECONDS));
+ }
+
+ value = ts.milliseconds;
+ if (value) {
+ list.push(formatter(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;
+
+ /**
+ * Customize the format settings.
+ * @public
+ * @param {Object} format settings object
+ */
+ var setFormat = countdown.setFormat = function(format) {
+ if (!format) { return; }
+
+ if ('singular' in format || 'plural' in format) {
+ var singular = format.singular || [];
+ if (singular.split) {
+ singular = singular.split('|');
+ }
+ var plural = format.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];
+ }
+ }
+
+ if ('string' === typeof format.last) {
+ LABEL_LAST = format.last;
+ }
+ if ('string' === typeof format.delim) {
+ LABEL_DELIM = format.delim;
+ }
+ if ('string' === typeof format.empty) {
+ LABEL_NOW = format.empty;
+ }
+ if ('function' === typeof format.formatNumber) {
+ formatNumber = format.formatNumber;
+ }
+ if ('function' === typeof format.formatter) {
+ formatter = format.formatter;
+ }
+ };
+
+ /**
+ * Revert to the default formatting.
+ * @public
+ */
+ var resetFormat = countdown.resetFormat = 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; };
+ formatter = plurality;
+ };
+
+ /**
+ * 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=} formatNumber a function which formats numbers as a string
+ * @param {function(number,number):string=} formatter a function which formats a number/unit pair as a string
+ * @deprecated since version 2.6.0
+ */
+ countdown.setLabels = function(singular, plural, last, delim, empty, formatNumber, formatter) {
+ setFormat({
+ singular: singular,
+ plural: plural,
+ last: last,
+ delim: delim,
+ empty: empty,
+ formatNumber: formatNumber,
+ formatter: formatter
+ });
+ };
+
+ /**
+ * Revert to the default unit labels.
+ * @public
+ * @deprecated since version 2.6.0
+ */
+ countdown.resetLabels = resetFormat;
+
+ resetFormat();
+
+ 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);