baja/sys/structures/OrderedMap.js

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

/**
 * Defines {@link baja.OrderedMap}.
 * @module baja/sys/structures/OrderedMap
 * @private
 */
define([ "bajaScript/baja/sys/BaseBajaObj",
        "bajaScript/baja/sys/bajaUtils",
        "bajaScript/baja/sys/inherit",
        "bajaScript/baja/sys/structures/FilterCursor" ], function (
         BaseBajaObj,
         bajaUtils,
         inherit,
         FilterCursor) {
  
  "use strict";
  
  var subclass = inherit.subclass,
      callSuper = inherit.callSuper,
      strictArg = bajaUtils.strictArg;
  
  /**
   * Maintains an ordered list of key/value pairs.
   * 
   * This object forms the basis of a Complex's Slot Map.
   *
   * @class
   * @alias baja.OrderedMap
   * @extends baja.BaseBajaObj
   * @private
   * @param {object} [obj] initial object literal to populate the map
   */
  const OrderedMap = function OrderedMap(obj) {
    callSuper(OrderedMap, this, arguments);
    let map, array;
    if (obj) {
      map = Object.assign({}, obj);
      array = Object.keys(obj);
    } else {
      map = {};
      array = [];
    }
    this.$map = map;
    this.$array = array;
  };
  
  subclass(OrderedMap, BaseBajaObj);

  /**
   * Assign the value to the Map with the given key.
   *
   * @param {String} key  the key used for the entry in the Map
   * @param val  the value used to store in the Map
   */
  OrderedMap.prototype.put = function (key, val) {
    if (!this.$map.hasOwnProperty(key)) {
      this.$map[key] = val;
      this.$array.push(key);
    } else {
      this.$map[key] = val;
    }
  };

  /**
   * Remove a value from the map and return it.
   *
   * @param {String} key  the key used to remove the value.
   * @returns the value removed (return null if nothing is found to be removed).
   */   
  OrderedMap.prototype.remove = function (key) {
    strictArg(key, String);
    
    var v,
        i,
        map = this.$map,
        array = this.$array;
    
    if (!map.hasOwnProperty(key)) {
      return null;
    }
    
    v = map[key];
    
    // Remove the element from the Map
    delete map[key];

    // Find and remove the key from the array
    for (i = 0; i < array.length; ++i) {
      if (array[i] === key) {
        array.splice(i, 1);
        break;
      }
    }

    return v;
  };  

  /**
   * Query the Map to see if it contains the key.
   * 
   * @param {String} key
   * @returns {Boolean} a boolean value indicating if the Map contains the key.
   */
  OrderedMap.prototype.contains = function (key) {
    strictArg(key, String);
    return this.$map.hasOwnProperty(key);
  };

  /**
   * Return the value for the key.
   * 
   * @param {String} key
   * @returns  the value for the key (return null if key is not found in Map).
   */
  OrderedMap.prototype.get = function (key) {
    var map = this.$map;
    return map.hasOwnProperty(key) ? map[key] : null;
  };

  /**
   * Rename an entry.
   *
   * @param {String} oldName  the name of the existing entry to be renamed.
   * @param {String} newName  the new name of the entry.
   * @returns {Boolean} true if the entry was successfully renamed.
   */
  OrderedMap.prototype.rename = function (oldName, newName) { 
    strictArg(oldName, String);
    strictArg(newName, String);

    if (!this.contains(oldName)) {
      return false;
    }
    if (this.contains(newName)) {
      return false;
    }

    // Get existing entry
    var entry = this.$map[oldName];
    delete this.$map[oldName];

    // Create new entry
    this.$map[newName] = entry;

    // Update array
    var i;
    for (i = 0; i < this.$array.length; ++i) {
      if (this.$array[i] === oldName) {
        this.$array[i] = newName;
        break;
      }
    }

    return true;
  };

  /**
   * Return the key's index.
   *
   * @param {String} key
   * @returns {Number} the index for the key (return -1 if key is not found in Map).
   */
  OrderedMap.prototype.getIndex = function (key) {
    strictArg(key, String);
    if (this.$map.hasOwnProperty(key)) {
      var i;
      for (i = 0; i < this.$array.length; ++i) {
        if (this.$array[i] === key) {
          return i;
        }
      }  
    }
    return -1;    
  };

  /**
   * Return the value for the index.
   *
   * @param {Number} index
   * @returns the value for the index (return null if index is not found in Map).
   */
  OrderedMap.prototype.getFromIndex = function (index) {
    strictArg(index, Number);
    var key = this.$array[index];
    if (typeof key === "string") {
      return this.$map[key];
    }
    return null;   
  };

  /**
   * Return an ordered array of keys for iteration.
   * 
   * Please note, a copy of the internal array will be returned.
   *
   * @returns {Array} an array of keys that can be used for iteration.
   */
  OrderedMap.prototype.getKeys = function () {
    // Return a copy of the array
    return this.$array.slice(0);
  };

  /**
   * Return the size of the Map.
   *
   * @returns {Number} the size of the Map.
   */
  OrderedMap.prototype.getSize = function () {
    return this.$array.length;
  };

  /**
   * Sort the Map.
   *
   * @see Array#sort
   *
   * @param {Function} sortFunc  Function used to sort the map. This definition of the Function
   *                             should be the same as the one passed into a JavaScript Array's 
   *                             sort method.
   */
  OrderedMap.prototype.sort = function (sortFunc) {
    strictArg(sortFunc, Function);
    this.$array.sort(sortFunc);
  };

  /**
   * Return a Cursor used for iteration.
   *
   * @param context used as 'this' in iteration operations
   * @param {Function} [Cursor=baja.FilterCursor] the Constructor of the 
   * `Cursor` to use for the Map.
   *     
   * @returns {baja.Cursor} Cursor used for iteration 
   */
  OrderedMap.prototype.getCursor = function (context, Cursor) {
    Cursor = Cursor || FilterCursor;
    return new Cursor(context, this);
  };
  
  return OrderedMap;
});