add iterators and few usefull methods
This commit is contained in:
parent
1a2e89d45a
commit
6a4628294d
131
README.md
131
README.md
|
|
@ -3,79 +3,118 @@
|
||||||
[![NPM version][npm-image]][npm-url]
|
[![NPM version][npm-image]][npm-url]
|
||||||
[![Downloads][downloads-image]][downloads-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.
|
## Features
|
||||||
* **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.
|
|
||||||
|
|
||||||
#### 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
|
```bash
|
||||||
npm install ts-sparse-set
|
npm install ts-sparse-set
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Usage Example
|
---
|
||||||
|
|
||||||
```typescript
|
## Usage Example
|
||||||
|
|
||||||
|
```ts
|
||||||
import { SparseSet } from 'ts-sparse-set';
|
import { SparseSet } from 'ts-sparse-set';
|
||||||
|
|
||||||
// Create a new SparseSet for numbers
|
|
||||||
const set = new SparseSet<number>();
|
const set = new SparseSet<number>();
|
||||||
|
|
||||||
// Add elements
|
|
||||||
set.add(10, 42);
|
set.add(10, 42);
|
||||||
set.add(20, 100);
|
set.add(20, 100);
|
||||||
|
|
||||||
// Check existence
|
|
||||||
console.log(set.has(10)); // true
|
console.log(set.has(10)); // true
|
||||||
console.log(set.has(5)); // false
|
|
||||||
|
|
||||||
// Get element by ID
|
|
||||||
console.log(set.get(20)); // 100
|
console.log(set.get(20)); // 100
|
||||||
|
|
||||||
// Overwrite existing value
|
set.add(10, 77);
|
||||||
set.add(10, 50);
|
console.log(set.get(10)); // 77
|
||||||
console.log(set.get(10)); // 50
|
|
||||||
|
|
||||||
// Remove an element
|
|
||||||
set.remove(20);
|
set.remove(20);
|
||||||
console.log(set.has(20)); // false
|
console.log(set.has(20)); // false
|
||||||
|
|
||||||
// Add multiple elements and check dense array integrity
|
// Iteration (dense order)
|
||||||
set.add(30, 300);
|
for (const entry of set) {
|
||||||
set.add(40, 400);
|
console.log(entry.id, entry.value);
|
||||||
set.remove(10);
|
}
|
||||||
console.log(set.get(30)); // 300
|
|
||||||
console.log(set.get(40)); // 400
|
// 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-image]: https://img.shields.io/npm/v/ts-sparse-set.svg?style=flat-square
|
||||||
[npm-url]: https://npmjs.org/package/ts-sparse-set
|
[npm-url]: https://npmjs.org/package/ts-sparse-set
|
||||||
|
|
|
||||||
|
|
@ -89,4 +89,142 @@ describe("SparseSet", () => {
|
||||||
expect(set.get(i)).toBeNull();
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
export class SparseSet<V> {
|
||||||
private sparse: number[] = [];
|
private sparse: number[] = [];
|
||||||
private dense: number[] = [];
|
private dense: number[] = [];
|
||||||
private data: V[] = [];
|
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 {
|
public has(id: number): boolean {
|
||||||
const index = this.sparse[id];
|
const index = this.sparse[id];
|
||||||
return (
|
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 {
|
public get(id: number): V | null {
|
||||||
const index = this.sparse[id];
|
const index = this.sparse[id];
|
||||||
if (
|
if (
|
||||||
|
|
@ -24,6 +48,14 @@ export class SparseSet<V> {
|
||||||
return this.data[index];
|
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 {
|
public add(id: number, value: V): V {
|
||||||
const existing = this.sparse[id];
|
const existing = this.sparse[id];
|
||||||
if (
|
if (
|
||||||
|
|
@ -48,6 +80,12 @@ export class SparseSet<V> {
|
||||||
return value;
|
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 {
|
public remove(id: number): void {
|
||||||
const index = this.sparse[id];
|
const index = this.sparse[id];
|
||||||
if (
|
if (
|
||||||
|
|
@ -71,4 +109,106 @@ export class SparseSet<V> {
|
||||||
this.data.pop();
|
this.data.pop();
|
||||||
delete this.sparse[id];
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue