From 6d4de4e127b154746cdf448c87e2255044a5bb22 Mon Sep 17 00:00:00 2001 From: inokawa <48897392+inokawa@users.noreply.github.com> Date: Sun, 13 Oct 2024 14:52:11 +0900 Subject: [PATCH] Refactor findIndex --- src/core/cache.spec.ts | 455 ++++++++++++++++++++--------------------- src/core/cache.ts | 25 +-- 2 files changed, 230 insertions(+), 250 deletions(-) diff --git a/src/core/cache.spec.ts b/src/core/cache.spec.ts index 23f49353..94bdb1bf 100644 --- a/src/core/cache.spec.ts +++ b/src/core/cache.spec.ts @@ -330,96 +330,6 @@ describe(computeOffset.name, () => { }); describe(computeRange.name, () => { - const INITIAL_INDEX = 0; - it("should get start if offset is at start", () => { - expect( - computeRange( - initCacheWithComputedOffsets( - range(10, () => 20), - 30 - ), - 0, - 100, - INITIAL_INDEX - ) - ).toEqual([0, 5]); - }); - - it("should get start + 1 if offset is at start + 1", () => { - expect( - computeRange( - initCacheWithComputedOffsets( - range(10, () => 20), - 30 - ), - 20, - 100, - INITIAL_INDEX - ) - ).toEqual([1, 6]); - }); - - it("should get last if offset is at end", () => { - const cache = initCacheWithComputedOffsets( - range(10, () => 20), - 30 - ); - const last = cache._length - 1; - expect(computeRange(cache, sum(cache._sizes), 100, INITIAL_INDEX)).toEqual([ - last, - last, - ]); - }); - - it("should get last if offset is at end - 1", () => { - const cache = initCacheWithComputedOffsets( - range(10, () => 20), - 30 - ); - const last = cache._length - 1; - expect( - computeRange(cache, sum(cache._sizes) - 20, 100, INITIAL_INDEX) - ).toEqual([last, last]); - }); - - it("should get last - 1 if offset is at end - 1 and more", () => { - const cache = initCacheWithComputedOffsets( - range(10, () => 20), - 30 - ); - const last = cache._length - 1; - expect( - computeRange(cache, sum(cache._sizes) - 20 - 1, 100, INITIAL_INDEX) - ).toEqual([last - 1, last]); - }); - - it("should get start if offset is before start", () => { - expect( - computeRange( - initCacheWithComputedOffsets( - range(10, () => 20), - 30 - ), - -1000, - 100, - INITIAL_INDEX - ) - ).toEqual([0, 0]); - }); - - it("should get last if offset is after end", () => { - const cache = initCacheWithComputedOffsets( - range(10, () => 20), - 30 - ); - const last = cache._length - 1; - expect( - computeRange(cache, sum(cache._sizes) + 1000, 100, INITIAL_INDEX) - ).toEqual([last, last]); - }); -}); - -describe(findIndex.name, () => { const CACHE_LENGTH = 10; describe.each([ @@ -427,205 +337,274 @@ describe(findIndex.name, () => { [Math.floor(CACHE_LENGTH / 2)], // mid [CACHE_LENGTH - 1], // end ])("start from %i", (initialIndex) => { - it("should resolve default height", () => { - const cache = initCacheWithEmptyOffsets( - range(10, () => -1), - 25 - ); - expect(findIndex(cache, 100, initialIndex)).toBe(4); - }); - it("should get start if offset is at start", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 0, initialIndex)).toBe(0); + expect( + computeRange( + initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ), + 0, + 100, + initialIndex + ) + ).toEqual([0, 5]); }); - it("should get start if offset is at start + 1px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 1, initialIndex)).toBe(0); + it("should get start + 1 if offset is at start + 1", () => { + expect( + computeRange( + initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ), + 20, + 100, + initialIndex + ) + ).toEqual([1, 6]); }); - it("should get start if offset is at start + 0.01px", () => { + it("should get last if offset is at end", () => { const cache = initCacheWithComputedOffsets( range(CACHE_LENGTH, () => 20), 30 ); - expect(findIndex(cache, 0.01, initialIndex)).toBe(0); - }); - - it("should get start if offset is at start - 1px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 + const last = cache._length - 1; + expect(computeRange(cache, sum(cache._sizes), 100, initialIndex)).toEqual( + [last, last] ); - expect(findIndex(cache, -1, initialIndex)).toBe(0); }); - it("should get start if offset is at start - 0.01px", () => { + it("should get last if offset is at end - 1", () => { const cache = initCacheWithComputedOffsets( range(CACHE_LENGTH, () => 20), 30 ); - expect(findIndex(cache, -0.01, initialIndex)).toBe(0); + const last = cache._length - 1; + expect( + computeRange(cache, sum(cache._sizes) - 20, 100, initialIndex) + ).toEqual([last, last]); }); - it("should get end if offset is at end", () => { + it("should get last - 1 if offset is at end - 1 and more", () => { const cache = initCacheWithComputedOffsets( range(CACHE_LENGTH, () => 20), 30 ); - expect(findIndex(cache, sum(cache._sizes), initialIndex)).toBe( - cache._length - 1 - ); + const last = cache._length - 1; + expect( + computeRange(cache, sum(cache._sizes) - 20 - 1, 100, initialIndex) + ).toEqual([last - 1, last]); }); - it("should get end if offset is at end + 1px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, sum(cache._sizes) + 1, initialIndex)).toBe( - cache._length - 1 - ); + it("should get start if offset is before start", () => { + expect( + computeRange( + initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ), + -1000, + 100, + initialIndex + ) + ).toEqual([0, 0]); }); - it("should get end if offset is at end + 0.01px", () => { + it("should get last if offset is after end", () => { const cache = initCacheWithComputedOffsets( range(CACHE_LENGTH, () => 20), 30 ); - expect(findIndex(cache, sum(cache._sizes) + 0.01, initialIndex)).toBe( - cache._length - 1 - ); + const last = cache._length - 1; + expect( + computeRange(cache, sum(cache._sizes) + 1000, 100, initialIndex) + ).toEqual([last, last]); }); - it("should get end if offset is at end - 1px", () => { + it("should get prevStartIndex if offset fits prevStartIndex", () => { + const offset = (cache: Cache, i: number) => sum(cache._sizes.slice(0, i)); const cache = initCacheWithComputedOffsets( range(CACHE_LENGTH, () => 20), 30 ); - expect(findIndex(cache, sum(cache._sizes) - 1, initialIndex)).toBe( - cache._length - 1 - ); + expect( + computeRange(cache, offset(cache, initialIndex), 100, initialIndex) + ).toEqual([initialIndex, expect.any(Number)]); }); + }); +}); - it("should get end if offset is at end - 0.01px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, sum(cache._sizes) - 0.01, initialIndex)).toBe( - cache._length - 1 - ); - }); +describe(findIndex.name, () => { + const CACHE_LENGTH = 10; - it("should get 1 if offset fits index 1", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 20, initialIndex)).toBe(1); - }); + it("should resolve default height", () => { + const cache = initCacheWithEmptyOffsets( + range(10, () => -1), + 25 + ); + expect(findIndex(cache, 100)).toBe(4); + }); - it("should get 1 if offset fits index 1 + 1px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 21, initialIndex)).toBe(1); - }); + it("should get start if offset is at start", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 0)).toBe(0); + }); - it("should get 1 if offset fits index 1 + 0.01px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 20.01, initialIndex)).toBe(1); - }); + it("should get start if offset is at start + 1px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 1)).toBe(0); + }); - it("should get 0 if offset fits index 1 - 1px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 19, initialIndex)).toBe(0); - }); + it("should get start if offset is at start + 0.01px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 0.01)).toBe(0); + }); - it("should get 0 if offset fits index 1 - 0.01px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 19.99, initialIndex)).toBe(0); - }); + it("should get start if offset is at start - 1px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, -1)).toBe(0); + }); - it("should get 1 if offset fits index 1.5", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 30, initialIndex)).toBe(1); - }); + it("should get start if offset is at start - 0.01px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, -0.01)).toBe(0); + }); - it("should get 1 if offset fits index 1.5 + 1px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 31, initialIndex)).toBe(1); - }); + it("should get end if offset is at end", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, sum(cache._sizes))).toBe(cache._length - 1); + }); - it("should get 1 if offset fits index 1.5 + 0.01px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 30.01, initialIndex)).toBe(1); - }); + it("should get end if offset is at end + 1px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, sum(cache._sizes) + 1)).toBe(cache._length - 1); + }); - it("should get 1 if offset fits index 1.5 - 1px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 29, initialIndex)).toBe(1); - }); + it("should get end if offset is at end + 0.01px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, sum(cache._sizes) + 0.01)).toBe(cache._length - 1); + }); - it("should get 1 if offset fits index 1.5 - 0.01px", () => { - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, 29.99, initialIndex)).toBe(1); - }); + it("should get end if offset is at end - 1px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, sum(cache._sizes) - 1)).toBe(cache._length - 1); + }); - it("should get prevStartIndex if offset fits prevStartIndex", () => { - const offset = (cache: Cache, i: number) => sum(cache._sizes.slice(0, i)); - const cache = initCacheWithComputedOffsets( - range(CACHE_LENGTH, () => 20), - 30 - ); - expect(findIndex(cache, offset(cache, initialIndex), initialIndex)).toBe( - initialIndex - ); - }); + it("should get end if offset is at end - 0.01px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, sum(cache._sizes) - 0.01)).toBe(cache._length - 1); }); - describe("both", () => { - it("should get same in forward and backward search", () => { - const cache = initCacheWithComputedOffsets( - range(10, (i) => (i % 2 === 0 ? 21 : 41)), - 30 - ); - expect(findIndex(cache, 1, 0)).toBe(0); - expect(findIndex(cache, 1, 1)).toBe(0); - }); + it("should get 1 if offset fits index 1", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 20)).toBe(1); + }); + + it("should get 1 if offset fits index 1 + 1px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 21)).toBe(1); + }); + + it("should get 1 if offset fits index 1 + 0.01px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 20.01)).toBe(1); + }); + + it("should get 0 if offset fits index 1 - 1px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 19)).toBe(0); + }); + + it("should get 0 if offset fits index 1 - 0.01px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 19.99)).toBe(0); + }); + + it("should get 1 if offset fits index 1.5", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 30)).toBe(1); + }); + + it("should get 1 if offset fits index 1.5 + 1px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 31)).toBe(1); + }); + + it("should get 1 if offset fits index 1.5 + 0.01px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 30.01)).toBe(1); + }); + + it("should get 1 if offset fits index 1.5 - 1px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 29)).toBe(1); + }); + + it("should get 1 if offset fits index 1.5 - 0.01px", () => { + const cache = initCacheWithComputedOffsets( + range(CACHE_LENGTH, () => 20), + 30 + ); + expect(findIndex(cache, 29.99)).toBe(1); }); }); diff --git a/src/core/cache.ts b/src/core/cache.ts index e99a6fb1..da3515b0 100644 --- a/src/core/cache.ts +++ b/src/core/cache.ts @@ -97,17 +97,13 @@ export const computeTotalSize = (cache: Cache): number => { * * @internal */ -export const findIndex = (cache: Cache, offset: number, i: number): number => { +export const findIndex = ( + cache: Cache, + offset: number, + low: number = 0, + high: number = cache._length - 1 +): number => { // Find with binary search - let low = 0; - let high = cache._length - 1; - - if (computeOffset(cache, i) <= offset) { - low = i; // Start searching from initialIndex -> up - } else { - high = i; // Start searching from initialIndex -> down - } - while (low <= high) { const mid = floor((low + high) / 2); const itemOffset = computeOffset(cache, mid); @@ -132,11 +128,16 @@ export const computeRange = ( viewportSize: number, prevStartIndex: number ): ItemsRange => { + // Clamp because prevStartIndex may exceed the limit when children decreased a lot after scrolling + prevStartIndex = min(prevStartIndex, cache._length - 1); + + const shouldSearchForward = + computeOffset(cache, prevStartIndex) <= scrollOffset; const start = findIndex( cache, scrollOffset, - // Clamp because prevStartIndex may exceed the limit when children decreased a lot after scrolling - min(prevStartIndex, cache._length - 1) + shouldSearchForward ? prevStartIndex : undefined, + shouldSearchForward ? undefined : prevStartIndex ); return [start, findIndex(cache, scrollOffset + viewportSize, start)]; };