baja/obj/Time.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/**
 * Defines {@link baja.Time}.
 * @module baja/obj/Time
 */
define([ "bajaScript/sys",
        "bajaScript/baja/obj/Simple",
        "bajaScript/baja/obj/TimeFormat",
        "bajaScript/baja/obj/dateTimeUtil",
        "bajaScript/baja/obj/numberUtil",
        "bajaScript/baja/obj/objUtil" ], function (
         baja,
         Simple,
         TimeFormat,
         dateTimeUtil,
         numberUtil,
         objUtil) {
  
  "use strict";
  
  var subclass = baja.subclass,
      callSuper = baja.callSuper,
      strictArg = baja.strictArg,
      strictAllArgs = baja.strictAllArgs,
      objectify = baja.objectify,
      bajaHasType = baja.hasType,
      bajaDef = baja.def,
      
      cacheDecode = objUtil.cacheDecode,
      cacheEncode = objUtil.cacheEncode,
      
      SHOW_DATE = TimeFormat.SHOW_DATE,
      SHOW_TIME = TimeFormat.SHOW_TIME,
      SHOW_SECONDS = TimeFormat.SHOW_SECONDS,
      SHOW_MILLIS = TimeFormat.SHOW_MILLIS,
      
      MILLIS_IN_SECOND = dateTimeUtil.MILLIS_IN_SECOND,
      MILLIS_IN_MINUTE = dateTimeUtil.MILLIS_IN_MINUTE,
      MILLIS_IN_HOUR = dateTimeUtil.MILLIS_IN_HOUR,
      MILLIS_IN_DAY = dateTimeUtil.MILLIS_IN_DAY,
      toDateTimeString = dateTimeUtil.toDateTimeString,
      toDateTimeStringSync = dateTimeUtil.toDateTimeStringSync,
      addZeroPad = numberUtil.addZeroPad;
  
  /**
   * Represents a `baja:Time` in BajaScript.
   * 
   * `Time` stores a time of day which is independent 
   * of any date in the past or future.
   * 
   * When creating a `Simple`, always use the `make()` method instead of 
   * creating a new Object.
   *
   * @class
   * @alias baja.Time
   * @extends baja.Simple
   */
  var Time = function Time(hour, min, sec, ms) {
    // Constructor should be considered private
    callSuper(Time, this, arguments); 
    this.$hour = hour;
    this.$min = min;
    this.$sec = sec;
    this.$ms = ms;        
  };
  
  subclass(Time, Simple);
  
  function isNumber(num) {
    return baja.hasType(num, 'baja:Number');
  }

  /**
   * Make a `Time`.
   *
   * @param {Object|number} obj - the object literal used for the method's
   * arguments, or number of milliseconds past the start of the day.
   *
   * @param {Number} [obj.hour] hours - (0-23).
   *
   * @param {Number} [obj.min] minutes - (0-59).
   *
   * @param {Number} [obj.sec] seconds - (0-59).
   *
   * @param {Number} [obj.ms] milliseconds - (0-999).
   *
   * @param {baja.RelTime} [obj.relTime] - if defined, this is the milliseconds
   * since the start of the day. This overrides the other hour, min, sec and ms
   * arguments.
   * 
   * @param {number} [obj.milliseconds] - if defined, this is the milliseconds
   * since the start of the day. This overrides the other hour, min, sec, ms
   * and relTime arguments.
   *
   * @returns {baja.Time}
   * 
   * @example
   *   // An object literal is used for the method's arguments...
   *   var t1 = baja.Time.make({
   *     hour: 23,
   *     min: 12,
   *     sec: 15,
   *     ms: 789
   *   });
   *   
   *   // ...or use a baja.RelTime to specify hour, min, sec and ms...
   *   var t2 = baja.Time.make({
   *     relTime: myRelTime
   *   });
   *   
   *   // ...or pass in milliseconds past midnight...
   *   var t3 = baja.Time.make(12345);
   */   
  Time.make = function (obj) {
    obj = objectify(obj, "milliseconds");
    var hour = bajaDef(obj.hour, 0),
        min = bajaDef(obj.min, 0),
        sec = bajaDef(obj.sec, 0),
        ms = bajaDef(obj.ms, 0);

    function processMillis(millis) {
      strictArg(millis, Number);
      millis = millis % MILLIS_IN_DAY;
      hour = Math.floor(millis / MILLIS_IN_HOUR);
      millis = millis % MILLIS_IN_HOUR;
      min = Math.floor(millis / MILLIS_IN_MINUTE);
      millis = millis % MILLIS_IN_MINUTE;
      sec = Math.floor(millis / MILLIS_IN_SECOND);
      ms = Math.floor(millis % MILLIS_IN_SECOND);
    }

    if (typeof obj.milliseconds === "number") {
      // Create from a number of milliseconds...
      processMillis(obj.milliseconds);
    } else if (bajaHasType(obj.relTime, 'baja:RelTime')) {
      // Build from rel time (overrides other hour, min, sec and ms on object)...
      processMillis(obj.relTime.getMillis());
    } else {
      // Attempt to get time from Object Literal...
      if (isNumber(hour)) { hour = hour.valueOf(); }
      if (isNumber(min)) { min = min.valueOf(); }
      if (isNumber(sec)) { sec = sec.valueOf(); }
      if (isNumber(ms)) { ms = ms.valueOf(); }
    }
  
    // Ensure we're dealing with numbers
    strictAllArgs([ hour, min, sec, ms ], [ Number, Number, Number, Number ]);

    if (hour < 0 || hour > 23 ||
        min < 0 || min > 59 ||
        sec < 0 || sec > 59 ||
        ms < 0 || ms > 999) {

      throw new Error("Invalid time: " +
                       hour + ":" +
                       min + ":" +
                       sec + "." +
                       ms);
    }
    
    if (hour === 0 && min === 0 && sec === 0 && ms === 0) {
      return Time.DEFAULT;
    }
    
    return new Time(hour, min, sec, ms);
  };
  
  /**
   * Make a `Time`.
   *
   * @param {Object} obj - the object literal used for the method's arguments.
   *
   * @param {Number} [obj.hour] hours - (0-23).
   *
   * @param {Number} [obj.min] minutes - (0-59).
   *
   * @param {Number} [obj.sec] seconds - (0-59).
   *
   * @param {Number} [obj.ms] milliseconds - (0-999).
   *
   * @param {baja.RelTime} [obj.relTime] - if defined, this is the milliseconds
   * since the start of the day. This overrides the other hour, min, sec and ms
   * arguments.
   *
   * @returns {baja.Time}
   * 
   * @example
   *   // An object literal is used for the method's arguments...
   *   var t1 = baja.$("baja:Time").make({
   *     hour: 23,
   *     min: 12,
   *     sec: 15,
   *     ms: 789
   *   });
   *   
   *   // ...or use a baja.RelTime to specify hour, min, sec and ms...
   *   var t2 = baja.$("baja:Time").make({
   *     relTime: timeOfDayMillis 
   *   });
   *   
   *   // ...or pass in milliseconds past midnight...
   *   var t3 = baja.Time.make(12345);
   */   
  Time.prototype.make = function (obj) {
    return Time.make.apply(Time, arguments);
  };
  
  /**
   * Decode a `Time` from a `String`.
   *
   * @method
   *
   * @param {String} str
   *
   * @returns {baja.Time}
   */   
  Time.prototype.decodeFromString = cacheDecode(function (str) {
    // Time ISO 8601 format hh:mm:ss.mmm
    var res = /^([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])\.([0-9]{3})$/
              .exec(str);
    
    if (res === null) {
      throw new Error("Failed to decode time: " + str);
    }
    
    return Time.make({
      hour: parseInt(res[1], 10),
      min: parseInt(res[2], 10),
      sec: parseInt(res[3], 10),
      ms: parseInt(res[4], 10)
    });
  });
              
  /**
   * Encode a `Time` to a `String`.
   *
   * @method
   *
   * @returns {String}
   */
  Time.prototype.encodeToString = cacheEncode(function () {
    return addZeroPad(this.$hour, 2) + ":" + 
           addZeroPad(this.$min, 2) + ":" + 
           addZeroPad(this.$sec, 2) + "." +  
           addZeroPad(this.$ms, 3);
  });
  
  /**
   * Equality test.
   *
   * @param obj
   *
   * @returns {Boolean}
   */
  Time.prototype.equals = function (obj) {
    if (bajaHasType(obj) && obj.getType().equals(this.getType())) {
      return this.getTimeOfDayMillis() === obj.getTimeOfDayMillis();
    }

    return false;
  };
  
  /**
   * Default `Time` instance.
   * @type {baja.Time}
   */
  Time.DEFAULT = new Time(0, 0, 0, 0);
  
  /**
   * Midnight `Time`.
   * @type {baja.Time}
   */
  Time.MIDNIGHT = Time.DEFAULT;
        
  /**
   * Return hours (0-23).
   *
   * @returns {Number}
   */
  Time.prototype.getHour = function () {
    return this.$hour;
  };
  
  /**
   * Return minutes (0-59).
   *
   * @returns {Number}
   */
  Time.prototype.getMinute = function () {
    return this.$min;
  };
  
  /**
   * Return seconds (0-59).
   * 
   * @returns {Number}
   */
  Time.prototype.getSecond = function () {
    return this.$sec;
  };
  
  /**
   * Return milliseconds (0-999).
   *
   * @returns {Number}
   */
  Time.prototype.getMillisecond = function () {
    return this.$ms;
  };
  
  /**
   * Return the milliseconds since the start of the day.
   *
   * @returns {Number}
   */
  Time.prototype.getTimeOfDayMillis = function () {
    if (this.$timeOfDayMs === undefined) {
      var ret = this.$hour * MILLIS_IN_HOUR;
      ret += this.$min * MILLIS_IN_MINUTE;
      ret += this.$sec * MILLIS_IN_SECOND;
      ret += this.$ms;
      this.$timeOfDayMs = ret;
    }
    return this.$timeOfDayMs;
  };
  
  /**
   * Return a new time of day by adding the specified duration. If the result
   * goes past midnight, then roll into the next day.
   *
   * @param {baja.RelTime|baja.Time|Number} duration - RelTime or number of
   * millis
   *
   * @returns {baja.Time} the new time with the duration added on.
   */
  Time.prototype.add = function (duration) {
    strictArg(duration);
    
    if (typeof duration.getMillis === 'function') {
      duration = duration.getMillis();
    } else if (typeof duration.getTimeOfDayMillis === 'function') {
      duration = duration.getTimeOfDayMillis();
    }
    
    strictArg(duration, Number);
    
    return Time.make(MILLIS_IN_DAY + this.getTimeOfDayMillis() + duration);
  };

  /**
   * Return true if the specified time is before this time.
   *
   * @param {baja.Time} time
   *
   * @returns {Boolean}
   */
  Time.prototype.isBefore = function (time) {
    strictArg(time, Time);
    return this.getTimeOfDayMillis() < time.getTimeOfDayMillis();
  };
  
  /**
   * Return true if the specified time is after this time.
   *
   * @param {baja.Time} time
   *
   * @returns {Boolean}
   */
  Time.prototype.isAfter = function (time) {
    strictArg(time, Time);
    return this.getTimeOfDayMillis() > time.getTimeOfDayMillis();
  };

  function doToTimeString(toStringFunc, time, obj) {
    obj = objectify(obj);
  
    var textPattern = obj.textPattern || baja.getTimeFormatPattern(),
        show = calculateShow(obj) | SHOW_TIME;
        
    // Filter out invalid flags
    show &= ~SHOW_DATE;
    
    return toStringFunc({
      ok: obj.ok,
      fail: obj.fail,
      show: show,
      textPattern: textPattern,
      hour: time.$hour,
      min: time.$min,
      sec: time.$sec,
      ms: time.$ms
    }, obj.lex);
  }

  function calculateShow(obj) {
    if (typeof obj.show === 'number') {
      return obj.show;
    }

    var showSeconds = obj.showSeconds ? SHOW_SECONDS : 0,
      showMilliseconds = obj.showMilliseconds ?
        SHOW_MILLIS | SHOW_SECONDS : 0;

    return SHOW_TIME | showSeconds | showMilliseconds;
  }
  
 /**
  * Asynchronously get a `String` representation of the time.
  * 
  * This method is invoked asynchronously. A `Function` callback or an object
  * literal that contains a `Function` callback must be supplied.
  *
  * @param {Object|Function} [obj] - the Object Literal for the method's arguments
  * or a Function that will be called with the formatting time String.
  *
  * @param {Function} [obj.ok] - (Deprecated: use Promise) the Function callback
  * that will be invoked once the time has been formatted into a String.
  *
  * @param {Function} [obj.fail] - (Deprecated: use Promise) the Function
  * callback that will be invoked if a fatal error occurs whilst formatting the
  * String.
  *
  * @param {String} [obj.textPattern] - the text pattern to use for formatting.
  * If not specified, then the user's default time format text pattern will be
  * used.
  *
  * @param {Number} [obj.show] - flags used to format the time. For more
  * information, please see {@link baja.TimeFormat}.
  *
  * @returns {Promise.<String>} promise to be resolved with the time string
  *                            
  * @example
  *   myTime.toTimeString().then(function(timeStr) {
  *     baja.outln("The time is: " + timeStr);
  *   });
  */
  Time.prototype.toTimeString = function (obj) {
    return doToTimeString(toDateTimeString, this, objectify(obj, "ok"));
  };
  
  
  /**
   * Synchronously get a `String` representation of the time.
   *
   * This method is invoked synchronously. The string result will be returned
   * directly from this function. Since building up `Time` string
   * representations requires the `baja` lexicon, said lexicon *must* already be
   * retrieved and passed into this method. Apart from that, the behavior is the
   * same as the asynchronous {@link baja.Time#toTimeString}.
   * 
   * @param {Object|Function} obj the Object Literal for the method's arguments
   * or a Function that will be called with the formatting time String.
   *
   * @param {String} [obj.textPattern] the text pattern to use for formatting.
   * If not specified, then the user's default time format text pattern will be
   * used.
   *
   * @param {Number} [obj.show] flags used to format the time. For more
   * information, please see {@link baja.TimeFormat}.
   *                            
   * @param obj.lex the `baja` lexicon
   * 
   * @returns {String}
   * 
   * @throws {Error} if the lexicon is not passed in, or is not the
   * `baja` lexicon
   */
  Time.prototype.toTimeStringSync = function (obj) {
    return doToTimeString(toDateTimeStringSync, this, obj);
  };

  /**
   * @see .toTimeStringSync
   */
  Time.prototype.toString = function (obj) {
    if (obj) {
      return this.toTimeString(obj);
    } else {
      return this.toTimeStringSync();
    }
  };

  /**
   *
   * @returns {Number} milliseconds since the beginning of the day.
   */
  Time.prototype.valueOf = function () {
    return this.getTimeOfDayMillis();
  };

  return Time;
});