react / react-0.13.3 / examples / basic-commonjs / node_modules / reactify / node_modules / react-tools / src / utils / OrderedMap.js
81152 views/**1* Copyright 2013-2014, Facebook, Inc.2* All rights reserved.3*4* This source code is licensed under the BSD-style license found in the5* LICENSE file in the root directory of this source tree. An additional grant6* of patent rights can be found in the PATENTS file in the same directory.7*8* @providesModule OrderedMap9*/1011"use strict";1213var assign = require('Object.assign');14var invariant = require('invariant');1516var PREFIX = 'key:';1718/**19* Utility to extract a backing object from an initialization `Array`, allowing20* the caller to assist in resolving the unique ID for each entry via the21* `keyExtractor` callback. The `keyExtractor` must extract non-empty strings or22* numbers.23* @param {Array<Object!>} arr Array of items.24* @param {function} keyExtractor Extracts a unique key from each item.25* @return {Object} Map from unique key to originating value that the key was26* extracted from.27* @throws Exception if the initialization array has duplicate extracted keys.28*/29function extractObjectFromArray(arr, keyExtractor) {30var normalizedObj = {};31for (var i=0; i < arr.length; i++) {32var item = arr[i];33var key = keyExtractor(item);34assertValidPublicKey(key);35var normalizedKey = PREFIX + key;36invariant(37!(normalizedKey in normalizedObj),38'OrderedMap: IDs returned by the key extraction function must be unique.'39);40normalizedObj[normalizedKey] = item;41}42return normalizedObj;43}4445/**46* Utility class for mappings with ordering. This class is to be used in an47* immutable manner. A `OrderedMap` is very much like the native JavaScript48* object, where keys map to values via the `get()` function. Also, like the49* native JavaScript object, there is an ordering associated with the mapping.50* This class is helpful because it eliminates many of the pitfalls that come51* with the native JavaScript ordered mappings. Specifically, there are52* inconsistencies with numeric keys in some JavaScript implementations53* (enumeration ordering). This class protects against those pitfalls and54* provides functional utilities for dealing with these `OrderedMap`s.55*56* - TODO:57* - orderedMergeExclusive: Merges mutually exclusive `OrderedMap`s.58* - mapReverse().59*60* @class {OrderedMap}61* @constructor {OrderedMap}62* @param {Object} normalizedObj Object that is known to be a defensive copy of63* caller supplied data. We require a defensive copy to guard against callers64* mutating. It is also assumed that the keys of `normalizedObj` have been65* normalized and do not contain any numeric-appearing strings.66* @param {number} computedLength The precomputed length of `_normalizedObj`67* keys.68* @private69*/70function OrderedMapImpl(normalizedObj, computedLength) {71this._normalizedObj = normalizedObj;72this._computedPositions = null;73this.length = computedLength;74}7576/**77* Validates a "public" key - that is, one that the public facing API supplies.78* The key is then normalized for internal storage. In order to be considered79* valid, all keys must be non-empty, defined, non-null strings or numbers.80*81* @param {string?} key Validates that the key is suitable for use in a82* `OrderedMap`.83* @throws Error if key is not appropriate for use in `OrderedMap`.84*/85function assertValidPublicKey(key) {86invariant(87key !== '' && (typeof key === 'string' || typeof key === 'number'),88'OrderedMap: Key must be non-empty, non-null string or number.'89);90}9192/**93* Validates that arguments to range operations are within the correct limits.94*95* @param {number} start Start of range.96* @param {number} length Length of range.97* @param {number} actualLen Actual length of range that should not be98* exceeded.99* @return {void} description100*/101function assertValidRangeIndices(start, length, actualLen) {102invariant(103typeof start === 'number' &&104typeof length === 'number' &&105length >= 0 &&106start >= 0 &&107start + length <= actualLen,108'OrderedMap: `mapRange` and `forEachRange` expect non-negative start and ' +109'length arguments within the bounds of the instance.'110);111}112113/**114* Merges two "normalized" objects (objects who's key have been normalized) into115* a `OrderedMap`.116*117* @param {Object} a Object of key value pairs.118* @param {Object} b Object of key value pairs.119* @return {OrderedMap} new `OrderedMap` that results in merging `a` and `b`.120*/121function _fromNormalizedObjects(a, b) {122// Second optional, both must be plain JavaScript objects.123invariant(124a && a.constructor === Object && (!b || b.constructor === Object),125'OrderedMap: Corrupted instance of OrderedMap detected.'126);127128var newSet = {};129var length = 0;130var key;131for (key in a) {132if (a.hasOwnProperty(key)) {133newSet[key] = a[key];134length++;135}136}137138for (key in b) {139if (b.hasOwnProperty(key)) {140// Increment length if not already added via first object (a)141if (!(key in newSet)) {142length++;143}144newSet[key] = b[key];145}146}147return new OrderedMapImpl(newSet, length);148}149150/**151* Methods for `OrderedMap` instances.152*153* @lends OrderedMap.prototype154* TODO: Make this data structure lazy, unify with LazyArray.155* TODO: Unify this with ImmutableObject - it is to be used immutably.156* TODO: If so, consider providing `fromObject` API.157* TODO: Create faster implementation of merging/mapping from original Array,158* without having to first create an object - simply for the sake of merging.159*/160var OrderedMapMethods = {161162/**163* Returns whether or not a given key is present in the map.164*165* @param {string} key Valid string key to lookup membership for.166* @return {boolean} Whether or not `key` is a member of the map.167* @throws Error if provided known invalid key.168*/169has: function(key) {170assertValidPublicKey(key);171var normalizedKey = PREFIX + key;172return normalizedKey in this._normalizedObj;173},174175/**176* Returns the object for a given key, or `undefined` if not present. To177* distinguish a undefined entry vs not being in the set, use `has()`.178*179* @param {string} key String key to lookup the value for.180* @return {Object?} Object at key `key`, or undefined if not in map.181* @throws Error if provided known invalid key.182*/183get: function(key) {184assertValidPublicKey(key);185var normalizedKey = PREFIX + key;186return this.has(key) ? this._normalizedObj[normalizedKey] : undefined;187},188189/**190* Merges, appending new keys to the end of the ordering. Keys in `orderedMap`191* that are redundant with `this`, maintain the same ordering index that they192* had in `this`. This is how standard JavaScript object merging would work.193* If you wish to prepend a `OrderedMap` to the beginning of another194* `OrderedMap` then simply reverse the order of operation. This is the analog195* to `merge(x, y)`.196*197* @param {OrderedMap} orderedMap OrderedMap to merge onto the end.198* @return {OrderedMap} New OrderedMap that represents the result of the199* merge.200*/201merge: function(orderedMap) {202invariant(203orderedMap instanceof OrderedMapImpl,204'OrderedMap.merge(...): Expected an OrderedMap instance.'205);206return _fromNormalizedObjects(207this._normalizedObj,208orderedMap._normalizedObj209);210},211212/**213* Functional map API. Returns a new `OrderedMap`.214*215* @param {Function} cb Callback to invoke for each item.216* @param {Object?=} context Context to invoke callback from.217* @return {OrderedMap} OrderedMap that results from mapping.218*/219map: function(cb, context) {220return this.mapRange(cb, 0, this.length, context);221},222223/**224* The callback `cb` is invoked with the arguments (item, key,225* indexInOriginal).226*227* @param {Function} cb Determines result for each item.228* @param {number} start Start index of map range.229* @param {end} end End index of map range.230* @param {*!?} context Context of callback invocation.231* @return {OrderedMap} OrderedMap resulting from mapping the range.232*/233mapRange: function(cb, start, length, context) {234var thisSet = this._normalizedObj;235var newSet = {};236var i = 0;237assertValidRangeIndices(start, length, this.length);238var end = start + length - 1;239for (var key in thisSet) {240if (thisSet.hasOwnProperty(key)) {241if (i >= start) {242if (i > end) {243break;244}245var item = thisSet[key];246newSet[key] = cb.call(context, item, key.substr(PREFIX.length), i);247}248i++;249}250}251return new OrderedMapImpl(newSet, length);252},253254/**255* Function filter API. Returns new `OrderedMap`.256*257* @param {Function} cb Callback to invoke for each item.258* @param {Object?=} context Context to invoke callback from.259* @return {OrderedMap} OrderedMap that results from filtering.260*/261filter: function(cb, context) {262return this.filterRange(cb, 0, this.length, context);263},264265/**266* The callback `cb` is invoked with the arguments (item, key,267* indexInOriginal).268*269* @param {Function} cb Returns true if item should be in result.270* @param {number} start Start index of filter range.271* @param {number} end End index of map range.272* @param {*!?} context Context of callback invocation.273* @return {OrderedMap} OrderedMap resulting from filtering the range.274*/275filterRange: function(cb, start, length, context) {276var newSet = {};277var newSetLength = 0;278this.forEachRange(function(item, key, originalIndex) {279if (cb.call(context, item, key, originalIndex)) {280var normalizedKey = PREFIX + key;281newSet[normalizedKey] = item;282newSetLength++;283}284}, start, length);285return new OrderedMapImpl(newSet, newSetLength);286},287288forEach: function(cb, context) {289this.forEachRange(cb, 0, this.length, context);290},291292forEachRange: function(cb, start, length, context) {293assertValidRangeIndices(start, length, this.length);294var thisSet = this._normalizedObj;295var i = 0;296var end = start + length - 1;297for (var key in thisSet) {298if (thisSet.hasOwnProperty(key)) {299if (i >= start) {300if (i > end) {301break;302}303var item = thisSet[key];304cb.call(context, item, key.substr(PREFIX.length), i);305}306i++;307}308}309},310311/**312* Even though `mapRange`/`forEachKeyRange` allow zero length mappings, we'll313* impose an additional restriction here that the length of mapping be greater314* than zero - the only reason is that there are many ways to express length315* zero in terms of two keys and that is confusing.316*/317mapKeyRange: function(cb, startKey, endKey, context) {318var startIndex = this.indexOfKey(startKey);319var endIndex = this.indexOfKey(endKey);320invariant(321startIndex !== undefined && endIndex !== undefined,322'mapKeyRange must be given keys that are present.'323);324invariant(325endIndex >= startIndex,326'OrderedMap.mapKeyRange(...): `endKey` must not come before `startIndex`.'327);328return this.mapRange(cb, startIndex, (endIndex - startIndex) + 1, context);329},330331forEachKeyRange: function(cb, startKey, endKey, context) {332var startIndex = this.indexOfKey(startKey);333var endIndex = this.indexOfKey(endKey);334invariant(335startIndex !== undefined && endIndex !== undefined,336'forEachKeyRange must be given keys that are present.'337);338invariant(339endIndex >= startIndex,340'OrderedMap.forEachKeyRange(...): `endKey` must not come before ' +341'`startIndex`.'342);343this.forEachRange(cb, startIndex, (endIndex - startIndex) + 1, context);344},345346/**347* @param {number} pos Index to search for key at.348* @return {string|undefined} Either the key at index `pos` or undefined if349* not in map.350*/351keyAtIndex: function(pos) {352var computedPositions = this._getOrComputePositions();353var keyAtPos = computedPositions.keyByIndex[pos];354return keyAtPos ? keyAtPos.substr(PREFIX.length) : undefined;355},356357/**358* @param {string} key String key from which to find the next key.359* @return {string|undefined} Either the next key, or undefined if there is no360* next key.361* @throws Error if `key` is not in this `OrderedMap`.362*/363keyAfter: function(key) {364return this.nthKeyAfter(key, 1);365},366367/**368* @param {string} key String key from which to find the preceding key.369* @return {string|undefined} Either the preceding key, or undefined if there370* is no preceding.key.371* @throws Error if `key` is not in this `OrderedMap`.372*/373keyBefore: function(key) {374return this.nthKeyBefore(key, 1);375},376377/**378* @param {string} key String key from which to find a following key.379* @param {number} n Distance to scan forward after `key`.380* @return {string|undefined} Either the nth key after `key`, or undefined if381* there is no next key.382* @throws Error if `key` is not in this `OrderedMap`.383*/384nthKeyAfter: function(key, n) {385var curIndex = this.indexOfKey(key);386invariant(387curIndex !== undefined,388'OrderedMap.nthKeyAfter: The key `%s` does not exist in this instance.',389key390);391return this.keyAtIndex(curIndex + n);392},393394/**395* @param {string} key String key from which to find a preceding key.396* @param {number} n Distance to scan backwards before `key`.397* @return {string|undefined} Either the nth key before `key`, or undefined if398* there is no previous key.399* @throws Error if `key` is not in this `OrderedMap`.400*/401nthKeyBefore: function(key, n) {402return this.nthKeyAfter(key, -n);403},404405/**406* @param {string} key Key to find the index of.407* @return {number|undefined} Index of the provided key, or `undefined` if the408* key is not found.409*/410indexOfKey: function(key) {411assertValidPublicKey(key);412var normalizedKey = PREFIX + key;413var computedPositions = this._getOrComputePositions();414var computedPosition = computedPositions.indexByKey[normalizedKey];415// Just writing it this way to make it clear this is intentional.416return computedPosition === undefined ? undefined : computedPosition;417},418419/**420* @return {Array} An ordered array of this object's values.421*/422toArray: function() {423var result = [];424var thisSet = this._normalizedObj;425for (var key in thisSet) {426if (thisSet.hasOwnProperty(key)) {427result.push(thisSet[key]);428}429}430return result;431},432433/**434* Finds the key at a given position, or indicates via `undefined` that that435* position does not exist in the `OrderedMap`. It is appropriate to return436* undefined, indicating that the key doesn't exist in the `OrderedMap`437* because `undefined` is not ever a valid `OrderedMap` key.438*439* @private440* @param {number} pos Position for which we're querying the name.441* @return {string?} Name of the item at position `pos`, or `undefined` if442* there is no item at that position.443*/444_getOrComputePositions: function() {445// TODO: Entertain computing this at construction time in some less446// performance critical paths.447var computedPositions = this._computedPositions;448if (!computedPositions) {449this._computePositions();450}451return this._computedPositions;452},453454/**455* Precomputes the index/key mapping for future lookup. Since `OrderedMap`s456* are immutable, there is only ever a need to perform this once.457* @private458*/459_computePositions: function() {460this._computedPositions = {461keyByIndex: {},462indexByKey: {}463};464var keyByIndex = this._computedPositions.keyByIndex;465var indexByKey = this._computedPositions.indexByKey;466var index = 0;467var thisSet = this._normalizedObj;468for (var key in thisSet) {469if (thisSet.hasOwnProperty(key)) {470keyByIndex[index] = key;471indexByKey[key] = index;472index++;473}474}475}476};477478assign(OrderedMapImpl.prototype, OrderedMapMethods);479480var OrderedMap = {481from: function(orderedMap) {482invariant(483orderedMap instanceof OrderedMapImpl,484'OrderedMap.from(...): Expected an OrderedMap instance.'485);486return _fromNormalizedObjects(orderedMap._normalizedObj, null);487},488489fromArray: function(arr, keyExtractor) {490invariant(491Array.isArray(arr),492'OrderedMap.fromArray(...): First argument must be an array.'493);494invariant(495typeof keyExtractor === 'function',496'OrderedMap.fromArray(...): Second argument must be a function used ' +497'to determine the unique key for each entry.'498);499return new OrderedMapImpl(500extractObjectFromArray(arr, keyExtractor),501arr.length502);503}504};505506module.exports = OrderedMap;507508509