~/Projects/chrome-devtools-frontend
git clone https://code.lsong.org/chrome-devtools-frontend
Commit
- Commit
- c438a002930d6bc5a76b4aebe8683d519daf7160
- Author
- Jack Franklin <[email protected]>
- Date
- 2022-11-25 12:32:37 +0000 +0000
- Diffstat
front_end/core/platform/array-utilities.ts | 76 + test/unittests/front_end/core/platform/ArrayUtilities_test.ts | 274 +++++
Add Array nearestIndex utility helpers Bug: 1386092 Change-Id: I2070269052a5c95666374bf0e77374da7111f8a0 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4055666 Auto-Submit: Jack Franklin <[email protected]> Commit-Queue: Jack Franklin <[email protected]> Reviewed-by: Andres Olivares <[email protected]> Commit-Queue: Andres Olivares <[email protected]>
diff --git a/front_end/core/platform/array-utilities.ts b/front_end/core/platform/array-utilities.ts index 9d87980550ac5e4959b6a53b108125f4abe81c71..cc74cc54cd01b2f578c0b7900e3076ed7f3aa32a 100644 --- a/front_end/core/platform/array-utilities.ts +++ b/front_end/core/platform/array-utilities.ts @@ -168,3 +168,79 @@ } } return r; } + +const enum NearestSearchStart { + BEGINNING = 'BEGINNING', + END = 'END', +} +/** + * Obtains the first or last item in the array that satisfies the predicate function. + * So, for example, if the array were arr = [2, 4, 6, 8, 10], and you are looking for + * the last item arr[i] such that arr[i] < 5 you would be returned 1, because + * array[1] is 4, the last item in the array that satisfies the + * predicate function. + * + * If instead you were looking for the first item in the same array that satisfies + * arr[i] > 5 you would be returned 2 because array[2] = 6. + * + * Please note: this presupposes that the array is already ordered. + */ +function nearestIndex<T>(arr: T[], predicate: (arrayItem: T) => boolean, searchStart: NearestSearchStart): number|null { + const searchFromEnd = searchStart === NearestSearchStart.END; + if (arr.length === 0) { + return null; + } + + let left = 0; + let right = arr.length - 1; + let pivot = 0; + let matchesPredicate = false; + let moveToTheRight = false; + let middle = 0; + do { + middle = left + (right - left) / 2; + pivot = searchFromEnd ? Math.ceil(middle) : Math.floor(middle); + matchesPredicate = predicate(arr[pivot]); + moveToTheRight = matchesPredicate === searchFromEnd; + if (moveToTheRight) { + left = Math.min(right, pivot + (left === pivot ? 1 : 0)); + } else { + right = Math.max(left, pivot + (right === pivot ? -1 : 0)); + } + } while (right !== left); + + // Special-case: the indexed item doesn't pass the predicate. This + // occurs when none of the items in the array are a match for the + // predicate. + if (!predicate(arr[left])) { + return null; + } + return left; +} + +/** + * Obtains the first item in the array that satisfies the predicate function. + * So, for example, if the array was arr = [2, 4, 6, 8, 10], and you are looking for + * the first item arr[i] such that arr[i] > 5 you would be returned 2, because + * array[2] is 6, the first item in the array that satisfies the + * predicate function. + * + * Please note: this presupposes that the array is already ordered. + */ +export function nearestIndexFromBeginning<T>(arr: T[], predicate: (arrayItem: T) => boolean): number|null { + return nearestIndex(arr, predicate, NearestSearchStart.BEGINNING); +} + +/** + * Obtains the last item in the array that satisfies the predicate function. + * So, for example, if the array was arr = [2, 4, 6, 8, 10], and you are looking for + * the last item arr[i] such that arr[i] < 5 you would be returned 1, because + * arr[1] is 4, the last item in the array that satisfies the + * predicate function. + * + * Please note: this presupposes that the array is already ordered. + */ + +export function nearestIndexFromEnd<T>(arr: T[], predicate: (arrayItem: T) => boolean): number|null { + return nearestIndex(arr, predicate, NearestSearchStart.END); +} diff --git a/test/unittests/front_end/core/platform/ArrayUtilities_test.ts b/test/unittests/front_end/core/platform/ArrayUtilities_test.ts index 7c0a1e142ec45f844981f78725a8c23f2908b5e1..399ce7cffdfe0f2d41f177c0a02d218b07bf0a3e 100644 --- a/test/unittests/front_end/core/platform/ArrayUtilities_test.ts +++ b/test/unittests/front_end/core/platform/ArrayUtilities_test.ts @@ -215,4 +215,278 @@ testArray(fixture, true); } }); }); + + describe('Nearest', () => { + describe('Finding the last item where predicate is true', () => { + it('works with an even number of entries', () => { + const ascEntries = [{a: 1}, {a: 3}, {a: 3}, {a: 12}, {a: 13}, {a: 18}, {a: 23}, {a: 24}]; + let nearest = Platform.ArrayUtilities.nearestIndexFromEnd(ascEntries, value => value.a < 7); + + assert.strictEqual(nearest, 2); + + const descEntries = [{a: 23}, {a: 18}, {a: 13}, {a: 12}, {a: 12}, {a: 3}, {a: 1}, {a: 0}]; + nearest = Platform.ArrayUtilities.nearestIndexFromEnd(descEntries, value => value.a > 7); + + assert.strictEqual(nearest, 4); + }); + + it('works with an odd number of entries', () => { + const ascEntries = [ + {a: 1}, + {a: 3}, + {a: 12}, + {a: 13}, + {a: 18}, + {a: 23}, + {a: 23}, + {a: 32}, + {a: 33}, + ]; + let nearest = Platform.ArrayUtilities.nearestIndexFromEnd(ascEntries, value => value.a < 31); + assert.strictEqual(nearest, 6); + + const descEntries = [ + {a: 32}, + {a: 23}, + {a: 18}, + {a: 13}, + {a: 12}, + {a: 3}, + {a: 3}, + {a: 1}, + ]; + nearest = Platform.ArrayUtilities.nearestIndexFromEnd(descEntries, value => value.a > 2); + assert.strictEqual(nearest, 6); + }); + + it('returns null if there are no matches at all', () => { + const ascEntries = [ + {a: 1}, + {a: 3}, + {a: 12}, + {a: 13}, + {a: 18}, + {a: 23}, + {a: 32}, + ]; + let zeroth = Platform.ArrayUtilities.nearestIndexFromEnd(ascEntries, value => value.a < 0); + assert.isNull(zeroth); + + const descEntries = [ + {a: 32}, + {a: 23}, + {a: 18}, + {a: 13}, + {a: 12}, + {a: 3}, + {a: 1}, + ]; + zeroth = Platform.ArrayUtilities.nearestIndexFromEnd(descEntries, value => value.a > 40); + assert.isNull(zeroth); + }); + + it('works when the result is the last item', () => { + const ascEntries = [ + {a: 1}, + {a: 3}, + {a: 12}, + {a: 13}, + {a: 18}, + {a: 23}, + {a: 32}, + {a: 32}, + ]; + let last = Platform.ArrayUtilities.nearestIndexFromEnd(ascEntries, value => value.a < 40); + assert.strictEqual(last, ascEntries.length - 1); + + const descEntries = [ + {a: 32}, + {a: 23}, + {a: 18}, + {a: 13}, + {a: 12}, + {a: 3}, + {a: 1}, + {a: 1}, + ]; + last = Platform.ArrayUtilities.nearestIndexFromEnd(descEntries, value => value.a > 0); + assert.strictEqual(last, descEntries.length - 1); + }); + + it('works on exact values', () => { + const ascEntries = [ + {a: 1}, + {a: 2}, + {a: 3}, + {a: 3}, + {a: 4}, + {a: 5}, + {a: 6}, + ]; + const predicateFunc = (value: {a: number}) => value.a <= 3; + + // Odd number of entries. + // Note that the predicate is allowing an the exact match. + let nearest = Platform.ArrayUtilities.nearestIndexFromEnd(ascEntries, predicateFunc); + assert.strictEqual(nearest, 3); + + // Even number of entries. + ascEntries.push({a: 7}); + nearest = Platform.ArrayUtilities.nearestIndexFromEnd(ascEntries, predicateFunc); + assert.strictEqual(nearest, 3); + + const descEntries = [ + {a: 6}, + {a: 5}, + {a: 4}, + {a: 3}, + {a: 3}, + {a: 2}, + {a: 1}, + ]; + // Note that the predicate is allowing an the exact match. + const gePredicate = (value: {a: number}) => value.a >= 3; + + // Odd number of entries. + nearest = Platform.ArrayUtilities.nearestIndexFromEnd(descEntries, gePredicate); + assert.strictEqual(nearest, 4); + + // Even number of entries. + descEntries.push({a: 7}); + nearest = Platform.ArrayUtilities.nearestIndexFromEnd(descEntries, gePredicate); + assert.strictEqual(nearest, 4); + }); + }); + describe('Finding the first item in the array where predicate is true', () => { + it('works with an even number of entries', () => { + const ascEntries = [{a: 1}, {a: 3}, {a: 12}, {a: 12}, {a: 13}, {a: 18}, {a: 23}, {a: 24}]; + let nearest = Platform.ArrayUtilities.nearestIndexFromBeginning(ascEntries, value => value.a > 7); + assert.strictEqual(nearest, 2); + + const descEntries = [{a: 23}, {a: 18}, {a: 13}, {a: 12}, {a: 12}, {a: 3}, {a: 1}, {a: 0}]; + nearest = Platform.ArrayUtilities.nearestIndexFromBeginning(descEntries, value => value.a < 13); + assert.strictEqual(nearest, 3); + }); + + it('works with an odd number of entries', () => { + const ascEntries = [ + {a: 1}, + {a: 3}, + {a: 12}, + {a: 13}, + {a: 18}, + {a: 23}, + {a: 32}, + {a: 32}, + {a: 33}, + ]; + let nearest = Platform.ArrayUtilities.nearestIndexFromBeginning(ascEntries, value => value.a > 31); + assert.strictEqual(nearest, 6); + + const descEntries = [ + {a: 33}, + {a: 32}, + {a: 23}, + {a: 23}, + {a: 18}, + {a: 23}, + {a: 32}, + {a: 3}, + {a: 1}, + ]; + nearest = Platform.ArrayUtilities.nearestIndexFromBeginning(descEntries, value => value.a < 32); + assert.strictEqual(nearest, 2); + }); + + it('returns null if there are no matches at all', () => { + const entries = [ + {a: 1}, + {a: 3}, + {a: 12}, + {a: 13}, + {a: 18}, + {a: 23}, + {a: 32}, + ]; + const predicate = (value: {a: number}) => value.a > 33; + const nearest = Platform.ArrayUtilities.nearestIndexFromBeginning(entries, predicate); + assert.isNull(nearest); + }); + + it('works when the result is the first item', () => { + const ascEntries = [ + {a: 1}, + {a: 1}, + {a: 3}, + {a: 12}, + {a: 13}, + {a: 18}, + {a: 23}, + {a: 32}, + ]; + const greaterThanPredicate = (value: {a: number}) => value.a > 0; + let first = Platform.ArrayUtilities.nearestIndexFromBeginning(ascEntries, greaterThanPredicate); + assert.strictEqual(first, 0); + + const descEntries = [ + {a: 32}, + {a: 32}, + {a: 23}, + {a: 18}, + {a: 13}, + {a: 12}, + {a: 5}, + {a: 5}, + ]; + const predicate = (value: {a: number}) => value.a < 64; + first = Platform.ArrayUtilities.nearestIndexFromBeginning(descEntries, predicate); + assert.strictEqual(first, 0); + }); + + it('works on exact values', () => { + const ascEntries = [ + {a: 1}, + {a: 2}, + {a: 3}, + {a: 3}, + {a: 4}, + {a: 5}, + {a: 6}, + ]; + // Note that the predicate is allowing an the exact match. + const gePredicate = (value: {a: number}) => value.a >= 3; + + // Even number of entries. + let nearest = Platform.ArrayUtilities.nearestIndexFromBeginning(ascEntries, gePredicate); + assert.strictEqual(nearest, 2); + + // Odd number of entries. + ascEntries.push({a: 7}); + nearest = Platform.ArrayUtilities.nearestIndexFromBeginning(ascEntries, gePredicate); + assert.strictEqual(nearest, 2); + + const descEntries = [ + {a: 6}, + {a: 5}, + {a: 4}, + {a: 3}, + {a: 3}, + {a: 2}, + {a: 1}, + ]; + // Note that the predicate is allowing an the exact match. + const predicateFunc = (value: {a: number}) => value.a <= 3; + + // Even number of entries. + nearest = Platform.ArrayUtilities.nearestIndexFromBeginning(descEntries, predicateFunc); + assert.strictEqual(nearest, 3); + + // Odd number of entries. + descEntries.push({a: 7}); + nearest = Platform.ArrayUtilities.nearestIndexFromBeginning(descEntries, predicateFunc); + + assert.strictEqual(nearest, 3); + }); + }); + }); });