time-span.js

# time-span.js |
--- | ---
¶ |
    /*
    * 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: |
      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: |
      if ((match = str.match(timeSpanNoDays))) {
        return new TimeSpan(parseMilliseconds(match[4]), match[3], match[2], match[1], 0);
      }

      return null;
    };

    var months  = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
¶ 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,
        get: function (date) { return date.getMilliseconds(date) },
        set: function (val, date) { date.setMilliseconds(val) },
        compute: function (delta, date, computed) {
          var round = delta > 0 ? Math.floor : Math.ceil;
          if (delta) {
            computed.seconds += round.call(null, delta / 1000);
            computed.milliseconds += delta % 1000;
          }

          if (Math.abs(computed.milliseconds) >= 1000) {
            computed.seconds += round.call(null, computed.milliseconds / 1000)
            computed.milliseconds = computed.milliseconds % 1000;
          }

          return computed;
        }
      },
      'seconds': {
        exp: /(\d+)second[s]?/i,
        get: function (date) { return date.getSeconds(date) },
        set: function (val, date) { date.setSeconds(val) },
        compute: function (delta, date, computed) {
          var round = delta > 0 ? Math.floor : Math.ceil;
          if (delta) {
            computed.minutes += round.call(null, delta / 60);
            computed.seconds += delta % 60;
          }

          if (Math.abs(computed.seconds) >= 60) {
            computed.minutes += round.call(null, computed.seconds / 60);
            computed.seconds = computed.seconds % 60;
          }

          return computed;
        }
      },
      'minutes': {
        exp: /(\d+)minute[s]?/i,
        get: function (date) { return date.getMinutes(date) },
        set: function (val, date) { date.setMinutes(val) },
        compute: function (delta, date, computed) {
          var round = delta > 0 ? Math.floor : Math.ceil;
          if (delta) {
            computed.hours += round.call(null, delta / 60);
            computed.minutes += delta % 60;
          }

          if (Math.abs(computed.minutes) >= 60) {
            computed.hours += round.call(null, computed.minutes / 60);
            computed.minutes = computed.minutes % 60;
          }

          return computed;
        }
      },
      'hours': {
        exp: /(\d+)hour[s]?/i,
        get: function (date) { return date.getHours(date) },
        set: function (val, date) { date.setHours(val) },
        compute: function (delta, date, computed) {
          var round = delta > 0 ? Math.floor : Math.ceil;
          if (delta) {
            computed.days += round.call(null, delta / 24);
            computed.hours += delta % 24;
          }

          if (Math.abs(computed.hours) >= 24) {
            computed.days += round.call(null, computed.hours / 24);
            computed.hours = computed.hours % 24;
          }

          return computed;
        }
      },
      'days': {
        exp: /(\d+)day[s]?/i,
        get: function (date) { return date.getDate(date) },
        set: function (val, date) { date.setDate(val) },
        compute: function (delta, date, computed) {
          var sign     = delta >= 0 ? 1 : -1,
              opsign   = delta >= 0 ? -1 : 1,
              clean    = 0,
              original = delta,
              month    = computed.months,
              days     = months[month];

          if (delta) {
            while (Math.abs(delta) >= days) {
              month += sign * 1;
              computed.months += sign * 1;
              delta += opsign * days;

              if (month < 0) { month = 11 }
              else if (month > 11) { month = 0 }

              days = months[month];
            }

            computed.days += (sign * delta);
          }

          if (computed.days < 0) {
            clean = -1;
          }
          else if (computed.days > months[computed.months]) {
            clean = 1;
          }

          if (clean !== 0) {
            computed.months += clean;
            if (computed.months < 0) { computed.months = 11 }
            else if (computed.months > 11) { computed.months = 0 }
            computed.days = months[computed.months] + computed.days;
          }

          return computed;
        }
      },
      'months': {
        exp: /(\d+)month[s]?/i,
        get: function (date) { return date.getMonth(date) },
        set: function (val, date) { date.setMonth(val) },
        compute: function (delta, date, 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,
        get: function (date) { return date.getFullYear(date) },
        set: function (val, date) { date.setFullYear(val) },
        compute: function (delta, date, 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: `2010-04-03T12:34:15Z+12MINUTES` `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 simple = Date.parse(str),
          iso = '^([^Z]+)',
          zulu = 'Z([\\+|\\-])?',
          diff = {},
          base,
          sign,
          complex,
          inspect,
          dateTime,
          modifiers;

      if (/now/i.test(str)) {
        iso = '^(NOW)';
        zulu = zulu.replace(/Z/, 'NOW');
      }
¶ If Date string supplied actually conforms to UTC Time (ISO8601), return a new Date. |
      if (!isNaN(simple)) {
        return new Date(simple);
      }
¶ Create the `RegExp` for the end component of the target `str` to parse. |
      parserNames.forEach(function (group) {
        zulu += '(\\d+[a-zA-Z]+)?';
      });
¶ Parse the `ISO8601` component, and the end component from the target `str`. |
      dateTime = str.match(new RegExp(iso, 'i'));
      modifiers = str.match(new RegExp(zulu, 'i'));
¶ If there was no match on either part then it must be a bad value. |
      if (!dateTime || !modifiers) {
        return null;
      }
¶ Create a new `Date` object from the `ISO8601` component of the target `str`. |
      base = /now/i.test(dateTime[1]) ? Date.now() : Date.parse(dateTime[1]);
      complex = new Date(base);
      sign = modifiers[1] === '+' ? 1 : -1;
¶ 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. |
      var computed = {
        milliseconds: complex.getMilliseconds(),
        seconds: complex.getSeconds(),
        minutes: complex.getMinutes(),
        hours: complex.getHours(),
        days: complex.getDate(),
        months: complex.getMonth(),
        years: complex.getFullYear()
      };

      parserNames.forEach(function (name) {
        computed = parsers[name].compute(diff[name], complex, computed);
      });

      return new Date(
        Date.UTC(
          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 (!start instanceof Date) {
        start = exports.parseDate(start);
      }

      if (!end instanceof Date) {
        end = exports.parseDate(end);
      }

      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);
    };