'use strict';
/**
* @module br/util/MapUtility
*/
var Errors = require('br/Errors');
/**
* This is a static class that never needs to be instantiated.
*
* @class
* @alias module:br/util/MapUtility
*
* @classdesc
* Utility class providing common operations on maps.
*
* <p>In the context of this class, a <code>Map</code> is considered to be anything that is an instance of
* <code>Object</code>.</p>
*/
var MapUtility = {};
/** @private */
MapUtility.m_nToStringRecursionCount = 0;
/** @private */
MapUtility.m_pMapsProcessedByToString = [];
/**
* Returns <code>true</code> if the given map is empty (has no enumerable properties), and <code>false</code> otherwise.
*
* @param {Object} srcMap The map that may or may not be empty.
* @returns {boolean}
*/
MapUtility.isEmpty = function(srcMap) {
for (var key in srcMap) {
if (srcMap.hasOwnProperty(key)) {
return false;
}
}
return true;
};
/**
* Returns the number of enumerable items within the given map.
*
* <p>If you find yourself using this method you should consider whether a map is the correct data structure.</p>
*
* @param {Object} srcMap The map the size is required for.
* @returns {number} The number of items within the map.
*/
MapUtility.size = function(srcMap) {
var size = 0;
for (var key in srcMap) {
if (srcMap.hasOwnProperty(key)) {
size += 1;
}
}
return size;
};
/**
* Returns an array containing the values obtained by iterating the given map object.
*
* @param {Object} srcMap The map to iterate
* @returns {Array} an array of all the enumerable values in the map.
*/
MapUtility.valuesToArray = function(srcMap) {
var values = [];
for (var key in srcMap) {
if (srcMap.hasOwnProperty(key)) {
values.push(srcMap[key]);
}
}
return values;
};
/**
* Add the items within the given array to the map, using each item as a key pointing to the boolean value
* <code>true</code>. This will overwrite the value for a key that is already defined within the specified map.
*
* <p>As an example, the array <code>['foo', 'bar']</code> would be converted to the map
* <code>{foo:true, bar:true}</code>.</p>
*
* <p>This method is useful in that it allows a number of arrays to be condensed into a list of the unique values
* within this set of arrays. Once all the arrays have been added, then the {@link #keysToArray} method may be used
* to convert this list of unique entries back to an array.</p>
*
* @see #removeArrayFromMap
*
* @param {Object} tgtMap The map to add the items to. May not be null or undefined
* @param {Array} keys The array that contains the map keys to add. May not be null or undefined.
*
* @returns {Object} the passed map.
*/
MapUtility.addArrayToMap = function(tgtMap, keys) {
for (var i = 0, len = keys.length; i < len; ++i) {
tgtMap[keys[i]] = true;
}
return tgtMap;
};
/**
* Remove all entries within the specified map, whose keys are contained within the given array.
* Keys included in the array that do not exist within the map will be ignored.
*
* @see #addArrayToMap
*
* @param {Object} tgtMap The map to remove the items from.
* @param {Array} keys The array of keys to remove from the map.
* @returns {Object} A map containing all name/value pairs that have been removed from the specified map.
*/
MapUtility.removeArrayFromMap = function(tgtMap, keys) {
var deleted = {},
key, value;
for(var i = 0, len = keys.length; i < len; ++i) {
key = keys[i];
value = tgtMap[key];
if (delete tgtMap[key]) {
// if delete is successful then add the deleted value to the map of deleted entries
deleted[key] = value;
}
}
return deleted;
};
/**
* Returns a string representation of the map in the form:
*
* <pre>
* map#1{ a: 1, b: 2, c: 3, ... }
* </pre>
*
* <p>The values contained within the map will be returned as per the value of their <code>toString()</code> method.
* Another map will be displayed as <code>[object Object]</code>, as will any object that does not explicitly implement
* or inherit a <code>toString()</code> method.</p>
*
* <p>This method can be invoked multiple times from within the same callstack, such that if an object contained within
* the map implements a <code>toString()</code> method that calls it, there can be no infinite recursion. For example,
* if the object contained a reference to another map, the output would be of the form:</p>
*
* <pre>
* map#1{ obj: myObject<map#2{ x: 24, y: 25, z: 26 }> }
* </pre>
*
* <p>Whilst if the object contains a circular reference back to the map, the output will be of the form:</p>
*
* <pre>
* map#1{ obj: myObject<map#1<see-earlier-definition>> }
* </pre>
*
* <i>Warning: The output from this method will become unreliable if the <code>toString()</code> method of any of the
* values contained within the specified map throw an exception, however it is strongly advised that
* <code>toString()</code> methods should never throw exceptions.</i>
*
* @param {Object} srcMap The map to be converted to a String.
*
* @returns {String} A string representation of the specified map.
*/
MapUtility.toString = function(srcMap) {
var serialized = '',
mapIdx = this._getMapIndex(srcMap);
// has this map been already been processed
if (mapIdx !== -1) {
serialized += 'map#'+ mapIdx + '{<see-earlier-definition>}';
} else {
this.m_nToStringRecursionCount += 1;
this.m_pMapsProcessedByToString.push(srcMap);
mapIdx = this.m_pMapsProcessedByToString.length;
serialized += 'map#'+ mapIdx + '{';
var isFirst = true;
for (var key in srcMap) {
serialized += (isFirst ? '' : ',') + ' ' + key + ': ' + srcMap[key];
if (isFirst === true) {
isFirst = false;
}
}
serialized += ' }';
this.m_nToStringRecursionCount -= 1;
if (this.m_nToStringRecursionCount === 0) {
// toString() has completed, clear down all the temporary flags that were set on the maps
// that were processed by this method
for (var i = 0, len = this.m_pMapsProcessedByToString.length; i < len; ++i) {
delete (this.m_pMapsProcessedByToString[i].constructor.toStringMapIndex);
}
// clear down the array
this.m_pMapsProcessedByToString = [];
}
}
return serialized;
};
/**
* Gets the index that was associated with the specified map when it was output by the {@link #toString} method
* during the current callstack invocation, or returns a code indicating that the map has not been processed
* previously.
*
* @private
* @param {Object} srcMap The map the index is required for.
* @returns {Number} The index of the map, or <code>-1</code> if this map has not been processed before.
*/
MapUtility._getMapIndex = function(srcMap) {
var index = -1;
for (var i = 0, len = this.m_pMapsProcessedByToString.length; i < len; ++i) {
if (this.m_pMapsProcessedByToString[i] === srcMap) {
// first map has the index 1
index = i + 1;
break;
}
}
return index;
};
/**
* Merges all of the maps specified in the array into a new map.
*
* <p>The default behaviour of this method is to throw an exception if two maps contain the same key, however these
* duplicates can be ignored by setting the optional <code>overwriteDuplicateKeys</code> argument to
* <code>true</code>. In this case the value of the key within the merged map will be that of the last map to
* contain the key. For example, merging <code>[ { a: '1' }, { a: '2' } ]</code> would result in the map
* <code>{ a: '2' }</code>.</p>
*
* @param {Array} mergeMapArr An array of all the maps to be merged.
* @param {boolean} overwriteDuplicateKeys (Optional) Flag that can be set to force this method to ignore duplicate
* keys and overwrite their values. If omitted this argument defaults to <code>false</code>.
* @param {boolean} throwOnDuplicateKey (Optional) Defaults to <code>true</code>. Indicates if an exception
* should be thrown if a duplicate value is found and the method is not to overwrite duplicates. This should be
* used if the original values should be preserved and not overwritten. If <code>overwriteDuplicateKeys</code> is set
* to <code>true</code> then this parameter is ignored.
* @param {boolean} isDeepCopy (Optional) Defaults to <code>false</code>, shallow copy. Identifies if map objects
* should have deep copy applied to them.
*
* @returns {Object} A new map containing the merged key/value pairs from each of the specified maps.
*
* @throws {br.util.Error} if one or more of the contents of the maps to merge array is not a <code>Map</code>, or
* if any duplicate keys are found and the <code>overwriteDuplicateKeys</code> argument is <code>false</code>.
*/
MapUtility.mergeMaps = function(mergeMapArr, overwriteDuplicateKeys, throwOnDuplicateKey, isDeepCopy) {
throwOnDuplicateKey = (typeof throwOnDuplicateKey === 'undefined' ? true : throwOnDuplicateKey);
isDeepCopy = (typeof isDeepCopy === 'undefined' ? false : isDeepCopy);
var currMap,
merged = {};
for (var i = 0, len = mergeMapArr.length; i < len; ++i) {
currMap = mergeMapArr[i];
if (typeof currMap !== 'object' || currMap === null) {
throw new Errors.InvalidParametersError('Failed to merge maps; one of the specified maps was of an invalid type');
}
for (var key in currMap) {
if (overwriteDuplicateKeys !== true && typeof merged[key] !== 'undefined') {
if (throwOnDuplicateKey) {
throw new Errors.InvalidParametersError('Failed to merge maps due to a duplicate key \'' + key + '\': conflicting values \'' + merged[key] + '\'/\'' + currMap[key] + '\'');
}
// do not overwrite the value, keep the original and continue with next value
continue;
}
if (typeof merged[key] === 'object' && typeof currMap[key] == 'object' && isDeepCopy ) {
merged[key] = this.mergeMaps([currMap[key], merged[key]], throwOnDuplicateKey, throwOnDuplicateKey, true);
} else if (!merged[key] && typeof currMap[key] == 'object' && isDeepCopy ) {
merged[key] = this.copy(currMap[key], {}, true );
} else {
// shallow copy
merged[key] = currMap[key];
}
}
}
return merged;
};
/**
* Converts a map to its inverse, which has keys based on the original map's values, and vice-versa.
* @private
*/
MapUtility.invert = function(srcMap) {
var inverted = {};
for (var key in srcMap) {
inverted[srcMap[key]] = key;
}
return inverted;
};
// TODO: determine whether copy() and mergeMaps() be combined into a more useful method
/**
* Creates a shallow copy of the supplied map. If the destination map is supplied, then it adds the map values onto
* the destination map.
*
* @private
* @param {Object} srcMap
* @param {Object} tgtMap (optional)
* @param {Boolean} isDeepCopy indicates whether a deep copy will occur on the Map.
*/
MapUtility.copy = function(srcMap, tgtMap, isDeepCopy) {
tgtMap = tgtMap || {};
for (var key in srcMap) {
if (isDeepCopy && typeof srcMap[key] === 'object') {
tgtMap[key] = this.deepClone(srcMap[key]);
} else {
tgtMap[key] = srcMap[key];
}
}
return tgtMap;
};
/**
* Creates a shallow clone of the supplied map. Map references are copied one level deep.
*
* @param {Object} srcMap The map to clone.
*
* @returns {Object} A shallow clone of the map.
*/
MapUtility.clone = function(srcMap) {
var clone = {};
for (var key in srcMap) {
clone[key] = srcMap[key];
}
return clone;
};
/**
* Creates a deep clone of the supplied map/array. Copies the full depth of the data structure recursively.
*
* @param {Object} src The object to clone.
*
* @returns {Object} A deep clone of the object.
*/
MapUtility.deepClone = function(src) {
if (Array.isArray(src)) {
return this.deepCloneArray(src);
} else if (typeof src === 'object'){
return this.deepCloneMap(src);
} else {
return src;
}
};
/**
* Use deepClone to copy your map
* @private
* @param srcMap Map to clone
* @returns {Object} A deep clone of the input srcMap
*/
MapUtility.deepCloneMap = function(srcMap) {
var clone = {};
for (var key in srcMap) {
clone[key] = this.deepClone(srcMap[key]);
}
return clone;
};
/**
* Use deepClone to copy your Array
* @private
* @param srcArray Array to clone
* @returns {Array} A deep clone of the input Array
*/
MapUtility.deepCloneArray = function(srcArray) {
return srcArray.map(function(item) {
return this.deepClone(item);
}, this);
};
/**
* Helper method to check if parameter values passed in to methods are members of the enumerations they are meant to
* be. BEWARE: The check is whether <code>item</code> is a value on a member of the object, such as an entry in an
* Array.
*
* @private
* @param item exact instance that must be equal(===) to one of the members.
* @param srcObj the object that will have its members checked.
*/
MapUtility.isMemberValueOf = function(item, srcObj) {
var isMember = false,
srcItem;
for (var key in srcObj) {
srcItem = srcObj[key];
if (item === srcItem) {
isMember = true;
break;
}
}
return isMember;
};
/**
* Returns true if the source map contains all the keys of the given map.
*
* @param {Object} tgtMap The map you are checking
* @param {Object} srcMap The map you are using the check against
* @returns {Boolean}
*/
MapUtility.hasAllKeys = function(tgtMap, srcMap) {
return Object.keys(srcMap).every(function(key) {
return typeof tgtMap[key] !== 'undefined';
});
};
module.exports = MapUtility;