blob: 62180bf66ffe6cacbbd234f70bd4f33a47f4b547 [file] [log] [blame]
/*
* JavaScript TimeSpan Library
*
* Copyright (c) 2010 Michael Stum, Charlie Robbins
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
//
// ### Time constants
//
var msecPerSecond = 1000,
msecPerMinute = 60000,
msecPerHour = 3600000,
msecPerDay = 86400000;
//
// ### Timespan Parsers
//
var timeSpanWithDays = /^(\d+):(\d+):(\d+):(\d+)(\.\d+)?/,
timeSpanNoDays = /^(\d+):(\d+):(\d+)(\.\d+)?/;
//
// ### function TimeSpan (milliseconds, seconds, minutes, hours, days)
// #### @milliseconds {Number} Number of milliseconds for this instance.
// #### @seconds {Number} Number of seconds for this instance.
// #### @minutes {Number} Number of minutes for this instance.
// #### @hours {Number} Number of hours for this instance.
// #### @days {Number} Number of days for this instance.
// Constructor function for the `TimeSpan` object which represents a length
// of positive or negative milliseconds componentized into milliseconds,
// seconds, hours, and days.
//
var TimeSpan = exports.TimeSpan = function (milliseconds, seconds, minutes, hours, days) {
this.msecs = 0;
if (isNumeric(days)) {
this.msecs += (days * msecPerDay);
}
if (isNumeric(hours)) {
this.msecs += (hours * msecPerHour);
}
if (isNumeric(minutes)) {
this.msecs += (minutes * msecPerMinute);
}
if (isNumeric(seconds)) {
this.msecs += (seconds * msecPerSecond);
}
if (isNumeric(milliseconds)) {
this.msecs += milliseconds;
}
};
//
// ## Factory methods
// Helper methods for creating new TimeSpan objects
// from various criteria: milliseconds, seconds, minutes,
// hours, days, strings and other `TimeSpan` instances.
//
//
// ### function fromMilliseconds (milliseconds)
// #### @milliseconds {Number} Amount of milliseconds for the new TimeSpan instance.
// Creates a new `TimeSpan` instance with the specified `milliseconds`.
//
exports.fromMilliseconds = function (milliseconds) {
if (!isNumeric(milliseconds)) { return }
return new TimeSpan(milliseconds, 0, 0, 0, 0);
}
//
// ### function fromSeconds (seconds)
// #### @milliseconds {Number} Amount of seconds for the new TimeSpan instance.
// Creates a new `TimeSpan` instance with the specified `seconds`.
//
exports.fromSeconds = function (seconds) {
if (!isNumeric(seconds)) { return }
return new TimeSpan(0, seconds, 0, 0, 0);
};
//
// ### function fromMinutes (milliseconds)
// #### @milliseconds {Number} Amount of minutes for the new TimeSpan instance.
// Creates a new `TimeSpan` instance with the specified `minutes`.
//
exports.fromMinutes = function (minutes) {
if (!isNumeric(minutes)) { return }
return new TimeSpan(0, 0, minutes, 0, 0);
};
//
// ### function fromHours (hours)
// #### @milliseconds {Number} Amount of hours for the new TimeSpan instance.
// Creates a new `TimeSpan` instance with the specified `hours`.
//
exports.fromHours = function (hours) {
if (!isNumeric(hours)) { return }
return new TimeSpan(0, 0, 0, hours, 0);
};
//
// ### function fromDays (days)
// #### @milliseconds {Number} Amount of days for the new TimeSpan instance.
// Creates a new `TimeSpan` instance with the specified `days`.
//
exports.fromDays = function (days) {
if (!isNumeric(days)) { return }
return new TimeSpan(0, 0, 0, 0, days);
};
//
// ### function parse (str)
// #### @str {string} Timespan string to parse.
// Creates a new `TimeSpan` instance from the specified
// string, `str`.
//
exports.parse = function (str) {
var match, milliseconds;
function parseMilliseconds (value) {
return value ? parseFloat('0' + value) * 1000 : 0;
}
// If we match against a full TimeSpan:
// [days]:[hours]:[minutes]:[seconds].[milliseconds]?
if ((match = str.match(timeSpanWithDays))) {
return new TimeSpan(parseMilliseconds(match[5]), match[4], match[3], match[2], match[1]);
}
// If we match against a partial TimeSpan:
// [hours]:[minutes]:[seconds].[milliseconds]?
if ((match = str.match(timeSpanNoDays))) {
return new TimeSpan(parseMilliseconds(match[4]), match[3], match[2], match[1], 0);
}
return null;
};
//
// List of default singular time modifiers and associated
// computation algoritm. Assumes in order, smallest to greatest
// performing carry forward additiona / subtraction for each
// Date-Time component.
//
var parsers = {
'milliseconds': {
exp: /(\d+)milli(?:second)?[s]?/i,
compute: function (delta, computed) {
return _compute(delta, computed, {
current: 'milliseconds',
next: 'seconds',
max: 1000
});
}
},
'seconds': {
exp: /(\d+)second[s]?/i,
compute: function (delta, computed) {
return _compute(delta, computed, {
current: 'seconds',
next: 'minutes',
max: 60
});
}
},
'minutes': {
exp: /(\d+)minute[s]?/i,
compute: function (delta, computed) {
return _compute(delta, computed, {
current: 'minutes',
next: 'hours',
max: 60
});
}
},
'hours': {
exp: /(\d+)hour[s]?/i,
compute: function (delta, computed) {
return _compute(delta, computed, {
current: 'hours',
next: 'days',
max: 24
});
}
},
'days': {
exp: /(\d+)day[s]?/i,
compute: function (delta, computed) {
var days = monthDays(computed.months, computed.years),
sign = delta >= 0 ? 1 : -1,
opsign = delta >= 0 ? -1 : 1,
clean = 0;
function update (months) {
if (months < 0) {
computed.years -= 1;
return 11;
}
else if (months > 11) {
computed.years += 1;
return 0
}
return months;
}
if (delta) {
while (Math.abs(delta) >= days) {
computed.months += sign * 1;
computed.months = update(computed.months);
delta += opsign * days;
days = monthDays(computed.months, computed.years);
}
computed.days += (opsign * delta);
}
if (computed.days < 0) { clean = -1 }
else if (computed.days > months[computed.months]) { clean = 1 }
if (clean === -1 || clean === 1) {
computed.months += clean;
computed.months = update(computed.months);
computed.days = months[computed.months] + computed.days;
}
return computed;
}
},
'months': {
exp: /(\d+)month[s]?/i,
compute: function (delta, computed) {
var round = delta > 0 ? Math.floor : Math.ceil;
if (delta) {
computed.years += round.call(null, delta / 12);
computed.months += delta % 12;
}
if (computed.months > 11) {
computed.years += Math.floor((computed.months + 1) / 12);
computed.months = ((computed.months + 1) % 12) - 1;
}
return computed;
}
},
'years': {
exp: /(\d+)year[s]?/i,
compute: function (delta, computed) {
if (delta) { computed.years += delta; }
return computed;
}
}
};
//
// Compute the list of parser names for
// later use.
//
var parserNames = Object.keys(parsers);
//
// ### function parseDate (str)
// #### @str {string} String to parse into a date
// Parses the specified liberal Date-Time string according to
// ISO8601 **and**:
//
// 1. `2010-04-03T12:34:15Z+12MINUTES`
// 2. `NOW-4HOURS`
//
// Valid modifiers for the more liberal Date-Time string(s):
//
// YEAR, YEARS
// MONTH, MONTHS
// DAY, DAYS
// HOUR, HOURS
// MINUTE, MINUTES
// SECOND, SECONDS
// MILLI, MILLIS, MILLISECOND, MILLISECONDS
//
exports.parseDate = function (str) {
var dateTime = Date.parse(str),
iso = '^([^Z]+)',
zulu = 'Z([\\+|\\-])?',
diff = {},
computed,
modifiers,
sign;
//
// If Date string supplied actually conforms
// to UTC Time (ISO8601), return a new Date.
//
if (!isNaN(dateTime)) {
return new Date(dateTime);
}
//
// Create the `RegExp` for the end component
// of the target `str` to parse.
//
parserNames.forEach(function (group) {
zulu += '(\\d+[a-zA-Z]+)?';
});
if (/^NOW/i.test(str)) {
//
// If the target `str` is a liberal `NOW-*`,
// then set the base `dateTime` appropriately.
//
dateTime = Date.now();
zulu = zulu.replace(/Z/, 'NOW');
}
else if (/^\-/.test(str) || /^\+/.test(str)) {
dateTime = Date.now();
zulu = zulu.replace(/Z/, '');
}
else {
//
// Parse the `ISO8601` component, and the end
// component from the target `str`.
//
dateTime = str.match(new RegExp(iso, 'i'));
dateTime = Date.parse(dateTime[1]);
}
//
// If there was no match on either part then
// it must be a bad value.
//
if (!dateTime || !(modifiers = str.match(new RegExp(zulu, 'i')))) {
return null;
}
//
// Create a new `Date` object from the `ISO8601`
// component of the target `str`.
//
dateTime = new Date(dateTime);
sign = modifiers[1] === '+' ? 1 : -1;
//
// Create an Object-literal for consistently accessing
// the various components of the computed Date.
//
var computed = {
milliseconds: dateTime.getMilliseconds(),
seconds: dateTime.getSeconds(),
minutes: dateTime.getMinutes(),
hours: dateTime.getHours(),
days: dateTime.getDate(),
months: dateTime.getMonth(),
years: dateTime.getFullYear()
};
//
// Parse the individual component spans (months, years, etc)
// from the modifier strings that we parsed from the end
// of the target `str`.
//
modifiers.slice(2).filter(Boolean).forEach(function (modifier) {
parserNames.forEach(function (name) {
var match;
if (!(match = modifier.match(parsers[name].exp))) {
return;
}
diff[name] = sign * parseInt(match[1], 10);
})
});
//
// Compute the total `diff` by iteratively computing
// the partial components from smallest to largest.
//
parserNames.forEach(function (name) {
computed = parsers[name].compute(diff[name], computed);
});
return new Date(
computed.years,
computed.months,
computed.days,
computed.hours,
computed.minutes,
computed.seconds,
computed.milliseconds
);
};
//
// ### function fromDates (start, end, abs)
// #### @start {Date} Start date of the `TimeSpan` instance to return
// #### @end {Date} End date of the `TimeSpan` instance to return
// #### @abs {boolean} Value indicating to return an absolute value
// Returns a new `TimeSpan` instance representing the difference between
// the `start` and `end` Dates.
//
exports.fromDates = function (start, end, abs) {
if (typeof start === 'string') {
start = exports.parseDate(start);
}
if (typeof end === 'string') {
end = exports.parseDate(end);
}
if (!(start instanceof Date && end instanceof Date)) {
return null;
}
var differenceMsecs = end.valueOf() - start.valueOf();
if (abs) {
differenceMsecs = Math.abs(differenceMsecs);
}
return new TimeSpan(differenceMsecs, 0, 0, 0, 0);
};
//
// ## Module Helpers
// Module-level helpers for various utilities such as:
// instanceOf, parsability, and cloning.
//
//
// ### function test (str)
// #### @str {string} String value to test if it is a TimeSpan
// Returns a value indicating if the specified string, `str`,
// is a parsable `TimeSpan` value.
//
exports.test = function (str) {
return timeSpanWithDays.test(str) || timeSpanNoDays.test(str);
};
//
// ### function instanceOf (timeSpan)
// #### @timeSpan {Object} Object to check TimeSpan quality.
// Returns a value indicating if the specified `timeSpan` is
// in fact a `TimeSpan` instance.
//
exports.instanceOf = function (timeSpan) {
return timeSpan instanceof TimeSpan;
};
//
// ### function clone (timeSpan)
// #### @timeSpan {TimeSpan} TimeSpan object to clone.
// Returns a new `TimeSpan` instance with the same value
// as the `timeSpan` object supplied.
//
exports.clone = function (timeSpan) {
if (!(timeSpan instanceof TimeSpan)) { return }
return exports.fromMilliseconds(timeSpan.totalMilliseconds());
};
//
// ## Addition
// Methods for adding `TimeSpan` instances,
// milliseconds, seconds, hours, and days to other
// `TimeSpan` instances.
//
//
// ### function add (timeSpan)
// #### @timeSpan {TimeSpan} TimeSpan to add to this instance
// Adds the specified `timeSpan` to this instance.
//
TimeSpan.prototype.add = function (timeSpan) {
if (!(timeSpan instanceof TimeSpan)) { return }
this.msecs += timeSpan.totalMilliseconds();
};
//
// ### function addMilliseconds (milliseconds)
// #### @milliseconds {Number} Number of milliseconds to add.
// Adds the specified `milliseconds` to this instance.
//
TimeSpan.prototype.addMilliseconds = function (milliseconds) {
if (!isNumeric(milliseconds)) { return }
this.msecs += milliseconds;
};
//
// ### function addSeconds (seconds)
// #### @seconds {Number} Number of seconds to add.
// Adds the specified `seconds` to this instance.
//
TimeSpan.prototype.addSeconds = function (seconds) {
if (!isNumeric(seconds)) { return }
this.msecs += (seconds * msecPerSecond);
};
//
// ### function addMinutes (minutes)
// #### @minutes {Number} Number of minutes to add.
// Adds the specified `minutes` to this instance.
//
TimeSpan.prototype.addMinutes = function (minutes) {
if (!isNumeric(minutes)) { return }
this.msecs += (minutes * msecPerMinute);
};
//
// ### function addHours (hours)
// #### @hours {Number} Number of hours to add.
// Adds the specified `hours` to this instance.
//
TimeSpan.prototype.addHours = function (hours) {
if (!isNumeric(hours)) { return }
this.msecs += (hours * msecPerHour);
};
//
// ### function addDays (days)
// #### @days {Number} Number of days to add.
// Adds the specified `days` to this instance.
//
TimeSpan.prototype.addDays = function (days) {
if (!isNumeric(days)) { return }
this.msecs += (days * msecPerDay);
};
//
// ## Subtraction
// Methods for subtracting `TimeSpan` instances,
// milliseconds, seconds, hours, and days from other
// `TimeSpan` instances.
//
//
// ### function subtract (timeSpan)
// #### @timeSpan {TimeSpan} TimeSpan to subtract from this instance.
// Subtracts the specified `timeSpan` from this instance.
//
TimeSpan.prototype.subtract = function (timeSpan) {
if (!(timeSpan instanceof TimeSpan)) { return }
this.msecs -= timeSpan.totalMilliseconds();
};
//
// ### function subtractMilliseconds (milliseconds)
// #### @milliseconds {Number} Number of milliseconds to subtract.
// Subtracts the specified `milliseconds` from this instance.
//
TimeSpan.prototype.subtractMilliseconds = function (milliseconds) {
if (!isNumeric(milliseconds)) { return }
this.msecs -= milliseconds;
};
//
// ### function subtractSeconds (seconds)
// #### @seconds {Number} Number of seconds to subtract.
// Subtracts the specified `seconds` from this instance.
//
TimeSpan.prototype.subtractSeconds = function (seconds) {
if (!isNumeric(seconds)) { return }
this.msecs -= (seconds * msecPerSecond);
};
//
// ### function subtractMinutes (minutes)
// #### @minutes {Number} Number of minutes to subtract.
// Subtracts the specified `minutes` from this instance.
//
TimeSpan.prototype.subtractMinutes = function (minutes) {
if (!isNumeric(minutes)) { return }
this.msecs -= (minutes * msecPerMinute);
};
//
// ### function subtractHours (hours)
// #### @hours {Number} Number of hours to subtract.
// Subtracts the specified `hours` from this instance.
//
TimeSpan.prototype.subtractHours = function (hours) {
if (!isNumeric(hours)) { return }
this.msecs -= (hours * msecPerHour);
};
//
// ### function subtractDays (days)
// #### @days {Number} Number of days to subtract.
// Subtracts the specified `days` from this instance.
//
TimeSpan.prototype.subtractDays = function (days) {
if (!isNumeric(days)) { return }
this.msecs -= (days * msecPerDay);
};
//
// ## Getters
// Methods for retrieving components of a `TimeSpan`
// instance: milliseconds, seconds, minutes, hours, and days.
//
//
// ### function totalMilliseconds (roundDown)
// #### @roundDown {boolean} Value indicating if the value should be rounded down.
// Returns the total number of milliseconds for this instance, rounding down
// to the nearest integer if `roundDown` is set.
//
TimeSpan.prototype.totalMilliseconds = function (roundDown) {
var result = this.msecs;
if (roundDown === true) {
result = Math.floor(result);
}
return result;
};
//
// ### function totalSeconds (roundDown)
// #### @roundDown {boolean} Value indicating if the value should be rounded down.
// Returns the total number of seconds for this instance, rounding down
// to the nearest integer if `roundDown` is set.
//
TimeSpan.prototype.totalSeconds = function (roundDown) {
var result = this.msecs / msecPerSecond;
if (roundDown === true) {
result = Math.floor(result);
}
return result;
};
//
// ### function totalMinutes (roundDown)
// #### @roundDown {boolean} Value indicating if the value should be rounded down.
// Returns the total number of minutes for this instance, rounding down
// to the nearest integer if `roundDown` is set.
//
TimeSpan.prototype.totalMinutes = function (roundDown) {
var result = this.msecs / msecPerMinute;
if (roundDown === true) {
result = Math.floor(result);
}
return result;
};
//
// ### function totalHours (roundDown)
// #### @roundDown {boolean} Value indicating if the value should be rounded down.
// Returns the total number of hours for this instance, rounding down
// to the nearest integer if `roundDown` is set.
//
TimeSpan.prototype.totalHours = function (roundDown) {
var result = this.msecs / msecPerHour;
if (roundDown === true) {
result = Math.floor(result);
}
return result;
};
//
// ### function totalDays (roundDown)
// #### @roundDown {boolean} Value indicating if the value should be rounded down.
// Returns the total number of days for this instance, rounding down
// to the nearest integer if `roundDown` is set.
//
TimeSpan.prototype.totalDays = function (roundDown) {
var result = this.msecs / msecPerDay;
if (roundDown === true) {
result = Math.floor(result);
}
return result;
};
//
// ### @milliseconds
// Returns the length of this `TimeSpan` instance in milliseconds.
//
TimeSpan.prototype.__defineGetter__('milliseconds', function () {
return this.msecs % 1000;
});
//
// ### @seconds
// Returns the length of this `TimeSpan` instance in seconds.
//
TimeSpan.prototype.__defineGetter__('seconds', function () {
return Math.floor(this.msecs / msecPerSecond) % 60;
});
//
// ### @minutes
// Returns the length of this `TimeSpan` instance in minutes.
//
TimeSpan.prototype.__defineGetter__('minutes', function () {
return Math.floor(this.msecs / msecPerMinute) % 60;
});
//
// ### @hours
// Returns the length of this `TimeSpan` instance in hours.
//
TimeSpan.prototype.__defineGetter__('hours', function () {
return Math.floor(this.msecs / msecPerHour) % 24;
});
//
// ### @days
// Returns the length of this `TimeSpan` instance in days.
//
TimeSpan.prototype.__defineGetter__('days', function () {
return Math.floor(this.msecs / msecPerDay);
});
//
// ## Instance Helpers
// Various help methods for performing utilities
// such as equality and serialization
//
//
// ### function equals (timeSpan)
// #### @timeSpan {TimeSpan} TimeSpan instance to assert equal
// Returns a value indicating if the specified `timeSpan` is equal
// in milliseconds to this instance.
//
TimeSpan.prototype.equals = function (timeSpan) {
if (!(timeSpan instanceof TimeSpan)) { return }
return this.msecs === timeSpan.totalMilliseconds();
};
//
// ### function toString ()
// Returns a string representation of this `TimeSpan`
// instance according to current `format`.
//
TimeSpan.prototype.toString = function () {
if (!this.format) { return this._format() }
return this.format(this);
};
//
// ### @private function _format ()
// Returns the default string representation of this instance.
//
TimeSpan.prototype._format = function () {
return [
this.days,
this.hours,
this.minutes,
this.seconds + '.' + this.milliseconds
].join(':')
};
//
// ### @private function isNumeric (input)
// #### @input {Number} Value to check numeric quality of.
// Returns a value indicating the numeric quality of the
// specified `input`.
//
function isNumeric (input) {
return input && !isNaN(parseFloat(input)) && isFinite(input);
};
//
// ### @private function _compute (delta, date, computed, options)
// #### @delta {Number} Channge in this component of the date
// #### @computed {Object} Currently computed date.
// #### @options {Object} Options for the computation
// Performs carry forward addition or subtraction for the
// `options.current` component of the `computed` date, carrying
// it forward to `options.next` depending on the maximum value,
// `options.max`.
//
function _compute (delta, computed, options) {
var current = options.current,
next = options.next,
max = options.max,
round = delta > 0 ? Math.floor : Math.ceil;
if (delta) {
computed[next] += round.call(null, delta / max);
computed[current] += delta % max;
}
if (Math.abs(computed[current]) >= max) {
computed[next] += round.call(null, computed[current] / max)
computed[current] = computed[current] % max;
}
return computed;
}
//
// ### @private monthDays (month, year)
// #### @month {Number} Month to get days for.
// #### @year {Number} Year of the month to get days for.
// Returns the number of days in the specified `month` observing
// leap years.
//
var months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
function monthDays (month, year) {
if (((year % 100 !== 0 && year % 4 === 0)
|| year % 400 === 0) && month === 1) {
return 29;
}
return months[month];
}