Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81152 views
1
/**
2
* Copyright 2013-2014, Facebook, Inc.
3
* All rights reserved.
4
*
5
* This source code is licensed under the BSD-style license found in the
6
* LICENSE file in the root directory of this source tree. An additional grant
7
* of patent rights can be found in the PATENTS file in the same directory.
8
*
9
* @providesModule OrderedMap
10
*/
11
12
"use strict";
13
14
var assign = require('Object.assign');
15
var invariant = require('invariant');
16
17
var PREFIX = 'key:';
18
19
/**
20
* Utility to extract a backing object from an initialization `Array`, allowing
21
* the caller to assist in resolving the unique ID for each entry via the
22
* `keyExtractor` callback. The `keyExtractor` must extract non-empty strings or
23
* numbers.
24
* @param {Array<Object!>} arr Array of items.
25
* @param {function} keyExtractor Extracts a unique key from each item.
26
* @return {Object} Map from unique key to originating value that the key was
27
* extracted from.
28
* @throws Exception if the initialization array has duplicate extracted keys.
29
*/
30
function extractObjectFromArray(arr, keyExtractor) {
31
var normalizedObj = {};
32
for (var i=0; i < arr.length; i++) {
33
var item = arr[i];
34
var key = keyExtractor(item);
35
assertValidPublicKey(key);
36
var normalizedKey = PREFIX + key;
37
invariant(
38
!(normalizedKey in normalizedObj),
39
'OrderedMap: IDs returned by the key extraction function must be unique.'
40
);
41
normalizedObj[normalizedKey] = item;
42
}
43
return normalizedObj;
44
}
45
46
/**
47
* Utility class for mappings with ordering. This class is to be used in an
48
* immutable manner. A `OrderedMap` is very much like the native JavaScript
49
* object, where keys map to values via the `get()` function. Also, like the
50
* native JavaScript object, there is an ordering associated with the mapping.
51
* This class is helpful because it eliminates many of the pitfalls that come
52
* with the native JavaScript ordered mappings. Specifically, there are
53
* inconsistencies with numeric keys in some JavaScript implementations
54
* (enumeration ordering). This class protects against those pitfalls and
55
* provides functional utilities for dealing with these `OrderedMap`s.
56
*
57
* - TODO:
58
* - orderedMergeExclusive: Merges mutually exclusive `OrderedMap`s.
59
* - mapReverse().
60
*
61
* @class {OrderedMap}
62
* @constructor {OrderedMap}
63
* @param {Object} normalizedObj Object that is known to be a defensive copy of
64
* caller supplied data. We require a defensive copy to guard against callers
65
* mutating. It is also assumed that the keys of `normalizedObj` have been
66
* normalized and do not contain any numeric-appearing strings.
67
* @param {number} computedLength The precomputed length of `_normalizedObj`
68
* keys.
69
* @private
70
*/
71
function OrderedMapImpl(normalizedObj, computedLength) {
72
this._normalizedObj = normalizedObj;
73
this._computedPositions = null;
74
this.length = computedLength;
75
}
76
77
/**
78
* Validates a "public" key - that is, one that the public facing API supplies.
79
* The key is then normalized for internal storage. In order to be considered
80
* valid, all keys must be non-empty, defined, non-null strings or numbers.
81
*
82
* @param {string?} key Validates that the key is suitable for use in a
83
* `OrderedMap`.
84
* @throws Error if key is not appropriate for use in `OrderedMap`.
85
*/
86
function assertValidPublicKey(key) {
87
invariant(
88
key !== '' && (typeof key === 'string' || typeof key === 'number'),
89
'OrderedMap: Key must be non-empty, non-null string or number.'
90
);
91
}
92
93
/**
94
* Validates that arguments to range operations are within the correct limits.
95
*
96
* @param {number} start Start of range.
97
* @param {number} length Length of range.
98
* @param {number} actualLen Actual length of range that should not be
99
* exceeded.
100
* @return {void} description
101
*/
102
function assertValidRangeIndices(start, length, actualLen) {
103
invariant(
104
typeof start === 'number' &&
105
typeof length === 'number' &&
106
length >= 0 &&
107
start >= 0 &&
108
start + length <= actualLen,
109
'OrderedMap: `mapRange` and `forEachRange` expect non-negative start and ' +
110
'length arguments within the bounds of the instance.'
111
);
112
}
113
114
/**
115
* Merges two "normalized" objects (objects who's key have been normalized) into
116
* a `OrderedMap`.
117
*
118
* @param {Object} a Object of key value pairs.
119
* @param {Object} b Object of key value pairs.
120
* @return {OrderedMap} new `OrderedMap` that results in merging `a` and `b`.
121
*/
122
function _fromNormalizedObjects(a, b) {
123
// Second optional, both must be plain JavaScript objects.
124
invariant(
125
a && a.constructor === Object && (!b || b.constructor === Object),
126
'OrderedMap: Corrupted instance of OrderedMap detected.'
127
);
128
129
var newSet = {};
130
var length = 0;
131
var key;
132
for (key in a) {
133
if (a.hasOwnProperty(key)) {
134
newSet[key] = a[key];
135
length++;
136
}
137
}
138
139
for (key in b) {
140
if (b.hasOwnProperty(key)) {
141
// Increment length if not already added via first object (a)
142
if (!(key in newSet)) {
143
length++;
144
}
145
newSet[key] = b[key];
146
}
147
}
148
return new OrderedMapImpl(newSet, length);
149
}
150
151
/**
152
* Methods for `OrderedMap` instances.
153
*
154
* @lends OrderedMap.prototype
155
* TODO: Make this data structure lazy, unify with LazyArray.
156
* TODO: Unify this with ImmutableObject - it is to be used immutably.
157
* TODO: If so, consider providing `fromObject` API.
158
* TODO: Create faster implementation of merging/mapping from original Array,
159
* without having to first create an object - simply for the sake of merging.
160
*/
161
var OrderedMapMethods = {
162
163
/**
164
* Returns whether or not a given key is present in the map.
165
*
166
* @param {string} key Valid string key to lookup membership for.
167
* @return {boolean} Whether or not `key` is a member of the map.
168
* @throws Error if provided known invalid key.
169
*/
170
has: function(key) {
171
assertValidPublicKey(key);
172
var normalizedKey = PREFIX + key;
173
return normalizedKey in this._normalizedObj;
174
},
175
176
/**
177
* Returns the object for a given key, or `undefined` if not present. To
178
* distinguish a undefined entry vs not being in the set, use `has()`.
179
*
180
* @param {string} key String key to lookup the value for.
181
* @return {Object?} Object at key `key`, or undefined if not in map.
182
* @throws Error if provided known invalid key.
183
*/
184
get: function(key) {
185
assertValidPublicKey(key);
186
var normalizedKey = PREFIX + key;
187
return this.has(key) ? this._normalizedObj[normalizedKey] : undefined;
188
},
189
190
/**
191
* Merges, appending new keys to the end of the ordering. Keys in `orderedMap`
192
* that are redundant with `this`, maintain the same ordering index that they
193
* had in `this`. This is how standard JavaScript object merging would work.
194
* If you wish to prepend a `OrderedMap` to the beginning of another
195
* `OrderedMap` then simply reverse the order of operation. This is the analog
196
* to `merge(x, y)`.
197
*
198
* @param {OrderedMap} orderedMap OrderedMap to merge onto the end.
199
* @return {OrderedMap} New OrderedMap that represents the result of the
200
* merge.
201
*/
202
merge: function(orderedMap) {
203
invariant(
204
orderedMap instanceof OrderedMapImpl,
205
'OrderedMap.merge(...): Expected an OrderedMap instance.'
206
);
207
return _fromNormalizedObjects(
208
this._normalizedObj,
209
orderedMap._normalizedObj
210
);
211
},
212
213
/**
214
* Functional map API. Returns a new `OrderedMap`.
215
*
216
* @param {Function} cb Callback to invoke for each item.
217
* @param {Object?=} context Context to invoke callback from.
218
* @return {OrderedMap} OrderedMap that results from mapping.
219
*/
220
map: function(cb, context) {
221
return this.mapRange(cb, 0, this.length, context);
222
},
223
224
/**
225
* The callback `cb` is invoked with the arguments (item, key,
226
* indexInOriginal).
227
*
228
* @param {Function} cb Determines result for each item.
229
* @param {number} start Start index of map range.
230
* @param {end} end End index of map range.
231
* @param {*!?} context Context of callback invocation.
232
* @return {OrderedMap} OrderedMap resulting from mapping the range.
233
*/
234
mapRange: function(cb, start, length, context) {
235
var thisSet = this._normalizedObj;
236
var newSet = {};
237
var i = 0;
238
assertValidRangeIndices(start, length, this.length);
239
var end = start + length - 1;
240
for (var key in thisSet) {
241
if (thisSet.hasOwnProperty(key)) {
242
if (i >= start) {
243
if (i > end) {
244
break;
245
}
246
var item = thisSet[key];
247
newSet[key] = cb.call(context, item, key.substr(PREFIX.length), i);
248
}
249
i++;
250
}
251
}
252
return new OrderedMapImpl(newSet, length);
253
},
254
255
/**
256
* Function filter API. Returns new `OrderedMap`.
257
*
258
* @param {Function} cb Callback to invoke for each item.
259
* @param {Object?=} context Context to invoke callback from.
260
* @return {OrderedMap} OrderedMap that results from filtering.
261
*/
262
filter: function(cb, context) {
263
return this.filterRange(cb, 0, this.length, context);
264
},
265
266
/**
267
* The callback `cb` is invoked with the arguments (item, key,
268
* indexInOriginal).
269
*
270
* @param {Function} cb Returns true if item should be in result.
271
* @param {number} start Start index of filter range.
272
* @param {number} end End index of map range.
273
* @param {*!?} context Context of callback invocation.
274
* @return {OrderedMap} OrderedMap resulting from filtering the range.
275
*/
276
filterRange: function(cb, start, length, context) {
277
var newSet = {};
278
var newSetLength = 0;
279
this.forEachRange(function(item, key, originalIndex) {
280
if (cb.call(context, item, key, originalIndex)) {
281
var normalizedKey = PREFIX + key;
282
newSet[normalizedKey] = item;
283
newSetLength++;
284
}
285
}, start, length);
286
return new OrderedMapImpl(newSet, newSetLength);
287
},
288
289
forEach: function(cb, context) {
290
this.forEachRange(cb, 0, this.length, context);
291
},
292
293
forEachRange: function(cb, start, length, context) {
294
assertValidRangeIndices(start, length, this.length);
295
var thisSet = this._normalizedObj;
296
var i = 0;
297
var end = start + length - 1;
298
for (var key in thisSet) {
299
if (thisSet.hasOwnProperty(key)) {
300
if (i >= start) {
301
if (i > end) {
302
break;
303
}
304
var item = thisSet[key];
305
cb.call(context, item, key.substr(PREFIX.length), i);
306
}
307
i++;
308
}
309
}
310
},
311
312
/**
313
* Even though `mapRange`/`forEachKeyRange` allow zero length mappings, we'll
314
* impose an additional restriction here that the length of mapping be greater
315
* than zero - the only reason is that there are many ways to express length
316
* zero in terms of two keys and that is confusing.
317
*/
318
mapKeyRange: function(cb, startKey, endKey, context) {
319
var startIndex = this.indexOfKey(startKey);
320
var endIndex = this.indexOfKey(endKey);
321
invariant(
322
startIndex !== undefined && endIndex !== undefined,
323
'mapKeyRange must be given keys that are present.'
324
);
325
invariant(
326
endIndex >= startIndex,
327
'OrderedMap.mapKeyRange(...): `endKey` must not come before `startIndex`.'
328
);
329
return this.mapRange(cb, startIndex, (endIndex - startIndex) + 1, context);
330
},
331
332
forEachKeyRange: function(cb, startKey, endKey, context) {
333
var startIndex = this.indexOfKey(startKey);
334
var endIndex = this.indexOfKey(endKey);
335
invariant(
336
startIndex !== undefined && endIndex !== undefined,
337
'forEachKeyRange must be given keys that are present.'
338
);
339
invariant(
340
endIndex >= startIndex,
341
'OrderedMap.forEachKeyRange(...): `endKey` must not come before ' +
342
'`startIndex`.'
343
);
344
this.forEachRange(cb, startIndex, (endIndex - startIndex) + 1, context);
345
},
346
347
/**
348
* @param {number} pos Index to search for key at.
349
* @return {string|undefined} Either the key at index `pos` or undefined if
350
* not in map.
351
*/
352
keyAtIndex: function(pos) {
353
var computedPositions = this._getOrComputePositions();
354
var keyAtPos = computedPositions.keyByIndex[pos];
355
return keyAtPos ? keyAtPos.substr(PREFIX.length) : undefined;
356
},
357
358
/**
359
* @param {string} key String key from which to find the next key.
360
* @return {string|undefined} Either the next key, or undefined if there is no
361
* next key.
362
* @throws Error if `key` is not in this `OrderedMap`.
363
*/
364
keyAfter: function(key) {
365
return this.nthKeyAfter(key, 1);
366
},
367
368
/**
369
* @param {string} key String key from which to find the preceding key.
370
* @return {string|undefined} Either the preceding key, or undefined if there
371
* is no preceding.key.
372
* @throws Error if `key` is not in this `OrderedMap`.
373
*/
374
keyBefore: function(key) {
375
return this.nthKeyBefore(key, 1);
376
},
377
378
/**
379
* @param {string} key String key from which to find a following key.
380
* @param {number} n Distance to scan forward after `key`.
381
* @return {string|undefined} Either the nth key after `key`, or undefined if
382
* there is no next key.
383
* @throws Error if `key` is not in this `OrderedMap`.
384
*/
385
nthKeyAfter: function(key, n) {
386
var curIndex = this.indexOfKey(key);
387
invariant(
388
curIndex !== undefined,
389
'OrderedMap.nthKeyAfter: The key `%s` does not exist in this instance.',
390
key
391
);
392
return this.keyAtIndex(curIndex + n);
393
},
394
395
/**
396
* @param {string} key String key from which to find a preceding key.
397
* @param {number} n Distance to scan backwards before `key`.
398
* @return {string|undefined} Either the nth key before `key`, or undefined if
399
* there is no previous key.
400
* @throws Error if `key` is not in this `OrderedMap`.
401
*/
402
nthKeyBefore: function(key, n) {
403
return this.nthKeyAfter(key, -n);
404
},
405
406
/**
407
* @param {string} key Key to find the index of.
408
* @return {number|undefined} Index of the provided key, or `undefined` if the
409
* key is not found.
410
*/
411
indexOfKey: function(key) {
412
assertValidPublicKey(key);
413
var normalizedKey = PREFIX + key;
414
var computedPositions = this._getOrComputePositions();
415
var computedPosition = computedPositions.indexByKey[normalizedKey];
416
// Just writing it this way to make it clear this is intentional.
417
return computedPosition === undefined ? undefined : computedPosition;
418
},
419
420
/**
421
* @return {Array} An ordered array of this object's values.
422
*/
423
toArray: function() {
424
var result = [];
425
var thisSet = this._normalizedObj;
426
for (var key in thisSet) {
427
if (thisSet.hasOwnProperty(key)) {
428
result.push(thisSet[key]);
429
}
430
}
431
return result;
432
},
433
434
/**
435
* Finds the key at a given position, or indicates via `undefined` that that
436
* position does not exist in the `OrderedMap`. It is appropriate to return
437
* undefined, indicating that the key doesn't exist in the `OrderedMap`
438
* because `undefined` is not ever a valid `OrderedMap` key.
439
*
440
* @private
441
* @param {number} pos Position for which we're querying the name.
442
* @return {string?} Name of the item at position `pos`, or `undefined` if
443
* there is no item at that position.
444
*/
445
_getOrComputePositions: function() {
446
// TODO: Entertain computing this at construction time in some less
447
// performance critical paths.
448
var computedPositions = this._computedPositions;
449
if (!computedPositions) {
450
this._computePositions();
451
}
452
return this._computedPositions;
453
},
454
455
/**
456
* Precomputes the index/key mapping for future lookup. Since `OrderedMap`s
457
* are immutable, there is only ever a need to perform this once.
458
* @private
459
*/
460
_computePositions: function() {
461
this._computedPositions = {
462
keyByIndex: {},
463
indexByKey: {}
464
};
465
var keyByIndex = this._computedPositions.keyByIndex;
466
var indexByKey = this._computedPositions.indexByKey;
467
var index = 0;
468
var thisSet = this._normalizedObj;
469
for (var key in thisSet) {
470
if (thisSet.hasOwnProperty(key)) {
471
keyByIndex[index] = key;
472
indexByKey[key] = index;
473
index++;
474
}
475
}
476
}
477
};
478
479
assign(OrderedMapImpl.prototype, OrderedMapMethods);
480
481
var OrderedMap = {
482
from: function(orderedMap) {
483
invariant(
484
orderedMap instanceof OrderedMapImpl,
485
'OrderedMap.from(...): Expected an OrderedMap instance.'
486
);
487
return _fromNormalizedObjects(orderedMap._normalizedObj, null);
488
},
489
490
fromArray: function(arr, keyExtractor) {
491
invariant(
492
Array.isArray(arr),
493
'OrderedMap.fromArray(...): First argument must be an array.'
494
);
495
invariant(
496
typeof keyExtractor === 'function',
497
'OrderedMap.fromArray(...): Second argument must be a function used ' +
498
'to determine the unique key for each entry.'
499
);
500
return new OrderedMapImpl(
501
extractObjectFromArray(arr, keyExtractor),
502
arr.length
503
);
504
}
505
};
506
507
module.exports = OrderedMap;
508
509