From 6a4628294d24e800d3e1f623d167ab23dfd1a5a6 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 3 Dec 2025 11:27:38 +0300 Subject: [PATCH] add iterators and few usefull methods --- README.md | 131 ++++++++++++++++++++++++-------------- src/sparse_set.test.ts | 138 ++++++++++++++++++++++++++++++++++++++++ src/sparse_set.ts | 140 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 363 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index d7b70df..a109dbb 100644 --- a/README.md +++ b/README.md @@ -3,79 +3,118 @@ [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][downloads-url] -**TS SparseSet** is a high-performance TypeScript data structure implementing a *sparse set*, enabling fast management of ID-based collections and ECS components. +**TS SparseSet** is a high-performance TypeScript data structure implementing a *sparse set*, providing constant-time operations and cache-friendly dense storage. Ideal for ECS architectures, game engines, simulations, and any ID-indexed data. -#### Features +--- -* **O(1) Operations:** `add`, `remove`, `get`, and `has` with dense storage. -* **Dense Storage:** Efficient memory layout for maximum performance and cache locality. -* **Swap-Remove:** Maintains a compact dense array after removals. -* **Reusable Structure:** Suitable for ECS components, entities, object pools, or other ID-based collections. -* **Optional Object Pool Integration:** Easy integration with object pools to minimize allocations. -* **TypeScript Native:** Fully typed and compatible with modern TypeScript. +## Features -#### Installation +* **O(1) Operations:** `add`, `remove`, `has`, `get`, `ensure`, and index lookups. +* **Dense Storage:** Compact arrays for optimal data locality and iteration speed. +* **Swap-Remove Semantics:** Maintains dense compactness after deletions without gaps. +* **Full Iteration Support:** Native iterators, `values()`, `ids()`, `entries()`, `forEach()`. +* **Zero-GC Hot Path:** No intermediate allocations in iteration modes. +* **TypeScript Native:** Fully typed, generic, and safe. + +--- + +## Installation ```bash npm install ts-sparse-set ``` -#### Usage Example +--- -```typescript +## Usage Example + +```ts import { SparseSet } from 'ts-sparse-set'; -// Create a new SparseSet for numbers const set = new SparseSet(); -// Add elements set.add(10, 42); set.add(20, 100); -// Check existence console.log(set.has(10)); // true -console.log(set.has(5)); // false - -// Get element by ID console.log(set.get(20)); // 100 -// Overwrite existing value -set.add(10, 50); -console.log(set.get(10)); // 50 +set.add(10, 77); +console.log(set.get(10)); // 77 -// Remove an element set.remove(20); console.log(set.has(20)); // false -// Add multiple elements and check dense array integrity -set.add(30, 300); -set.add(40, 400); -set.remove(10); -console.log(set.get(30)); // 300 -console.log(set.get(40)); // 400 +// Iteration (dense order) +for (const entry of set) { + console.log(entry.id, entry.value); +} + +// Using ensure(...) +const v = set.ensure(50, () => 123); +console.log(v); // 123 + +// Utility iteration helpers +console.log([...set.ids()]); // [10, 50] +console.log([...set.values()]); // [77, 123] +console.log(set.size()); // 2 ``` -#### API Documentation - -* **SparseSet Class** - - * **Constructor:** `new SparseSet()` - * **Methods:** - - * `add(id: number, value: V): V` — adds a new element or updates an existing one. - * `get(id: number): V | null` — retrieves the element by ID or `null` if missing. - * `has(id: number): boolean` — checks if an element exists. - * `remove(id: number): void` — removes an element and keeps the dense array compact. - -#### Note - -SparseSet is ideal for ECS scenarios where entities and components are identified by numeric IDs. It is not a general-purpose Map/Set and is optimized for dense arrays and minimal operation cost. - -Good luck! - --- -Feel free to contribute, report issues, or suggest improvements on [GitHub](https://github.com/yazmeyaa/ts-sparse-set). +## API Documentation + +### `class SparseSet` + +#### Core Methods + +| Method | Description | +| ---------------------------------- | --------------------------------------------------- | +| **`add(id: number, value: V): V`** | Inserts a new value or replaces an existing one. | +| **`get(id: number): V \| null`** | Retrieves a value or returns `null` if not present. | +| **`has(id: number): boolean`** | Checks if an element exists. | +| **`remove(id: number): void`** | Removes the element using O(1) swap-remove logic. | +| **`size(): number`** | Returns the number of elements stored. | +| **`clear(): void`** | Removes all elements. | + +#### Iteration Helpers + +| Method | Description | +| --------------------------------------- | ------------------------------------------------------- | +| **`[Symbol.iterator]()`** | Iterates over `{ id, value }` entries in dense order. | +| **`ids(): Generator`** | Iterates over all ids. | +| **`values(): Generator`** | Iterates over all values. | +| **`entries(): Generator<[number, V]>`** | Iterates over `[id, value]` tuples. | +| **`forEach(fn)`** | Efficient callback-based iteration without allocations. | + +#### Utility Methods + +| Method | Description | +| --------------------------------------------- | --------------------------------------------------- | +| **`ensure(id: number, factory: () => V): V`** | Returns existing value or inserts a new one lazily. | +| **`tryGetIndex(id: number): number`** | Returns dense index or `-1` if id does not exist. | + +--- + +## When to Use SparseSet + +SparseSet excels when: + +* IDs are numeric and bounded. +* You need fast add/remove with compact dense data. +* You rely heavily on iteration speed (ECS, physics, grids, pools). +* You want predictable memory layout and minimal GC pressure. + +It is **not** a general-purpose map or associative container — it is optimized for performance-critical numeric ID use cases. + +--- + +## Contributing + +Issues, PRs, and suggestions are welcome. +Project repository: **[https://github.com/yazmeyaa/ts-sparse-set](https://github.com/yazmeyaa/ts-sparse-set)** + +--- [npm-image]: https://img.shields.io/npm/v/ts-sparse-set.svg?style=flat-square [npm-url]: https://npmjs.org/package/ts-sparse-set diff --git a/src/sparse_set.test.ts b/src/sparse_set.test.ts index dd032c5..d2d99c2 100644 --- a/src/sparse_set.test.ts +++ b/src/sparse_set.test.ts @@ -89,4 +89,142 @@ describe("SparseSet", () => { expect(set.get(i)).toBeNull(); } }); + + test("iterator yields entries in dense order", () => { + const set = new SparseSet(); + + set.add(2, 10); + set.add(3, 15); + set.add(4, 20); + set.add(5, 15); + + const entries = [...set]; + + expect(entries).toEqual([ + { id: 2, value: 10 }, + { id: 3, value: 15 }, + { id: 4, value: 20 }, + { id: 5, value: 15 }, + ]); + }); + + test("iterator respects swaps after removal", () => { + const set = new SparseSet(); + + set.add(2, 10); + set.add(3, 15); + set.add(4, 20); + + set.remove(3); + + const entries = [...set]; + + expect(entries).toEqual([ + { id: 2, value: 10 }, + { id: 4, value: 20 }, + ]); + }); + + test("size returns correct number of elements", () => { + expect(set.size()).toBe(0); + + set.add(1, 10); + set.add(2, 20); + expect(set.size()).toBe(2); + + set.remove(1); + expect(set.size()).toBe(1); + }); + + test("clear removes all elements", () => { + set.add(1, 10); + set.add(2, 20); + set.add(3, 30); + + set.clear(); + + expect(set.size()).toBe(0); + expect(set.has(1)).toBe(false); + expect(set.has(2)).toBe(false); + expect(set.has(3)).toBe(false); + }); + + test("ids() yields ids in dense order", () => { + set.add(5, 50); + set.add(7, 70); + set.add(9, 90); + + const ids = [...set.ids()]; + expect(ids).toEqual([5, 7, 9]); + }); + + test("values() yields values in dense order", () => { + set.add(5, 50); + set.add(7, 70); + set.add(9, 90); + + const values = [...set.values()]; + expect(values).toEqual([50, 70, 90]); + }); + + test("entries() yields [id, value] tuples", () => { + set.add(5, 50); + set.add(7, 70); + + expect([...set.entries()]).toEqual([ + [5, 50], + [7, 70], + ]); + }); + + test("forEach iterates in dense order", () => { + set.add(1, 10); + set.add(2, 20); + set.add(3, 30); + + const collected: Array<[number, number]> = []; + + set.forEach((value, id) => { + collected.push([id, value]); + }); + + expect(collected).toEqual([ + [1, 10], + [2, 20], + [3, 30], + ]); + }); + + test("ensure returns existing value", () => { + set.add(10, 100); + + const v = set.ensure(10, () => 999); + + expect(v).toBe(100); + expect(set.get(10)).toBe(100); + }); + + test("ensure creates value when missing", () => { + const v = set.ensure(20, () => 200); + + expect(v).toBe(200); + expect(set.get(20)).toBe(200); + expect(set.has(20)).toBe(true); + }); + + test("tryGetIndex returns correct dense index", () => { + set.add(5, 50); + set.add(7, 70); + + const idx5 = set.tryGetIndex(5); + const idx7 = set.tryGetIndex(7); + + expect(idx5).toBe(0); + expect(idx7).toBe(1); + }); + + test("tryGetIndex returns -1 for missing id", () => { + expect(set.tryGetIndex(123)).toBe(-1); + }); + }); diff --git a/src/sparse_set.ts b/src/sparse_set.ts index ecedef9..9b61cb1 100644 --- a/src/sparse_set.ts +++ b/src/sparse_set.ts @@ -1,8 +1,26 @@ +/** + * A pair of (id, value) returned by the SparseSet iterator. + */ +export type SparseSetEntry = { + value: V; + id: number; +}; + +/** + * SparseSet — a constant-time set/map structure using sparse/dense indexing. + * Provides O(1) add, remove, has, and get operations while keeping values packed densely. + */ export class SparseSet { private sparse: number[] = []; private dense: number[] = []; private data: V[] = []; + /** + * Checks whether an item with the given id exists in the set. + * + * @param id - The element identifier. + * @returns True if the element exists, false otherwise. + */ public has(id: number): boolean { const index = this.sparse[id]; return ( @@ -12,6 +30,12 @@ export class SparseSet { ); } + /** + * Retrieves the value associated with the given id. + * + * @param id - The element identifier. + * @returns The value if present, otherwise null. + */ public get(id: number): V | null { const index = this.sparse[id]; if ( @@ -24,6 +48,14 @@ export class SparseSet { return this.data[index]; } + /** + * Adds or updates an element with the specified id. + * If the id already exists, its value is overwritten. + * + * @param id - The element identifier. + * @param value - The element value. + * @returns The stored value. + */ public add(id: number, value: V): V { const existing = this.sparse[id]; if ( @@ -48,6 +80,12 @@ export class SparseSet { return value; } + /** + * Removes the element with the given id. + * Performs a swap with the last dense element to maintain O(1) removal. + * + * @param id - The element identifier to remove. + */ public remove(id: number): void { const index = this.sparse[id]; if ( @@ -71,4 +109,106 @@ export class SparseSet { this.data.pop(); delete this.sparse[id]; } + + /** + * Iterates over all entries in dense order. + * + * @returns A generator yielding { id, value } objects. + */ + public *[Symbol.iterator](): Generator> { + const len = this.dense.length; + for (let i = 0; i < len; i++) { + yield { + id: this.dense[i], + value: this.data[i], + }; + } + } + + /** + * Returns the number of stored elements. + * + * @returns Total element count. + */ + public size(): number { + return this.dense.length; + } + + /** + * Removes all elements from the set. + */ + public clear(): void { + this.sparse.length = 0; + this.dense.length = 0; + this.data.length = 0; + } + + /** + * Iterates over all ids in dense order. + * + * @returns A generator yielding element ids. + */ + public *ids(): Generator { + for (let i = 0; i < this.dense.length; i++) yield this.dense[i]; + } + + /** + * Iterates over all values in dense order. + * + * @returns A generator yielding element values. + */ + public *values(): Generator { + for (let i = 0; i < this.data.length; i++) yield this.data[i]; + } + + /** + * Iterates over all entries as [id, value] tuples. + * + * @returns A generator yielding [id, value] pairs. + */ + public *entries(): Generator<[number, V]> { + for (let i = 0; i < this.dense.length; i++) + yield [this.dense[i], this.data[i]]; + } + + /** + * Applies a callback to each element in dense order. + * + * @param fn - A function receiving (value, id). + */ + public forEach(fn: (value: V, id: number) => void): void { + for (let i = 0; i < this.dense.length; i++) { + fn(this.data[i], this.dense[i]); + } + } + + /** + * Ensures an element exists for the given id. + * If present, returns it. Otherwise, creates a new one using the factory. + * + * @param id - Element identifier. + * @param factory - Function creating a value if the id is missing. + * @returns The existing or newly created value. + */ + public ensure(id: number, factory: () => V): V { + const index = this.sparse[id]; + if (index !== undefined && this.dense[index] === id) { + return this.data[index]; + } + const v = factory(); + this.add(id, v); + return v; + } + + /** + * Returns the dense index of the element with the given id. + * Primarily useful for low-level optimizations. + * + * @param id - Element identifier. + * @returns The dense index or -1 if the id does not exist. + */ + public tryGetIndex(id: number): number { + const idx = this.sparse[id]; + return idx !== undefined && this.dense[idx] === id ? idx : -1; + } }