add iterators and few usefull methods

This commit is contained in:
Eugene 2025-12-03 11:27:38 +03:00
parent 1a2e89d45a
commit 6a4628294d
3 changed files with 363 additions and 46 deletions

131
README.md
View File

@ -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<number>();
// 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<V>()`
* **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<V>`
#### 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<number>`** | Iterates over all ids. |
| **`values(): Generator<V>`** | 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

View File

@ -89,4 +89,142 @@ describe("SparseSet", () => {
expect(set.get(i)).toBeNull();
}
});
test("iterator yields entries in dense order", () => {
const set = new SparseSet<number>();
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<number>();
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);
});
});

View File

@ -1,8 +1,26 @@
/**
* A pair of (id, value) returned by the SparseSet iterator.
*/
export type SparseSetEntry<V> = {
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<V> {
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<V> {
);
}
/**
* 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<V> {
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<V> {
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<V> {
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<SparseSetEntry<V>> {
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<number> {
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<V> {
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;
}
}