Skip to content

Commit a7d180c

Browse files
committed
feat: improving traverse
Adding options for DFS and BFS traversal. Also, adding options for pre and post order yielding
1 parent 0b50b7e commit a7d180c

10 files changed

Lines changed: 533 additions & 36 deletions

File tree

libs/js-tuple/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,22 @@ function createKey<T extends readonly unknown[]>(elements: T): Readonly<T> {
209209
- **Object Identity**: Objects are compared by reference, not deep equality
210210
- **Modern Environments**: Requires WeakRef support (Node.js 14.6+, modern browsers)
211211

212+
## Advanced Usage
213+
214+
For complex scenarios—such as custom traversal orders, subtree iteration, or post-order cleanup—js-tuple provides highly flexible traversal APIs for both `NestedMap` and `NestedSet`. You can choose between depth-first and breadth-first traversal, and between pre-order and post-order yielding, to match your algorithm's needs.
215+
216+
- **NestedMap:** See [Advanced NestedMap.entries](./docs/nestedmap-entries-advanced.md) for details on traversal modes, yield order, edge cases, and performance considerations.
217+
- **NestedSet:** See [Advanced NestedSet](./docs/nestedset-advanced.md) for set-specific traversal, subtree operations, and advanced patterns.
218+
219+
These guides cover:
220+
- How to use `basePath` for partial/subtree traversal
221+
- Differences between DFS/BFS and pre/post order
222+
- Edge cases (missing values, empty subtrees)
223+
- Performance and memory tradeoffs
224+
225+
If you need to implement advanced algorithms, process hierarchical data, or optimize traversal, start with these docs!
226+
227+
212228
## Browser Support
213229

214230
- **Node.js**: 14.6.0+
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Advanced Usage: `NestedMap.entries`
2+
3+
## Overview
4+
`NestedMap.entries` provides powerful and flexible iteration over nested keys and values. It supports multiple traversal modes and orderings, making it suitable for advanced tree and graph algorithms.
5+
6+
## Traversal Modes
7+
- **Depth-First (DFS):** Explores as deep as possible before backtracking. Use for recursive-like traversals.
8+
- **Breadth-First (BFS):** Explores all nodes at the current level before moving deeper. Use for shortest-path or level-order traversals.
9+
10+
## Yield Modes
11+
- **Pre-Order:** Yields a node before its children. Typical for copying or serializing trees.
12+
- **Post-Order:** Yields a node after all its children. Useful for cleanup, deletion, or dependency resolution.
13+
14+
## API Example
15+
```typescript
16+
for (const [key, value] of map.entries({
17+
traverseMode: TraverseMode.DepthFirst, // or BreadthFirst
18+
yieldMode: YieldMode.PreOrder, // or PostOrder
19+
})) {
20+
// key: array representing the path
21+
// value: stored value
22+
}
23+
```
24+
25+
## Particularities
26+
- **Flexible Key Paths:** Keys are always arrays, representing the full path in the nested structure.
27+
- **Partial Traversal:** You can start traversal from any subpath using `basePath`.
28+
- **Custom Traversal:** Combine `traverseMode` and `yieldMode` for custom iteration order.
29+
- **Performance:**
30+
- DFS uses a stack (LIFO), BFS uses a queue (FIFO).
31+
- BFS post-order requires temporary storage (O(N) memory) to yield nodes in reverse.
32+
- DFS post-order is naturally recursive and memory-efficient.
33+
- **Edge Cases:**
34+
- If a node has no value but has children, only children are yielded.
35+
- If `basePath` does not exist, iteration yields nothing.
36+
37+
## Advanced Patterns
38+
- **Subtree Traversal:**
39+
```typescript
40+
for (const [key, value] of map.entries({ basePath: [1, 2] })) {
41+
// Traverse only the subtree rooted at [1, 2]
42+
}
43+
```
44+
- **Post-Order Cleanup:**
45+
```typescript
46+
for (const [key, value] of map.entries({ yieldMode: YieldMode.PostOrder })) {
47+
// Safely delete or process children before parents
48+
}
49+
```
50+
- **Level-Order Processing:**
51+
```typescript
52+
for (const [key, value] of map.entries({ traverseMode: TraverseMode.BreadthFirst })) {
53+
// Process nodes level by level
54+
}
55+
```
56+
57+
## Summary
58+
`NestedMap.entries` is highly customizable for advanced iteration needs. Choose traversal and yield modes based on your algorithm requirements, and be aware of memory/performance tradeoffs for post-order BFS.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Advanced Usage: `NestedSet`
2+
3+
## Overview
4+
`NestedSet` is a set-like structure for storing and traversing nested keys (arrays as paths). It supports advanced traversal options, making it ideal for hierarchical data, trees, and graphs.
5+
6+
## Traversal Modes
7+
- **Depth-First (DFS):** Explores as deep as possible before backtracking. Use for recursive-like traversals.
8+
- **Breadth-First (BFS):** Explores all nodes at the current level before moving deeper. Use for level-order or shortest-path traversals.
9+
10+
## Yield Modes
11+
- **Pre-Order:** Yields a node before its children. Useful for copying, exporting, or visiting parents first.
12+
- **Post-Order:** Yields a node after all its children. Useful for cleanup, deletion, or dependency resolution.
13+
14+
## API Example
15+
```typescript
16+
for (const [key] of set.entries({
17+
traverseMode: TraverseMode.DepthFirst, // or BreadthFirst
18+
yieldMode: YieldMode.PreOrder, // or PostOrder
19+
})) {
20+
// key: array representing the path
21+
}
22+
```
23+
24+
## Particularities
25+
- **Flexible Key Paths:** Keys are always arrays, representing the full path in the nested structure.
26+
- **Partial Traversal:** You can start traversal from any subpath using `basePath`.
27+
- **Custom Traversal:** Combine `traverseMode` and `yieldMode` for custom iteration order.
28+
- **Performance:**
29+
- DFS uses a stack (LIFO), BFS uses a queue (FIFO).
30+
- BFS post-order requires temporary storage (O(N) memory) to yield nodes in reverse.
31+
- DFS post-order is naturally recursive and memory-efficient.
32+
- **Edge Cases:**
33+
- If a node has no value but has children, only children are yielded.
34+
- If `basePath` does not exist, iteration yields nothing.
35+
36+
## Advanced Patterns
37+
- **Subtree Traversal:**
38+
```typescript
39+
for (const [key] of set.entries({ basePath: [1, 2] })) {
40+
// Traverse only the subtree rooted at [1, 2]
41+
}
42+
```
43+
- **Post-Order Cleanup:**
44+
```typescript
45+
for (const [key] of set.entries({ yieldMode: YieldMode.PostOrder })) {
46+
// Safely delete or process children before parents
47+
}
48+
```
49+
- **Level-Order Processing:**
50+
```typescript
51+
for (const [key] of set.entries({ traverseMode: TraverseMode.BreadthFirst })) {
52+
// Process nodes level by level
53+
}
54+
```
55+
56+
## Summary
57+
`NestedSet` is highly customizable for advanced iteration needs. Choose traversal and yield modes based on your algorithm requirements, and be aware of memory/performance tradeoffs for post-order BFS.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// A queue implementation for DFS-like traversal, but with queue semantics
2+
// push adds to the end, pop removes from the front
3+
interface Node<T> {
4+
value: T;
5+
next?: Node<T>;
6+
}
7+
8+
export class BfsList<T> {
9+
private head?: Node<T>;
10+
private tail?: Node<T>;
11+
private _length = 0;
12+
13+
constructor(initial: T) {
14+
this.push(initial);
15+
}
16+
17+
push(item: T): void {
18+
const node: Node<T> = { value: item };
19+
if (!this.tail) {
20+
this.head = this.tail = node;
21+
} else {
22+
this.tail.next = node;
23+
this.tail = node;
24+
}
25+
this._length++;
26+
}
27+
28+
pop(): T | undefined {
29+
if (!this.head) return undefined;
30+
const value = this.head.value;
31+
this.head = this.head.next;
32+
if (!this.head) this.tail = undefined;
33+
this._length--;
34+
return value;
35+
}
36+
37+
get length(): number {
38+
return this._length;
39+
}
40+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './bfs-list';

libs/js-tuple/src/nested-map.ts

Lines changed: 140 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { PartialKey } from './types';
1+
import { BfsList } from './internal';
2+
import { IterationOptions, PartialKey, TraverseMode, YieldMode } from './types';
23

34
const VAL = Symbol('value');
45
const SET = Symbol('valueSet');
@@ -10,10 +11,119 @@ type CacheNode = {
1011
[VAL]?: unknown;
1112
};
1213

14+
type PathArray = readonly unknown[];
15+
16+
interface StackItem {
17+
node: CacheNode;
18+
path: PathArray;
19+
visited?: boolean;
20+
}
21+
1322
function treatKey<K>(nestedKey: K) {
1423
return Array.isArray(nestedKey) ? nestedKey : [nestedKey];
1524
}
1625

26+
function getValueFactory<K, V, T extends boolean>(justValue: T) {
27+
return justValue
28+
? ({ node }: StackItem) => node[VAL] as T extends false ? [K, V] : V
29+
: ({ path, node }: StackItem) =>
30+
[path as K, node[VAL] as V] as T extends false ? [K, V] : V;
31+
}
32+
33+
const EMPTY: PathArray = Object.freeze([]);
34+
35+
function pushToStack<T extends boolean>(
36+
stack: StackItem[] | BfsList<StackItem>,
37+
justValue: T,
38+
stackItem: StackItem,
39+
) {
40+
const { node, path } = stackItem;
41+
if (!node[MAP]?.size) return;
42+
for (const [key, sub] of node[MAP].entries()) {
43+
stack.push({
44+
node: sub,
45+
path: justValue ? EMPTY : Object.freeze([...path, key]),
46+
});
47+
}
48+
}
49+
50+
const traverser = {
51+
[TraverseMode.BreadthFirst]: {
52+
*[YieldMode.PostOrder]<K, V, T extends boolean>(
53+
root: CacheNode,
54+
prevPath: PathArray,
55+
justValue: T,
56+
): MapIterator<T extends false ? [K, V] : V> {
57+
const getValue = getValueFactory<K, V, T>(justValue);
58+
function* bfsPostOrderLevel(
59+
nodes: Array<StackItem>,
60+
): MapIterator<T extends false ? [K, V] : V> {
61+
const nextLevel: StackItem[] = [];
62+
for (const stackItem of nodes) {
63+
pushToStack(nextLevel, justValue, stackItem);
64+
}
65+
if (nextLevel.length) yield* bfsPostOrderLevel(nextLevel);
66+
for (const stackItem of nodes) {
67+
if (stackItem.node[SET]) yield getValue(stackItem);
68+
}
69+
}
70+
yield* bfsPostOrderLevel([{ node: root, path: prevPath }]);
71+
},
72+
73+
*[YieldMode.PreOrder]<K, V, T extends boolean>(
74+
root: CacheNode,
75+
prevPath: PathArray,
76+
justValue: T,
77+
): MapIterator<T extends false ? [K, V] : V> {
78+
const queue = new BfsList({ node: root, path: prevPath });
79+
const getValue = getValueFactory<K, V, T>(justValue);
80+
do {
81+
const stackItem = queue.pop() as StackItem;
82+
if (stackItem.node[SET]) yield getValue(stackItem);
83+
pushToStack(queue, justValue, stackItem);
84+
} while (queue.length > 0);
85+
},
86+
},
87+
88+
[TraverseMode.DepthFirst]: {
89+
*[YieldMode.PostOrder]<K, V, T extends boolean>(
90+
root: CacheNode,
91+
prevPath: unknown[],
92+
justValue: T,
93+
): MapIterator<T extends false ? [K, V] : V> {
94+
const stack: StackItem[] = [{ node: root, path: prevPath }];
95+
const getValue = getValueFactory<K, V, T>(justValue);
96+
do {
97+
const current = stack.pop() as StackItem;
98+
const { node } = current;
99+
if (current.visited) {
100+
if (node[SET]) yield getValue(current);
101+
} else if (!node[MAP]?.size) {
102+
if (node[SET]) yield getValue(current);
103+
} else {
104+
current.visited = true;
105+
stack.push(current);
106+
pushToStack(stack, justValue, current);
107+
}
108+
} while (stack.length > 0);
109+
},
110+
111+
*[YieldMode.PreOrder]<K, V, T extends boolean>(
112+
root: CacheNode,
113+
prevPath: readonly unknown[],
114+
justValue: T,
115+
): MapIterator<T extends false ? [K, V] : V> {
116+
const stack: Array<StackItem> = [{ node: root, path: prevPath }];
117+
const getValue = getValueFactory<K, V, T>(justValue);
118+
do {
119+
const current = stack.pop() as StackItem;
120+
if (current.node[SET]) yield getValue(current);
121+
pushToStack(stack, justValue, current);
122+
} while (stack.length > 0);
123+
},
124+
},
125+
};
126+
17127
/**
18128
* A Map implementation that uses arrays as keys by storing them in a nested Map structure.
19129
*
@@ -231,55 +341,58 @@ export class NestedMap<K, V> {
231341
}
232342
}
233343

234-
/**
235-
* Returns an iterator of key-value pairs.
236-
*/
237-
*entries(basePath?: PartialKey<K>): IterableIterator<[K, V]> {
344+
#traverse<T extends boolean>(
345+
justValue: T,
346+
options: IterationOptions<K> = {},
347+
): MapIterator<T extends false ? [K, V] : V> {
348+
const {
349+
basePath,
350+
traverseMode = TraverseMode.DepthFirst,
351+
yieldMode = YieldMode.PreOrder,
352+
} = options;
238353
let root: CacheNode | undefined;
239354
const prevPath: unknown[] = [];
240355
if (basePath) {
241356
root = this._getNode(basePath);
242-
if (!root) return;
357+
if (!root) {
358+
return traverser[TraverseMode.BreadthFirst][yieldMode](
359+
{},
360+
EMPTY,
361+
justValue,
362+
);
363+
} // empty iterator
243364
if (Array.isArray(basePath)) prevPath.push(...basePath);
244365
else prevPath.push(basePath);
245366
} else root = this._root;
246-
// Stack to store {node, path} pairs for traversal
247-
const stack = [{ node: root, path: prevPath }];
248-
249-
while (stack.length > 0) {
250-
const { node, path } = stack.pop() as {
251-
node: CacheNode;
252-
path: unknown[];
253-
};
254-
255-
for (const [key, sub] of node[MAP]?.entries() ?? []) {
256-
const newPath = [...path, key];
257-
if (sub[SET]) yield [newPath as unknown as K, sub[VAL] as V];
258-
// Add to stack for later processing (LIFO order maintains depth-first traversal)
259-
if (sub[MAP]) stack.push({ node: sub, path: newPath });
260-
}
261-
}
367+
return traverser[traverseMode][yieldMode](root, prevPath, justValue);
368+
}
369+
370+
/**
371+
* Returns an iterator of key-value pairs.
372+
*/
373+
entries(options: IterationOptions<K> = {}): IterableIterator<[K, V]> {
374+
return this.#traverse(false, options);
262375
}
263376

264377
/**
265378
* Returns an iterator of keys.
266379
*/
267-
*keys(basePath?: PartialKey<K>): MapIterator<K> {
268-
for (const [key] of this.entries(basePath)) yield key;
380+
*keys(options?: IterationOptions<K>): MapIterator<K> {
381+
for (const [key] of this.entries(options)) yield key;
269382
}
270383

271384
/**
272385
* Returns an iterator of values.
273386
*/
274-
*values(basePath?: PartialKey<K>): MapIterator<V> {
275-
for (const [, value] of this.entries(basePath)) yield value;
387+
values(options: IterationOptions<K> = {}): MapIterator<V> {
388+
return this.#traverse(true, options);
276389
}
277390

278391
/**
279392
* Returns the default iterator (same as entries()).
280393
*/
281394
[Symbol.iterator](): IterableIterator<[K, V]> {
282-
return this.entries();
395+
return this.#traverse(false, {});
283396
}
284397

285398
/**

0 commit comments

Comments
 (0)