typhonjs-core-utils/src/MultiMap.js
/**
* Provides a multi-level Map implementation that mirrors the Map API except for exclusion of `forEach`.
*
* Each method takes a variable list of parameters. By not including any parameters when invoking methods of
* `MultiLevelMap` the base Map is the target. A list of comma separated parameters will index into the backing
* multi-level map structure.
*
* Errors will be thrown for methods that require a minimum number of keys including:
* `delete(1), get(1), has(1), set(2)`. In particular set requires the actual value being set in addition to any
* number of keys. Another caveat of `set()` is that if at any level of indexed keys a value is already set for the
* given key index an Error will be thrown due to the pre-existing value not being a Map.
*
* @example
* const map = new MultiMap();
*
* map.set('key1', 'key2', 1); // creates a 2nd level Map indexed by 'key1' with value '1' indexed by 'key2'.
* map.get('key1'); // returns the 2nd level Map.
* map.get('key1', 'key2'); // returns '1'; 'key1' indexes into the 2nd level map with 'key2'.
* map.has('key1', 'key2'); // is true.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
*/
export default class MultiMap
{
/**
* Initializes the MultiLevelMap
*/
constructor()
{
this._internalMap = new Map();
Object.freeze(this);
}
/**
* The `clear()` method removes all elements from a Map object. If no keys are provided then the base map is cleared.
* Subsequent keys will attempt to index into additional levels of the MultiLevelMap.
*
* @param {*} keys - A variable list of keys to index subsequent levels of the MultiLevelMap.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear
*/
clear(...keys)
{
// Clear the entire internal map if no keys are provided.
if (keys.length === 0)
{
this._internalMap.clear();
}
else
{
const map = s_FIND_CHILD_MAP(keys, this._internalMap);
if (map !== null)
{
map.clear();
}
}
}
/**
* The `delete()` method removes the specified element from a Map object. If one key is provided then the base map
* is the target. Subsequent keys will attempt to index into additional levels of the MultiLevelMap.
*
* @param {*} keys - A variable list of keys to index subsequent levels of the MultiLevelMap.
* @returns {boolean}
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete
*/
delete(...keys)
{
let returnValue = false;
const keysLength = keys.length;
if (keysLength === 0)
{
throw new Error('delete - no keys specified.');
}
else if (keysLength === 1)
{
returnValue = this._internalMap.delete(keys[0]);
}
else if (keysLength > 1)
{
const mapKeys = keys.slice(0, keysLength - 1);
const map = s_FIND_CHILD_MAP(mapKeys, this._internalMap);
if (map !== null)
{
const indexKey = keys[keysLength - 1];
returnValue = map.delete(indexKey);
}
}
return returnValue;
}
/**
* The `entries()` method returns a new Iterator object that contains the [key, value] pairs for each element in the
* Map object in insertion order. If one key is provided then the base map is the target. Subsequent keys will
* attempt to index into additional levels of the MultiLevelMap. If no Map is found then an empty iterator is
* returned.
*
* @param {*} keys - A variable list of keys to index subsequent levels of the MultiLevelMap.
* @returns {Iterator}
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
*/
entries(...keys)
{
let returnValue = s_EMPTY_ITERATOR;
if (keys.length === 0)
{
returnValue = this._internalMap.entries();
}
else
{
const map = s_FIND_CHILD_MAP(keys, this._internalMap);
if (map !== null)
{
returnValue = map.entries();
}
}
return returnValue;
}
/**
* The `get()` method returns a specified element from a Map object. If one key is provided then the base map is the
* target. Subsequent keys will attempt to index into additional levels of the MultiLevelMap. If no indexed Map is
* found then `undefined` is returned.
*
* @param {*} keys - A variable list of keys to index subsequent levels of the MultiLevelMap.
* @returns {*}
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get
*/
get(...keys)
{
let returnValue = undefined;
const keysLength = keys.length;
if (keysLength === 0)
{
throw new Error('get - no keys specified.');
}
else if (keysLength === 1)
{
returnValue = this._internalMap.get(keys[0]);
}
else if (keysLength > 1)
{
const mapKeys = keys.slice(0, keysLength - 1);
const map = s_FIND_CHILD_MAP(mapKeys, this._internalMap);
if (map !== null)
{
const indexKey = keys[keysLength - 1];
returnValue = map.get(indexKey);
}
}
return returnValue;
}
/**
* The `has()` method returns a boolean indicating whether an element with the specified key exists or not. If one
* key is provided then the base map is the target. Subsequent keys will attempt to index into additional levels of
* the MultiLevelMap. If no indexed Map is found then `false` is returned.
*
* @param {*} keys - A variable list of keys to index subsequent levels of the MultiLevelMap.
* @returns {boolean}
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has
*/
has(...keys)
{
let returnValue = false;
const keysLength = keys.length;
if (keysLength === 0)
{
throw new Error('has - no keys specified.');
}
else if (keysLength === 1)
{
returnValue = this._internalMap.has(keys[0]);
}
else if (keysLength > 1)
{
const mapKeys = keys.slice(0, keysLength - 1);
const map = s_FIND_CHILD_MAP(mapKeys, this._internalMap);
if (map !== null)
{
const indexKey = keys[keysLength - 1];
returnValue = map.has(indexKey);
}
}
return returnValue;
}
/**
* The `isMap()` method returns a boolean indicating whether specified sequence of keys resolves to an existing Map.
* If one key is provided then the base map is the target. Subsequent keys will attempt to index into additional
* levels of the MultiLevelMap. If no indexed Map is found then `false` is returned.
*
* @param {*} keys - A variable list of keys to index subsequent levels of the MultiLevelMap.
* @returns {boolean}
*/
isMap(...keys)
{
let returnValue = false;
const keysLength = keys.length;
if (keysLength === 0)
{
returnValue = true;
}
else if (keysLength > 0)
{
const map = s_FIND_CHILD_MAP(keys, this._internalMap);
returnValue = map !== null && map instanceof Map;
}
return returnValue;
}
/**
* The `keys()` method returns a new Iterator object that contains the keys for each element in the Map object in
* insertion order. If one key is provided then the base map is the target. Subsequent keys will attempt to index
* into additional levels of the MultiLevelMap. If no Map is found then an empty iterator is returned.
*
* @param {*} keys - A variable list of keys to index subsequent levels of the MultiLevelMap.
* @returns {Iterator}
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
*/
keys(...keys)
{
let returnValue = s_EMPTY_ITERATOR;
if (keys.length === 0)
{
returnValue = this._internalMap.keys();
}
else
{
const map = s_FIND_CHILD_MAP(keys, this._internalMap);
if (map !== null)
{
returnValue = map.keys();
}
}
return returnValue;
}
/**
* The `set()` method adds a new element with a specified key and value to a Map object. If one key is provided then
* the base map is the target. Subsequent keys will attempt to index into additional levels of the MultiLevelMap.
* New Maps are automatically created at each level for the given key parameters. The target Map that the value
* is added to is returned.
*
* @param {*} params - A variable list of keys to index subsequent levels of the MultiLevelMap with the last entry
* being the value to be set / added.
*
* @returns {Map|undefined}
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set
*/
set(...params)
{
let returnValue = undefined;
const keysLength = params.length;
if (keysLength === 0)
{
throw new Error('set - no params specified; needs 2 minimum.');
}
else if (keysLength === 1)
{
throw new Error('set - only 1 param specified; needs 2 minimum.');
}
else if (keysLength === 2)
{
returnValue = this._internalMap.set(params[0], params[1]);
}
else if (keysLength > 2)
{
const mapKeys = params.slice(0, keysLength - 2);
const map = s_FIND_CHILD_MAP(mapKeys, this._internalMap, true);
if (map !== null)
{
const indexKey = params[keysLength - 2];
const value = params[keysLength - 1];
returnValue = map.set(indexKey, value);
}
}
return returnValue;
}
/**
* The `size()` method returns an integer representing how many entries the Map object has. If no key is provided
* then the base map is the target. Subsequent keys will attempt to index into additional levels of the
* MultiLevelMap. If no indexed Map is found then `0` is returned.
*
* @param {*} keys - A variable list of keys to index subsequent levels of the MultiLevelMap.
* @returns {number}
*/
size(...keys)
{
let returnValue = 0;
const keysLength = keys.length;
if (keysLength === 0)
{
returnValue = this._internalMap.size;
}
else if (keysLength > 0)
{
const map = s_FIND_CHILD_MAP(keys, this._internalMap);
if (map !== null)
{
returnValue = map.size;
}
}
return returnValue;
}
/**
* The `values()` method returns a new Iterator object that contains the values for each element in the Map object in
* insertion order. If one key is provided then the base map is the target. Subsequent keys will attempt to index
* into additional levels of the MultiLevelMap. If no Map is found then an empty iterator is returned.
*
* @param {*} keys - A variable list of keys to index subsequent levels of the MultiLevelMap.
* @returns {Iterator}
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
*/
values(...keys)
{
let returnValue = s_EMPTY_ITERATOR;
if (keys.length === 0)
{
returnValue = this._internalMap.values();
}
else
{
const map = s_FIND_CHILD_MAP(keys, this._internalMap);
if (map !== null)
{
returnValue = map.values();
}
}
return returnValue;
}
}
// Private utility methods ------------------------------------------------------------------------------------------
const s_EMPTY_ITERATOR = { next: () => { return { done: true }; } };
Object.freeze(s_EMPTY_ITERATOR);
/**
* Walks the given map to find the nested child map given the keys array.
*
* @param {Array} keys - The keys to resolve in the given map.
* @param {Map} map - The map to search.
* @param {boolean} create - Create intermediate maps
* @returns {Map|null}
*/
const s_FIND_CHILD_MAP = (keys, map, create = false) =>
{
let childMap = map;
for (let cntr = 0, length = keys.length; cntr < length; cntr++)
{
const key = keys[cntr];
const nextMap = childMap.get(key);
if (nextMap instanceof Map)
{
childMap = nextMap;
}
else if (create)
{
// Child map already has a value for this key.
if (childMap.has(key))
{
throw new Error(
`Could not create child Map as a value already exists for '${key}' in keys: ${JSON.stringify(keys)}.`);
}
const newMap = new Map();
childMap.set(key, newMap);
childMap = newMap;
}
else
{
return null;
}
}
return childMap;
};