mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-09 12:30:15 +02:00
1155 lines
23 KiB
JavaScript
1155 lines
23 KiB
JavaScript
/*global window */
|
|
/**
|
|
* @license countdown.js v2.3.4 http://countdownjs.org
|
|
* Copyright (c)2006-2012 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.setUTCMonth( ref.getUTCMonth() + 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.setUTCMonth( ref.getUTCMonth() + 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.setUTCFullYear( ref.getUTCFullYear() + 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
|
|
* @const
|
|
* @type {Array}
|
|
*/
|
|
var LABELS_SINGLUAR;
|
|
|
|
/**
|
|
* @private
|
|
* @const
|
|
* @type {Array}
|
|
*/
|
|
var LABELS_PLURAL;
|
|
|
|
/**
|
|
* @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 '';
|
|
}
|
|
return label.join(' ');
|
|
};
|
|
|
|
/**
|
|
* 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] = 'and '+label[count];
|
|
}
|
|
return label.join(', ');
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
ts.start = start;
|
|
ts.end = end;
|
|
ts.units = units;
|
|
|
|
ts.value = end.getTime() - start.getTime();
|
|
if (ts.value < 0) {
|
|
// swap if reversed
|
|
var temp = end;
|
|
end = start;
|
|
start = temp;
|
|
}
|
|
|
|
// reference month for determining days in month
|
|
ts.refMonth = new Date(start.getFullYear(), start.getMonth(), 15);
|
|
try {
|
|
// reset to initial deltas
|
|
ts.millennia = 0;
|
|
ts.centuries = 0;
|
|
ts.decades = 0;
|
|
ts.years = end.getUTCFullYear() - start.getUTCFullYear();
|
|
ts.months = end.getUTCMonth() - start.getUTCMonth();
|
|
ts.weeks = 0;
|
|
ts.days = end.getUTCDate() - start.getUTCDate();
|
|
ts.hours = end.getUTCHours() - start.getUTCHours();
|
|
ts.minutes = end.getUTCMinutes() - start.getUTCMinutes();
|
|
ts.seconds = end.getUTCSeconds() - start.getUTCSeconds();
|
|
ts.milliseconds = end.getUTCMilliseconds() - start.getUTCMilliseconds();
|
|
|
|
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||new Date()), /** @type{Date} */(end||new Date()), units, max, digits);
|
|
}
|
|
|
|
// base delay off units
|
|
var delay = getDelay(units),
|
|
timerId,
|
|
fn = function() {
|
|
callback(
|
|
populate(new Timespan(), /** @type{Date} */(start||new Date()), /** @type{Date} */(end||new Date()), 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
|
|
*/
|
|
var setLabels = countdown.setLabels = function(singular, plural) {
|
|
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];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Revert to the default unit labels
|
|
* @public
|
|
*/
|
|
var resetLabels = countdown.resetLabels = function() {
|
|
LABELS_SINGLUAR = 'millisecond|s|m|h|d|w|month|year|decade|century|millennium'.split('|');
|
|
LABELS_PLURAL = 'milliseconds|s|m|h|d|w|months|years|decades|centuries|millennia'.split('|');
|
|
};
|
|
|
|
resetLabels();
|
|
|
|
if (module && module.exports) {
|
|
module.exports = countdown;
|
|
|
|
} else if (typeof window.define === 'function' && window.define.amd) {
|
|
window.define('countdown', [], function() {
|
|
return countdown;
|
|
});
|
|
}
|
|
|
|
return countdown;
|
|
|
|
})(module);
|