feat: enhance useVirtualList for dynamic item sizing and improved scrolling behavior
Publish to NPM / Check version changes and publish (push) Failing after 11m20s
Publish to NPM / Check version changes and publish (push) Failing after 11m20s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@robonen/stdlib",
|
||||
"version": "0.0.10",
|
||||
"version": "0.0.11",
|
||||
"license": "Apache-2.0",
|
||||
"description": "A collection of tools, utilities, and helpers for TypeScript",
|
||||
"keywords": [
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { FenwickTree } from '.';
|
||||
|
||||
/** Deterministic LCG so failures reproduce. */
|
||||
function lcg(seed: number): () => number {
|
||||
let state = seed;
|
||||
return () => {
|
||||
state = (state * 48271) % 2147483647;
|
||||
return state / 2147483647;
|
||||
};
|
||||
}
|
||||
|
||||
describe('FenwickTree', () => {
|
||||
it('should match naive sums after build', () => {
|
||||
const rand = lcg(1);
|
||||
const values = Array.from({ length: 137 }, () => Math.floor(rand() * 100));
|
||||
const tree = new FenwickTree(values.length);
|
||||
tree.build(values);
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i <= values.length; i++) {
|
||||
expect(tree.prefix(i)).toBe(sum);
|
||||
if (i < values.length)
|
||||
sum += values[i]!;
|
||||
}
|
||||
});
|
||||
|
||||
it('should keep prefix sums consistent with a naive array under updates', () => {
|
||||
const rand = lcg(2);
|
||||
const length = 64;
|
||||
const naive = Array.from({ length }, () => Math.floor(rand() * 50));
|
||||
const tree = new FenwickTree(length);
|
||||
tree.build(naive);
|
||||
|
||||
for (let op = 0; op < 500; op++) {
|
||||
const index = Math.floor(rand() * length);
|
||||
const delta = Math.floor(rand() * 40) - 20;
|
||||
naive[index]! += delta;
|
||||
tree.update(index, delta);
|
||||
|
||||
const probe = Math.floor(rand() * (length + 1));
|
||||
const expected = naive.slice(0, probe).reduce((a, b) => a + b, 0);
|
||||
expect(tree.prefix(probe)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should match a linear scan in lowerBound, including stride and zero values', () => {
|
||||
const rand = lcg(3);
|
||||
for (let round = 0; round < 20; round++) {
|
||||
const length = 1 + Math.floor(rand() * 40);
|
||||
const values = Array.from({ length }, () => rand() < 0.2 ? 0 : Math.floor(rand() * 60));
|
||||
const stride = round % 3 === 0 ? 0 : Math.floor(rand() * 10);
|
||||
const tree = new FenwickTree(length);
|
||||
tree.build(values);
|
||||
|
||||
const total = values.reduce((a, b) => a + b, 0) + length * stride;
|
||||
for (const target of [-5, 0, 1, total / 3, total / 2, total - 1, total, total + 100]) {
|
||||
let expected = 0;
|
||||
for (let c = 0; c <= length; c++) {
|
||||
const g = values.slice(0, c).reduce((a, b) => a + b, 0) + c * stride;
|
||||
if (g <= target)
|
||||
expected = c;
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (target < 0)
|
||||
expected = 0;
|
||||
expect(tree.lowerBound(target, stride), `length=${length} stride=${stride} target=${target}`).toBe(expected);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle an empty tree', () => {
|
||||
const tree = new FenwickTree(0);
|
||||
|
||||
expect(tree.prefix(0)).toBe(0);
|
||||
expect(tree.lowerBound(0)).toBe(0);
|
||||
expect(tree.lowerBound(100)).toBe(0);
|
||||
});
|
||||
|
||||
it('should rebuild in place via build', () => {
|
||||
const tree = new FenwickTree(4);
|
||||
tree.build([1, 2, 3, 4]);
|
||||
expect(tree.prefix(4)).toBe(10);
|
||||
|
||||
tree.build([10, 10, 10, 10]);
|
||||
expect(tree.prefix(2)).toBe(20);
|
||||
expect(tree.prefix(4)).toBe(40);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @name FenwickTree
|
||||
* @category Data Structures
|
||||
* @description Fenwick (Binary Indexed) tree over an array of non-negative
|
||||
* numbers: O(log n) prefix sums, point updates, and monotonic lower-bound
|
||||
* search, plus O(n) bulk rebuild. `lowerBound` assumes all values are
|
||||
* non-negative (the prefix function must be non-decreasing)
|
||||
*
|
||||
* @example
|
||||
* const tree = new FenwickTree(5);
|
||||
* tree.build([10, 20, 30, 40, 50]);
|
||||
* tree.prefix(3); // 60
|
||||
* tree.update(1, 5); // value at index 1 becomes 25
|
||||
* tree.lowerBound(65); // 3 — largest c with prefix(c) <= 65
|
||||
*
|
||||
* @since 0.0.11
|
||||
*/
|
||||
export class FenwickTree {
|
||||
readonly size: number;
|
||||
private readonly tree: Float64Array;
|
||||
private readonly highBit: number;
|
||||
|
||||
constructor(size: number) {
|
||||
this.size = size;
|
||||
this.tree = new Float64Array(size + 1);
|
||||
this.highBit = size > 0 ? 1 << (31 - Math.clz32(size)) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk (re)initialization from raw values, O(n)
|
||||
*
|
||||
* @param {ArrayLike<number>} values The values to load, `values.length` must equal `size`
|
||||
*/
|
||||
build(values: ArrayLike<number>): void {
|
||||
const { tree, size } = this;
|
||||
tree.fill(0);
|
||||
for (let i = 1; i <= size; i++) {
|
||||
tree[i]! += values[i - 1]!;
|
||||
const parent = i + (i & -i);
|
||||
if (parent <= size)
|
||||
tree[parent]! += tree[i]!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add `delta` to the value at `index`, O(log n)
|
||||
*
|
||||
* @param {number} index Zero-based index of the value to change
|
||||
* @param {number} delta Amount to add (may be negative)
|
||||
*/
|
||||
update(index: number, delta: number): void {
|
||||
for (let i = index + 1; i <= this.size; i += i & -i)
|
||||
this.tree[i]! += delta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum of the first `count` values, O(log n)
|
||||
*
|
||||
* @param {number} count How many leading values to sum
|
||||
* @returns {number} The prefix sum
|
||||
*/
|
||||
prefix(count: number): number {
|
||||
let sum = 0;
|
||||
for (let i = count; i > 0; i -= i & -i)
|
||||
sum += this.tree[i]!;
|
||||
return sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Largest `c` in `[0, size]` with `prefix(c) + c * stride <= target`, O(log n).
|
||||
* `stride` models a constant per-item addition (e.g. a layout gap) without
|
||||
* storing it in the tree
|
||||
*
|
||||
* @param {number} target The offset to search for
|
||||
* @param {number} stride Constant added per item, defaults to `0`
|
||||
* @returns {number} The largest count whose strided prefix does not exceed `target`
|
||||
*/
|
||||
lowerBound(target: number, stride = 0): number {
|
||||
if (target < 0)
|
||||
return 0;
|
||||
let pos = 0;
|
||||
let sum = 0;
|
||||
for (let step = this.highBit; step > 0; step >>= 1) {
|
||||
const next = pos + step;
|
||||
if (next <= this.size) {
|
||||
const candidate = sum + this.tree[next]!;
|
||||
if (candidate + next * stride <= target) {
|
||||
pos = next;
|
||||
sum = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './BinaryHeap';
|
||||
export * from './CircularBuffer';
|
||||
export * from './Deque';
|
||||
export * from './FenwickTree';
|
||||
export * from './LinkedList';
|
||||
export * from './PriorityQueue';
|
||||
export * from './Queue';
|
||||
|
||||
Reference in New Issue
Block a user