refactor(core): Add array modification functions. (#34804)
This change introduces several functions for manipulating items in an array in an efficient (binary search) way. - `arraySplice` a faster version of `Array.splice()`. - `arrayInsert` a faster version of `Array.splice(index, 0, value)`. - `arrayInsert2` a faster version of `Array.splice(index, 0, value1, value2)`. - `arrayInsertSorted` a way to insert a value into sorted list. - `arrayRemoveSorted` a way to remove a value from a sorted list. - `arrayIndexOfSorted` a way to find a value in a sorted list. - `ArrayMap` Efficient implementation of `Map` as an `Array`. - `arrayMapSet`, `arrayMapGet`, `arrayMapIndexOf`, and `arrayMapDelete` for manipulating `ArrayMap`s. PR Close #34804
This commit is contained in:
parent
5aabe93abe
commit
4c7087ccdb
|
@ -6,6 +6,8 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {assertLessThanOrEqual} from './assert';
|
||||
|
||||
/**
|
||||
* Equivalent to ES6 spread, add each item to an array.
|
||||
*
|
||||
|
@ -71,3 +73,267 @@ export function newArray<T>(size: number, value?: T): T[] {
|
|||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from array (Same as `Array.splice()` but faster.)
|
||||
*
|
||||
* `Array.splice()` is not as fast because it has to allocate an array for the elements which were
|
||||
* removed. This causes memory pressure and slows down code when most of the time we don't
|
||||
* care about the deleted items array.
|
||||
*
|
||||
* https://jsperf.com/fast-array-splice (About 20x faster)
|
||||
*
|
||||
* @param array Array to splice
|
||||
* @param index Index of element in array to remove.
|
||||
* @param count Number of items to remove.
|
||||
*/
|
||||
export function arraySplice(array: any[], index: number, count: number): void {
|
||||
const length = array.length - count;
|
||||
while (index < length) {
|
||||
array[index] = array[index + count];
|
||||
index++;
|
||||
}
|
||||
while (count--) {
|
||||
array.pop(); // shrink the array
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `Array.splice(index, 0, value)` but faster.
|
||||
*
|
||||
* `Array.splice()` is not fast because it has to allocate an array for the elements which were
|
||||
* removed. This causes memory pressure and slows down code when most of the time we don't
|
||||
* care about the deleted items array.
|
||||
*
|
||||
* https://jsperf.com/fast-array-splice (About 20x faster)
|
||||
*
|
||||
* @param array Array to splice.
|
||||
* @param index Index in array where the `value` should be added.
|
||||
* @param value Value to add to array.
|
||||
*/
|
||||
export function arrayInsert(array: any[], index: number, value: any): void {
|
||||
ngDevMode && assertLessThanOrEqual(index, array.length, 'Can\'t insert past array end.');
|
||||
let end = array.length;
|
||||
while (end > index) {
|
||||
const previousEnd = end - 1;
|
||||
array[end] = array[previousEnd];
|
||||
end = previousEnd;
|
||||
}
|
||||
array[index] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `Array.splice2(index, 0, value1, value2)` but faster.
|
||||
*
|
||||
* `Array.splice()` is not fast because it has to allocate an array for the elements which were
|
||||
* removed. This causes memory pressure and slows down code when most of the time we don't
|
||||
* care about the deleted items array.
|
||||
*
|
||||
* https://jsperf.com/fast-array-splice (About 20x faster)
|
||||
*
|
||||
* @param array Array to splice.
|
||||
* @param index Index in array where the `value` should be added.
|
||||
* @param value1 Value to add to array.
|
||||
* @param value2 Value to add to array.
|
||||
*/
|
||||
export function arrayInsert2(array: any[], index: number, value1: any, value2: any): void {
|
||||
ngDevMode && assertLessThanOrEqual(index, array.length, 'Can\'t insert past array end.');
|
||||
let end = array.length;
|
||||
if (end == index) {
|
||||
// inserting at the end.
|
||||
array.push(value1, value2);
|
||||
} else if (end === 1) {
|
||||
// corner case when we have less items in array than we have items to insert.
|
||||
array.push(value2, array[0]);
|
||||
array[0] = value1;
|
||||
} else {
|
||||
end--;
|
||||
array.push(array[end - 1], array[end]);
|
||||
while (end > index) {
|
||||
const previousEnd = end - 2;
|
||||
array[end] = array[previousEnd];
|
||||
end--;
|
||||
}
|
||||
array[index] = value1;
|
||||
array[index + 1] = value2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a `value` into an `array` so that the array remains sorted.
|
||||
*
|
||||
* NOTE:
|
||||
* - Duplicates are not allowed, and are ignored.
|
||||
* - This uses binary search algorithm for fast inserts.
|
||||
*
|
||||
* @param array A sorted array to insert into.
|
||||
* @param value The value to insert.
|
||||
* @returns index of the inserted value.
|
||||
*/
|
||||
export function arrayInsertSorted(array: string[], value: string): number {
|
||||
let index = arrayIndexOfSorted(array, value);
|
||||
if (index < 0) {
|
||||
// if we did not find it insert it.
|
||||
index = ~index;
|
||||
arrayInsert(array, index, value);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove `value` from a sorted `array`.
|
||||
*
|
||||
* NOTE:
|
||||
* - This uses binary search algorithm for fast removals.
|
||||
*
|
||||
* @param array A sorted array to remove from.
|
||||
* @param value The value to remove.
|
||||
* @returns index of the removed value.
|
||||
* - positive index if value found and removed.
|
||||
* - negative index if value not found. (`~index` to get the value where it should have been
|
||||
* inserted)
|
||||
*/
|
||||
export function arrayRemoveSorted(array: string[], value: string): number {
|
||||
const index = arrayIndexOfSorted(array, value);
|
||||
if (index >= 0) {
|
||||
arraySplice(array, index, 1);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get an index of an `value` in a sorted `array`.
|
||||
*
|
||||
* NOTE:
|
||||
* - This uses binary search algorithm for fast removals.
|
||||
*
|
||||
* @param array A sorted array to binary search.
|
||||
* @param value The value to look for.
|
||||
* @returns index of the value.
|
||||
* - positive index if value found.
|
||||
* - negative index if value not found. (`~index` to get the value where it should have been
|
||||
* located)
|
||||
*/
|
||||
export function arrayIndexOfSorted(array: string[], value: string): number {
|
||||
return _arrayIndexOfSorted(array, value, 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `ArrayMap` is an array where even positions contain keys and odd positions contain values.
|
||||
*
|
||||
* `ArrayMap` provides a very efficient way of iterating over its contents. For small
|
||||
* sets (~10) the cost of binary searching an `ArrayMap` has about the same performance
|
||||
* characteristics that of a `Map` with significantly better memory footprint.
|
||||
*
|
||||
* If used as a `Map` the keys are stored in alphabetical order so that they can be binary searched
|
||||
* for retrieval.
|
||||
*
|
||||
* See: `arrayMapSet`, `arrayMapGet`, `arrayMapIndexOf`, `arrayMapDelete`.
|
||||
*/
|
||||
export interface ArrayMap<VALUE> extends Array<VALUE|string> { __brand__: 'array-map'; }
|
||||
|
||||
/**
|
||||
* Set a `value` for a `key`.
|
||||
*
|
||||
* @param arrayMap to modify.
|
||||
* @param key The key to locate or create.
|
||||
* @param value The value to set for a `key`.
|
||||
* @returns index (always even) of where the value vas set.
|
||||
*/
|
||||
export function arrayMapSet<V>(arrayMap: ArrayMap<V>, key: string, value: V): number {
|
||||
let index = arrayMapIndexOf(arrayMap, key);
|
||||
if (index >= 0) {
|
||||
// if we found it set it.
|
||||
arrayMap[index | 1] = value;
|
||||
} else {
|
||||
index = ~index;
|
||||
arrayInsert2(arrayMap, index, key, value);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a `value` for a `key` (on `undefined` if not found.)
|
||||
*
|
||||
* @param arrayMap to search.
|
||||
* @param key The key to locate.
|
||||
* @return The `value` stored at the `key` location or `undefined if not found.
|
||||
*/
|
||||
export function arrayMapGet<V>(arrayMap: ArrayMap<V>, key: string): V|undefined {
|
||||
const index = arrayMapIndexOf(arrayMap, key);
|
||||
if (index >= 0) {
|
||||
// if we found it retrieve it.
|
||||
return arrayMap[index | 1] as V;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a `key` index value in the array or `-1` if not found.
|
||||
*
|
||||
* @param arrayMap to search.
|
||||
* @param key The key to locate.
|
||||
* @returns index of where the key is (or should have been.)
|
||||
* - positive (even) index if key found.
|
||||
* - negative index if key not found. (`~index` (even) to get the index where it should have
|
||||
* been inserted.)
|
||||
*/
|
||||
export function arrayMapIndexOf<V>(arrayMap: ArrayMap<V>, key: string): number {
|
||||
return _arrayIndexOfSorted(arrayMap as string[], key, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a `key` (and `value`) from the `ArrayMap`.
|
||||
*
|
||||
* @param arrayMap to modify.
|
||||
* @param key The key to locate or delete (if exist).
|
||||
* @returns index of where the key was (or should have been.)
|
||||
* - positive (even) index if key found and deleted.
|
||||
* - negative index if key not found. (`~index` (even) to get the index where it should have
|
||||
* been.)
|
||||
*/
|
||||
export function arrayMapDelete<V>(arrayMap: ArrayMap<V>, key: string): number {
|
||||
const index = arrayMapIndexOf(arrayMap, key);
|
||||
if (index >= 0) {
|
||||
// if we found it remove it.
|
||||
arraySplice(arrayMap, index, 2);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* INTERNAL: Get an index of an `value` in a sorted `array` by grouping search by `shift`.
|
||||
*
|
||||
* NOTE:
|
||||
* - This uses binary search algorithm for fast removals.
|
||||
*
|
||||
* @param array A sorted array to binary search.
|
||||
* @param value The value to look for.
|
||||
* @param shift grouping shift.
|
||||
* - `0` means look at every location
|
||||
* - `1` means only look at every other (even) location (the odd locations are to be ignored as
|
||||
* they are values.)
|
||||
* @returns index of the value.
|
||||
* - positive index if value found.
|
||||
* - negative index if value not found. (`~index` to get the value where it should have been
|
||||
* inserted)
|
||||
*/
|
||||
function _arrayIndexOfSorted(array: string[], value: string, shift: number): number {
|
||||
let start = 0;
|
||||
let end = array.length >> shift;
|
||||
while (end !== start) {
|
||||
const middle = start + ((end - start) >> 1); // find the middle.
|
||||
const current = array[middle << shift];
|
||||
if (value === current) {
|
||||
return (middle << shift);
|
||||
} else if (current > value) {
|
||||
end = middle;
|
||||
} else {
|
||||
start = middle + 1; // We already searched middle so make it non-inclusive by adding 1
|
||||
}
|
||||
}
|
||||
return ~(end << shift);
|
||||
}
|
||||
|
|
|
@ -6,9 +6,11 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {flatten} from '../../src/util/array_utils';
|
||||
import {ArrayMap, arrayIndexOfSorted, arrayInsert, arrayInsert2, arrayInsertSorted, arrayMapDelete, arrayMapGet, arrayMapIndexOf, arrayMapSet, arrayRemoveSorted, arraySplice, flatten} from '../../src/util/array_utils';
|
||||
|
||||
describe('flatten', () => {
|
||||
describe('array_utils', () => {
|
||||
|
||||
describe('flatten', () => {
|
||||
|
||||
it('should flatten an empty array', () => { expect(flatten([])).toEqual([]); });
|
||||
|
||||
|
@ -22,4 +24,155 @@ describe('flatten', () => {
|
|||
expect(flatten([1, [2, [3]], [[[4]]]])).toEqual([1, 2, 3, 4]);
|
||||
expect(flatten([1, [], 2])).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fast arraySplice', () => {
|
||||
function expectArraySplice(array: any[], index: number) {
|
||||
arraySplice(array, index, 1);
|
||||
return expect(array);
|
||||
}
|
||||
|
||||
it('should remove items', () => {
|
||||
expectArraySplice([0, 1, 2], 0).toEqual([1, 2]);
|
||||
expectArraySplice([0, 1, 2], 1).toEqual([0, 2]);
|
||||
expectArraySplice([0, 1, 2], 2).toEqual([0, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arrayInsertSorted', () => {
|
||||
function expectArrayInsert(array: any[], index: number, value: any) {
|
||||
arrayInsert(array, index, value);
|
||||
return expect(array);
|
||||
}
|
||||
|
||||
function expectArrayInsert2(array: any[], index: number, value1: any, value2: any) {
|
||||
arrayInsert2(array, index, value1, value2);
|
||||
return expect(array);
|
||||
}
|
||||
|
||||
it('should insert items', () => {
|
||||
expectArrayInsert([], 0, 'A').toEqual(['A']);
|
||||
expectArrayInsert([0], 0, 'A').toEqual(['A', 0]);
|
||||
expectArrayInsert([0], 1, 'A').toEqual([0, 'A']);
|
||||
expectArrayInsert([0, 1, 2], 0, 'A').toEqual(['A', 0, 1, 2]);
|
||||
expectArrayInsert([0, 1, 2], 1, 'A').toEqual([0, 'A', 1, 2]);
|
||||
expectArrayInsert([0, 1, 2], 2, 'A').toEqual([0, 1, 'A', 2]);
|
||||
expectArrayInsert([0, 1, 2], 3, 'A').toEqual([0, 1, 2, 'A']);
|
||||
});
|
||||
|
||||
it('should insert items', () => {
|
||||
expectArrayInsert2([], 0, 'A', 'B').toEqual(['A', 'B']);
|
||||
expectArrayInsert2([0], 0, 'A', 'B').toEqual(['A', 'B', 0]);
|
||||
expectArrayInsert2([0], 1, 'A', 'B').toEqual([0, 'A', 'B']);
|
||||
expectArrayInsert2([0, 1, 2], 0, 'A', 'B').toEqual(['A', 'B', 0, 1, 2]);
|
||||
expectArrayInsert2([0, 1, 2], 1, 'A', 'B').toEqual([0, 'A', 'B', 1, 2]);
|
||||
expectArrayInsert2([0, 1, 2], 2, 'A', 'B').toEqual([0, 1, 'A', 'B', 2]);
|
||||
expectArrayInsert2([0, 1, 2], 3, 'A', 'B').toEqual([0, 1, 2, 'A', 'B']);
|
||||
expectArrayInsert2(['height', '1px', 'width', '2000px'], 0, 'color', 'red').toEqual([
|
||||
'color', 'red', 'height', '1px', 'width', '2000px'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arrayInsertSorted', () => {
|
||||
|
||||
it('should insert items don\'t allow duplicates', () => {
|
||||
let a;
|
||||
a = ['a', 'c', 'e', 'g', 'i'];
|
||||
expect(arrayInsertSorted(a, 'a')).toEqual(0);
|
||||
expect(a).toEqual(['a', 'c', 'e', 'g', 'i']);
|
||||
|
||||
a = ['a', 'c', 'e', 'g', 'i'];
|
||||
expect(arrayInsertSorted(a, 'b')).toEqual(1);
|
||||
expect(a).toEqual(['a', 'b', 'c', 'e', 'g', 'i']);
|
||||
|
||||
a = ['a', 'c', 'e', 'g', 'i'];
|
||||
expect(arrayInsertSorted(a, 'c')).toEqual(1);
|
||||
expect(a).toEqual(['a', 'c', 'e', 'g', 'i']);
|
||||
|
||||
a = ['a', 'c', 'e', 'g', 'i'];
|
||||
expect(arrayInsertSorted(a, 'd')).toEqual(2);
|
||||
expect(a).toEqual(['a', 'c', 'd', 'e', 'g', 'i']);
|
||||
|
||||
a = ['a', 'c', 'e', 'g', 'i'];
|
||||
expect(arrayInsertSorted(a, 'e')).toEqual(2);
|
||||
expect(a).toEqual(['a', 'c', 'e', 'g', 'i']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('arrayRemoveSorted', () => {
|
||||
|
||||
it('should remove items', () => {
|
||||
let a;
|
||||
a = ['a', 'b', 'c', 'd', 'e'];
|
||||
expect(arrayRemoveSorted(a, 'a')).toEqual(0);
|
||||
expect(a).toEqual(['b', 'c', 'd', 'e']);
|
||||
|
||||
a = ['a', 'b', 'c', 'd', 'e'];
|
||||
expect(arrayRemoveSorted(a, 'b')).toEqual(1);
|
||||
expect(a).toEqual(['a', 'c', 'd', 'e']);
|
||||
|
||||
a = ['a', 'b', 'c', 'd', 'e'];
|
||||
expect(arrayRemoveSorted(a, 'c')).toEqual(2);
|
||||
expect(a).toEqual(['a', 'b', 'd', 'e']);
|
||||
|
||||
a = ['a', 'b', 'c', 'd', 'e'];
|
||||
expect(arrayRemoveSorted(a, 'd')).toEqual(3);
|
||||
expect(a).toEqual(['a', 'b', 'c', 'e']);
|
||||
|
||||
a = ['a', 'b', 'c', 'd', 'e'];
|
||||
expect(arrayRemoveSorted(a, 'e')).toEqual(4);
|
||||
expect(a).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('arrayIndexOfSorted', () => {
|
||||
it('should get index of', () => {
|
||||
const a = ['a', 'b', 'c', 'd', 'e'];
|
||||
expect(arrayIndexOfSorted(a, 'a')).toEqual(0);
|
||||
expect(arrayIndexOfSorted(a, 'b')).toEqual(1);
|
||||
expect(arrayIndexOfSorted(a, 'c')).toEqual(2);
|
||||
expect(arrayIndexOfSorted(a, 'd')).toEqual(3);
|
||||
expect(arrayIndexOfSorted(a, 'e')).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ArrayMap', () => {
|
||||
it('should support basic operations', () => {
|
||||
const map: ArrayMap<number> = [] as any;
|
||||
|
||||
expect(arrayMapIndexOf(map, 'A')).toEqual(~0);
|
||||
|
||||
expect(arrayMapSet(map, 'B', 1)).toEqual(0);
|
||||
expect(map).toEqual(['B', 1]);
|
||||
expect(arrayMapIndexOf(map, 'B')).toEqual(0);
|
||||
|
||||
expect(arrayMapSet(map, 'A', 0)).toEqual(0);
|
||||
expect(map).toEqual(['A', 0, 'B', 1]);
|
||||
expect(arrayMapIndexOf(map, 'B')).toEqual(2);
|
||||
expect(arrayMapIndexOf(map, 'AA')).toEqual(~2);
|
||||
|
||||
expect(arrayMapSet(map, 'C', 2)).toEqual(4);
|
||||
expect(map).toEqual(['A', 0, 'B', 1, 'C', 2]);
|
||||
|
||||
expect(arrayMapGet(map, 'A')).toEqual(0);
|
||||
expect(arrayMapGet(map, 'B')).toEqual(1);
|
||||
expect(arrayMapGet(map, 'C')).toEqual(2);
|
||||
expect(arrayMapGet(map, 'AA')).toEqual(undefined);
|
||||
|
||||
expect(arrayMapSet(map, 'B', -1)).toEqual(2);
|
||||
expect(map).toEqual(['A', 0, 'B', -1, 'C', 2]);
|
||||
|
||||
expect(arrayMapDelete(map, 'AA')).toEqual(~2);
|
||||
expect(arrayMapDelete(map, 'B')).toEqual(2);
|
||||
expect(map).toEqual(['A', 0, 'C', 2]);
|
||||
expect(arrayMapDelete(map, 'A')).toEqual(0);
|
||||
expect(map).toEqual(['C', 2]);
|
||||
expect(arrayMapDelete(map, 'C')).toEqual(0);
|
||||
expect(map).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue