Liu Song’s Projects


~/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);
+      });
+    });
+  });
 });