baja/obj/RelTime.js

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

/**
 * Defines {@link baja.RelTime}.
 * @module baja/obj/RelTime
 */
define([
  "bajaScript/sys",
  "bajaScript/baja/obj/Simple",
  "bajaScript/baja/obj/dateTimeUtil",
  "bajaScript/baja/obj/objUtil",
  "bajaPromises" ], function (
    baja,
    Simple,
    dateTimeUtil,
    objUtil,
    Promise) {

 "use strict";
 
  var subclass = baja.subclass,
    callSuper = baja.callSuper,
    objectify = baja.objectify,
    strictArg = baja.strictArg,
    bajaDef = baja.def,
     
    cacheDecode = objUtil.cacheDecode,
    cacheEncode = objUtil.cacheEncode,

    DAYS_IN_MONTH = 30,
    DAYS_IN_YEAR = 365,
    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,
    MILLIS_IN_MONTH = MILLIS_IN_DAY * DAYS_IN_MONTH,
    MILLIS_IN_YEAR = MILLIS_IN_DAY * DAYS_IN_YEAR;
 
  /**
   * Represents a `baja:RelTime` in BajaScript.
   * 
   * `RelTime` is a `Simple` type for managing a relative amount of time. 
   * 
   * When creating a `Simple`, always use the `make()` method instead of 
   * creating a new Object.
   *
   * @class
   * @alias baja.RelTime
   * @extends baja.Simple
   */
  var RelTime = function RelTime(ms) {
    callSuper(RelTime, this, arguments); 
    this.$ms = parseInt(ms, 10);
  };
  
  subclass(RelTime, Simple);
  
  /**
   * Make a `RelTime`.
   * 
   * @param {Object|Number} [obj] the Object Literal or the number of milliseconds.
   * @param {Number} [obj.days] the number of days.
   * @param {Number} [obj.hours] the number of hours.
   * @param {Number} [obj.minutes] the number of minutes.
   * @param {Number} [obj.seconds] the number of seconds.
   * @param {Number} [obj.ms] the number of milliseconds.
   * @returns {baja.RelTime}
   * 
   * @example
   *  //This method can take a number of milliseconds or an Object Literal as 
   *  //method's argument...
   *   var rt1 = baja.RelTime.make(1000); // One second
   *
   *   // ...or we can specify an Object Literal for more arguments...
   *
   *   // Create a RelTime with 2 days + 2 hours + 2 minutes + 2 seconds + 2 milliseconds...
   *   var rt2 = baja.RelTime.make({
   *     days: 2,
   *     hours: 2,
   *     minutes: 2,
   *     seconds: 2,
   *     ms: 2
   *   });
   */
  RelTime.make = function (obj) {
    var ms = 0;
    
    if (typeof obj === "number") {
      ms = obj;
    } else {
      obj = objectify(obj, "ms");
      ms = bajaDef(obj.ms, 0);
      
      strictArg(ms, Number);
      
      if (typeof obj.days === "number") {
        ms += MILLIS_IN_DAY * obj.days;
      }
      if (typeof obj.hours === "number") {
        ms += MILLIS_IN_HOUR * obj.hours;
      }
      if (typeof obj.minutes === "number") {
        ms += MILLIS_IN_MINUTE * obj.minutes;
      }
      if (typeof obj.seconds === "number") {
        ms += MILLIS_IN_SECOND * obj.seconds;
      }
    }
        
    if (ms === 0) {
      return RelTime.DEFAULT;
    }
    return new RelTime(ms);
  };
  
  /**
   * Make a `RelTime`.
   * 
   * @param {Object|Number} [obj] the Object Literal or the number of milliseconds.
   * @param {Number} [obj.days] the number of days.
   * @param {Number} [obj.hours] the number of hours.
   * @param {Number} [obj.minutes] the number of minutes.
   * @param {Number} [obj.seconds] the number of seconds.
   * @param {Number} [obj.ms] the number of milliseconds.
   * @returns {baja.RelTime}
   * 
   * @example
   * //This method can take a number of milliseconds of an Object Literal with 
   * //the method's argument...
   *   var rt1 = baja.$("baja:RelTime").make(1000); // One second
   *
   *   // ...or we can specify an Object Literal for more arguments...
   *
   *   // Create a RelTime with 2 days + 2 hours + 2 minutes + 2 seconds + 2 milliseconds...
   *   var rt2 = baja.$("baja:RelTime").make({
   *     days: 2,
   *     hours: 2,
   *     minutes: 2,
   *     seconds: 2,
   *     ms: 2
   *   });
   */
  RelTime.prototype.make = function (obj) {
    return RelTime.make.apply(RelTime, arguments);
  };
  
  /**
   * Decode a `RelTime` from a `String`.
   *
   * @method
   * @param {String} str
   * @returns {baja.RelTime}
   */  
  RelTime.prototype.decodeFromString = cacheDecode(function (str) {
    // Parse number
    var n = Number(str);
    
    // If still not a number then throw an error
    if (isNaN(n)) {
      throw new Error("Unable to create RelTime: " + str);
    }
  
    return RelTime.make(n);
  });
  
  /**
   * Encode the `RelTime` to a `String`.
   *
   * @method
   * @returns {String}
   */ 
  RelTime.prototype.encodeToString = cacheEncode(function () {
    return this.$ms.toString();
  });
  
  /**
   * Default `RelTime` instance.
   * @type {baja.RelTime}
   */   
  RelTime.DEFAULT = new RelTime(0);
        
  /**
   * Milliseconds in a second.
   * @type {Number}
   */   
  RelTime.MILLIS_IN_SECOND = MILLIS_IN_SECOND;
  
  /**
   * Milliseconds in a minute.
   * @type {Number}
   */  
  RelTime.MILLIS_IN_MINUTE = MILLIS_IN_MINUTE;
  
  /**
   * Milliseconds in an hour.
   * @type {Number}
   */  
  RelTime.MILLIS_IN_HOUR = MILLIS_IN_HOUR;
  
  /**
   * Milliseconds in a day.
   * @type {Number}
   */  
  RelTime.MILLIS_IN_DAY = MILLIS_IN_DAY;
  
  /**
   * `RelTime` instance for a second.
   * @type {baja.RelTime}
   */  
  RelTime.SECOND = RelTime.make(MILLIS_IN_SECOND);
  
  /**
   * `RelTime` instance for a minute.
   * @type {baja.RelTime}
   */
  RelTime.MINUTE = RelTime.make(MILLIS_IN_MINUTE);
  
  /**
   * `RelTime` instance for an hour.
   * @type {baja.RelTime}
   */
  RelTime.HOUR = RelTime.make(MILLIS_IN_HOUR);
  
  /**
   * `RelTime` instance for a day.
   * @type {baja.RelTime}
   */
  RelTime.DAY = RelTime.make(MILLIS_IN_DAY);
  
  /**
   * Equality test.
   *
   * @param obj
   * @returns {Boolean}
   */
  RelTime.prototype.equals = function (obj) {
    return objUtil.valueOfEquals(this, obj);
  };
  
  /**
   * Return the data type symbol.
   * 
   * @returns {String}
   */   
  RelTime.prototype.getDataTypeSymbol = function () {
    return "r";
  };
  
  /**
   * Return number of milliseconds.
   *
   * @returns {Number}
   */    
  RelTime.prototype.valueOf = function () {
    return this.$ms;
  };

  /**
   * Return a `String` representation of a `RelTime`.
   *
   * @param {Object} [cx] the context.
   * @param {Boolean} [cx.showMilliseconds=true] set to false to hide milliseconds.
   * @param {Boolean} [cx.showSeconds=true] set to false to hide seconds. If
   * showSeconds is false then milliseconds will not be shown.
   * @param {Boolean} [cx.showMinutes=true] set to false to hide minutes.
   * @param {Boolean} [cx.showHours=true] set to false to hide hours.
   * @param {Boolean} [cx.showDays=true] set to false to hide days.
   *
   * @example
   *   // IMPORTANT NOTE
   *   //
   *   // If a unit is greater than zero but the show flag for it is set to
   *   // false, then its value will get carried over to the leftmost shown
   *   // unit.
   *
   *   var relTime = baja.RelTime.make({
   *     hours: 1, seconds: 1
   *   });
   *
   *   // 1hour 1sec
   *   relTime.toString();
   *
   *   // 60mins 1sec
   *   relTime.toString({
   *     showHours: false
   *   });
   *
   *   // 3601secs
   *   relTime.toString({
   *     showHours: false,
   *     showMinutes: false
   *   });
   *
   * @returns {String|Promise.<string>} if context is given, a Promise that
   * resolves to a localized string; otherwise an unlocalized string
   */
  RelTime.prototype.toString = function (cx) {
    var showHideContext = cx || {};
    var numberContext = cx && { showSeparators: cx.showSeparators, precision: 0 };

    var showMillis = showHideContext.showMilliseconds !== false,
        showSeconds = showHideContext.showSeconds !== false,
        showMinutes = showHideContext.showMinutes !== false,
        showHours = showHideContext.showHours !== false,
      //if either context.showDays or context.showDay === false then showDays will be set to false
        showDays = showHideContext.showDays !== false,
        fields = [],
        prefix = '';

    var ms = this.$ms;

    if (ms < 0) {
      prefix = '-';
      ms = -ms;
    }

    if (ms < 1000) {
      if (showSeconds) {
        if (showMillis) {
          fields.push(getText('ms', numberContext, ms));
        } else {
          fields.push(getText('seconds', numberContext, 0));
        }
      } else if (showMinutes) {
        fields.push(getText('minutes', numberContext, 0));
      } else if (showHours) {
        fields.push(getText('hours', numberContext, 0));
      } else if (showDays) {
        fields.push(getText('days', numberContext, 0));
      }
    } else {
      if (ms >= MILLIS_IN_DAY && showDays) {
        var days = Math.abs(this.getDaysPart());
        ms = ms % MILLIS_IN_DAY;
        fields.push(getText(days === 1 ? 'day' : 'days', numberContext, days));
      }

      if (ms >= MILLIS_IN_HOUR && showHours) {
        var hours;

        if (!showDays) {
          hours = Math.abs(this.getHours());
        } else {
          hours = Math.abs(this.getHoursPart());
        }

        ms = ms % MILLIS_IN_HOUR;
        fields.push(getText(hours === 1 ? 'hour' : 'hours', numberContext, hours));
      }

      if (ms >= MILLIS_IN_MINUTE && showMinutes) {
        var mins;

        if (!showHours) {
          mins = Math.abs(this.getMinutes());
        } else {
          mins = Math.abs(this.getMinutesPart());
        }

        ms = ms % MILLIS_IN_MINUTE;
        fields.push(getText(mins === 1 ? 'minute' : 'minutes', numberContext, mins));
      }

      if (ms > 0 && showSeconds) {
        fields.push(toSeconds(ms, showMillis, numberContext));
      }
    }

    return joinFields(fields, prefix, cx);
  };

  /**
   * Returns a friendly string indicating the time interval from the present
   * moment. For example: "right now", "a few seconds ago", "3 months from now",
   * "an hour ago".
   * @returns {Promise.<string>} a human-readable string indicating the time interval, relative to the present moment
   * @since Niagara 4.8
   */
  RelTime.prototype.toFriendlyString = function () {
    var that = this;
    return baja.lex({ module: 'baja' })
      .then(function (bajaLex) {
        var millis = Math.abs(that.getMillis());
        var tag;

        if (millis === 0) {
          return getFriendlyText(bajaLex, 'rightNow');
        }
        if (millis < MILLIS_IN_MINUTE) {
          tag = getFriendlyText(bajaLex, 'aFewSeconds');
        } else if (millis < MILLIS_IN_MINUTE * 2) {
          tag = getFriendlyText(bajaLex, 'minute');
        } else if (millis < MILLIS_IN_HOUR) {
          tag = getFriendlyText(bajaLex, 'minutes', Math.abs(that.getMinutes()));
        } else if (millis < MILLIS_IN_HOUR * 2) {
          tag = getFriendlyText(bajaLex, 'hour');
        } else if (millis < MILLIS_IN_DAY) {
          tag = getFriendlyText(bajaLex, 'hours', Math.abs(that.getHours()));
        } else if (millis < MILLIS_IN_DAY * 2) {
          tag = getFriendlyText(bajaLex, 'day');
        } else if (millis < MILLIS_IN_MONTH) {
          tag = getFriendlyText(bajaLex, 'days', Math.abs(that.getDays()));
        } else if (millis < MILLIS_IN_MONTH * 2) {
          tag = getFriendlyText(bajaLex, 'month');
        } else if (millis < MILLIS_IN_YEAR) {
          tag = getFriendlyText(bajaLex, 'months', Math.floor(Math.abs(that.getDays()) / DAYS_IN_MONTH));
        } else if (millis < MILLIS_IN_YEAR * 2) {
          tag = getFriendlyText(bajaLex, 'year');
        } else {
          tag = getFriendlyText(bajaLex, 'years', Math.floor(Math.abs(that.getDays()) / DAYS_IN_YEAR));
        }

        return getFriendlyText(bajaLex, that.getMillis() < 0 ? 'inThePast' : 'inTheFuture', tag);
      });
  };

  /**
   * @param {string} suffix retrieve from baja lexicon as `relTime.{suffix}`
   * @param {object} [cx]
   * @param {*} arg
   * @returns {Promise.<string>|string} promise if context is present, otherwise string
   */
  function getText(suffix, cx, arg) {
    if (cx) {
      return Promise.all([
        getBajaLex(),
        arg.toString(cx)
      ])
        .then(function (results) {
          return results[0].get({
            key: 'relTime.' + suffix,
            def: '{0} ' + suffix,
            args: [ results[1] ]
          });
        });
    } else {
      return arg + ' ' + suffix;
    }
  }

  /**
   * @param bajaLex the `baja` lexicon
   * @param {string} suffix retrieve from baja lexicon as `relTime.friendly.{suffix}`
   * @param {*} arg
   * @returns {Promise.<string>}
   */
  function getFriendlyText(bajaLex, suffix, arg) {
    return bajaLex.get({
      key: 'relTime.friendly.' + suffix,
      def: '{0} ' + suffix,
      args: [ arg ]
    });
  }

  /**
   * @param {Array.<Promise.<string>|string>} fields if context present will
   * resolve these as promises, otherwise as synchronous strings
   * @param {string} [prefix] prefix to prepend to result
   * @param {object} [cx]
   * @returns {Promise.<string>|string} promise if context is present, otherwise string
   */
  function joinFields(fields, prefix, cx) {
    if (cx) {
      return Promise.all([
        Promise.all(fields),
        getBajaLex()
          .then(function (lex) {
            return lex.get({ key: 'relTime.separator', def: ',' });
          })
      ])
        .then(function (results) {
          return prefix + results[0].join(results[1] + ' ');
        });
    } else {
      return prefix + fields.join(', ');
    }
  }

  /**
   * @param {number} ms number of milliseconds, truncated to seconds
   * @param {boolean} showMillis
   * @param {object} [cx]
   * @returns {Promise.<string>|string} promise if context is present, otherwise string
   */
  function toSeconds(ms, showMillis, cx) {
    var secs = ms / MILLIS_IN_SECOND;
    var millis = ms % MILLIS_IN_SECOND;
    var suffix = (secs === 1 && !millis) ? 'second' : 'seconds';

    if (cx) {
      return Promise.resolve(secs.toString({
        showSeparators: cx.showSeparators,
        precision: (millis && showMillis) ? 3 : 0
      }))
        .then(function (string) {
          return getText(suffix, cx, string);
        });
    } else {
      return getText(suffix, cx, millis ? secs.toFixed(3) : secs);
    }
  }

  /** @returns {Promise} */
  function getBajaLex() {
    return baja.lex({ module: 'baja' });
  }
  
  /**
   * Return number of milliseconds.
   *
   * @returns {Number}
   */
  RelTime.prototype.getMillis = function () {
    return this.$ms;
  };
  
  /**
   * Return the milliseconds part of this duration.
   * 
   * @returns {Number}
   */
  RelTime.prototype.getMillisPart = function () {
    return this.$ms % 1000;
  };
  
  function truncateToInteger(num) {
    return Math[num < 0 ? 'ceil' : 'floor'](num);
  }
  /**
   * Return number of seconds.
   *
   * @returns {Number}
   */
  RelTime.prototype.getSeconds = function () {
    return truncateToInteger(this.$ms / MILLIS_IN_SECOND);
  };
  
  /**
   * Return the seconds part of this duration.
   * 
   * @returns {Number}
   */
  RelTime.prototype.getSecondsPart = function () {
    return this.getSeconds() % 60;
  };
  
  /**
   * Return number of minutes.
   *
   * @returns {Number}
   */
  RelTime.prototype.getMinutes = function () {
    return truncateToInteger(this.$ms / MILLIS_IN_MINUTE);
  };
  
  /**
   * Return the minutes part of this duration.
   * 
   * @returns {Number}
   */
  RelTime.prototype.getMinutesPart = function () {
    return this.getMinutes() % 60;
  };
  
  /**
   * Return number of hours.
   *
   * @returns {Number}
   */
  RelTime.prototype.getHours = function () {
    return truncateToInteger(this.$ms / MILLIS_IN_HOUR);
  };
  
  /**
   * Return the hours part of this duration.
   * 
   * @returns {Number}
   */
  RelTime.prototype.getHoursPart = function () {
    return this.getHours() % 24;
  };
  
  /**
   * Return number of days.
   *
   * @returns {Number}
   */
  RelTime.prototype.getDays = function () {
    return truncateToInteger(this.$ms / MILLIS_IN_DAY);
  };
  
  /**
   * Return the days part of this duration.
   * 
   * @returns {Number}
   */
  RelTime.prototype.getDaysPart = function () {
    return this.getDays();
  };
  
  
  return RelTime;
});