baja/obj/EnumSet.js

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

/**
 * Defines {@link baja.EnumSet}.
 * @module baja/obj/EnumSet
 */
define([ "bajaScript/sys", 
        "bajaScript/baja/obj/EnumRange",
        "bajaScript/baja/obj/Simple",
        "bajaScript/baja/obj/objUtil",
        "Promise" ],
        function (baja, EnumRange, Simple, objUtil, Promise) {
  
  'use strict';
  
  var subclass = baja.subclass,
      callSuper = baja.callSuper,
      objectify = baja.objectify,
      strictArg = baja.strictArg,
      cacheDecode = objUtil.cacheDecode,
      cacheEncode = objUtil.cacheEncode;
  
  /**
   * Represents a `baja:EnumSet` in BajaScript.
   * 
   * An `EnumSet` contains an `EnumRange` and an array of ordinals.
   * 
   * When creating a `Simple`, always use the `make()` method instead of 
   * creating a new Object.
   *
   * @class
   * @alias baja.EnumSet
   * @extends baja.Simple
   */
  var EnumSet = function EnumSet(ordinals, range) {
    callSuper(EnumSet, this, arguments);  
    this.$ordinals = strictArg(ordinals);
    this.$range = strictArg(range, Object);
  };
  
  subclass(EnumSet, Simple);
  
  /**
   * Make an `EnumSet`. An `EnumSet` can be created using either an array of
   * ordinals (in which case the range will be set to 
   * {@link baja.EnumRange.DEFAULT}), or, to specify a range as well, an 
   * object literal with `ordinals` and `range` properties.
   *
   * @param {Object|Array<Number>} obj the object literal that holds the 
   * method's arguments (or an array of ordinals).
   * @param {Array.<Number>} [obj.ordinals] an array of ordinals.
   * @param {baja.EnumRange} [obj.range] the EnumRange to assign the EnumSet.
   * @returns {baja.EnumSet} the EnumSet.
   * 
   * @example
   *   var defaultRange = baja.EnumSet.make([0, 1, 2]);
   *   var customRange = baja.EnumSet.make({
   *     ordinals: [0, 2, 4],
   *     range: baja.EnumRange.make({
   *       ordinals: [0, 1, 2, 3, 4],
   *       tags: ['a', 'b', 'c', 'd', 'e']
   *     })
   *   });
   */
  EnumSet.make = function (obj) {    
    obj = objectify(obj, "ordinals");
    
    var ordinals = obj.ordinals,
        range = obj.range;
    
    if (ordinals === undefined) {
      ordinals = [];
    }
    
    if (range === undefined) {
      range = EnumRange.DEFAULT;
    }
   
    strictArg(ordinals, Array);
    strictArg(range, EnumRange);
    
    // optimization
    if (ordinals.length === 0 && range === EnumRange.DEFAULT) {
      return EnumSet.NULL;
    }
    
    return new EnumSet(ordinals, range);
  };
  
  /**
   * Make an `EnumSet`. Same as static method {@link baja.EnumSet.make}.
   *
   * @see baja.EnumSet.make
   */
  EnumSet.prototype.make = function (obj) {    
    return EnumSet.make.apply(EnumSet, arguments);
  };

  /**
   * Decode an `EnumSet` from a `String`.
   *
   * @method
   * @param {String} str
   * @param {Object} [params]
   * @param {Boolean} [params.unsafe=false] if set to true, this will allow
   * decodeFromString to continue. If not, decodeFromString will throw an error. This flag is for
   * internal bajaScript use only. All external implementations should use decodeAsync instead.
   * @returns {baja.EnumSet}
   */
  EnumSet.prototype.decodeFromString = cacheDecode(function decodeFromString(str, { unsafe = false } = {}) {
    if (!unsafe) { throw new Error('EnumSet#decodeAsync should be called instead to ensure all types are loaded for the decode'); }

    // parse range if specified
    var ordinals = [],
        range,
        at = str.indexOf('@');

    if (at >= 0) {
      range = EnumRange.DEFAULT.decodeFromString(str.substring(at + 1), baja.Simple.$unsafeDecode);
      str = str.substring(0, at);
    }                       

    ordinals = parseOrdinals(str);
    
    return this.make({
      ordinals: ordinals,
      range: range
    });
  });

  /**
   * @param {string} str
   * @returns {Promise.<baja.EnumSet>}
   */
  EnumSet.prototype.decodeAsync = function (str) {
    const [ o, r ] = str.split('@');
    const ordinals = parseOrdinals(o);
    const defaultRange = EnumRange.DEFAULT;

    return Promise.resolve(r ? defaultRange.decodeAsync(r) : defaultRange)
      .then((range) => this.make({ ordinals, range }));
  };
  
  /**
   * Encode an `EnumSet` to a `String`.
   *
   * @method
   * @returns {String}
   */
  EnumSet.prototype.encodeToString = cacheEncode(function encodeToString() {  
    var ordinals = this.$ordinals,
        range = this.$range,
        s = ordinals.join(',');
    
    if (range && (range !== EnumRange.DEFAULT)) {
      s += '@' + range.encodeToString();
    }
    return s;
  });
  
  /**
   * Return the data type symbol (E).
   *
   * @returns {String} data type symbol
   */
  EnumSet.prototype.getDataTypeSymbol = function () {
    return "E";
  };
  
  /**
   * Return all of the ordinals for the `EnumSet`.
   *
   * @returns {Array} an array of numbers that represents the ordinals for this EnumSet.
   */
  EnumSet.prototype.getOrdinals = function () {
    return this.$ordinals;
  };
  
  /**
   * Return the range.
   *
   * @returns {baja.EnumRange}
   */
  EnumSet.prototype.getRange = function () {
    return this.$range;
  };
  
  /**
   * Default `EnumSet` instance.
   * @type {baja.EnumSet}
   */   
  EnumSet.DEFAULT = new EnumSet([], EnumRange.DEFAULT);
  
  /**
   * NULL `EnumSet` instance.
   * @type {baja.EnumSet}
   */
  EnumSet.NULL = EnumSet.DEFAULT;

  function parseOrdinals(str) {
    if (!str.length) { return []; }

    const ordinals = [];
    const split = str.split(',');
    const count = split.length;
    for (let i = 0; i < count; i++) {
      const ordinal = parseInt(split[i], 10);
      if (isNaN(ordinal)) {
        throw new Error("Invalid ordinal: " + split[i]);
      }
      ordinals.push(ordinal);
    }
    return ordinals;
  }
  
  return EnumSet;
});