data-structure-typed 2.4.3 → 2.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.github/workflows/release.yml +27 -0
  2. package/CHANGELOG.md +24 -1
  3. package/README.md +70 -51
  4. package/dist/cjs/index.cjs +486 -167
  5. package/dist/cjs-legacy/index.cjs +487 -165
  6. package/dist/esm/index.mjs +486 -168
  7. package/dist/esm-legacy/index.mjs +487 -166
  8. package/dist/types/common/error.d.ts +23 -0
  9. package/dist/types/common/index.d.ts +1 -0
  10. package/dist/types/data-structures/base/iterable-element-base.d.ts +1 -1
  11. package/dist/types/data-structures/binary-tree/binary-tree.d.ts +15 -5
  12. package/dist/types/data-structures/binary-tree/bst.d.ts +1 -1
  13. package/dist/types/data-structures/binary-tree/red-black-tree.d.ts +7 -1
  14. package/dist/types/data-structures/graph/abstract-graph.d.ts +44 -0
  15. package/dist/types/data-structures/graph/directed-graph.d.ts +3 -2
  16. package/dist/types/data-structures/graph/undirected-graph.d.ts +16 -2
  17. package/dist/types/data-structures/hash/hash-map.d.ts +2 -2
  18. package/dist/types/data-structures/heap/heap.d.ts +3 -7
  19. package/dist/types/data-structures/queue/deque.d.ts +41 -1
  20. package/dist/types/types/data-structures/binary-tree/avl-tree.d.ts +1 -1
  21. package/dist/types/types/data-structures/binary-tree/red-black-tree.d.ts +1 -1
  22. package/dist/types/types/data-structures/linked-list/doubly-linked-list.d.ts +1 -1
  23. package/dist/types/types/data-structures/linked-list/singly-linked-list.d.ts +1 -1
  24. package/dist/types/types/data-structures/priority-queue/priority-queue.d.ts +1 -1
  25. package/dist/types/types/data-structures/queue/deque.d.ts +6 -0
  26. package/dist/types/types/data-structures/stack/stack.d.ts +1 -1
  27. package/dist/umd/data-structure-typed.js +486 -164
  28. package/dist/umd/data-structure-typed.min.js +6 -4
  29. package/package.json +2 -2
  30. package/src/common/error.ts +60 -0
  31. package/src/common/index.ts +2 -0
  32. package/src/data-structures/base/iterable-element-base.ts +5 -4
  33. package/src/data-structures/binary-tree/binary-indexed-tree.ts +6 -5
  34. package/src/data-structures/binary-tree/binary-tree.ts +121 -49
  35. package/src/data-structures/binary-tree/bst.ts +12 -4
  36. package/src/data-structures/binary-tree/red-black-tree.ts +20 -0
  37. package/src/data-structures/binary-tree/tree-map.ts +8 -7
  38. package/src/data-structures/binary-tree/tree-multi-map.ts +4 -4
  39. package/src/data-structures/binary-tree/tree-multi-set.ts +10 -9
  40. package/src/data-structures/binary-tree/tree-set.ts +7 -6
  41. package/src/data-structures/graph/abstract-graph.ts +124 -19
  42. package/src/data-structures/graph/directed-graph.ts +8 -4
  43. package/src/data-structures/graph/map-graph.ts +1 -1
  44. package/src/data-structures/graph/undirected-graph.ts +99 -4
  45. package/src/data-structures/hash/hash-map.ts +19 -6
  46. package/src/data-structures/heap/heap.ts +21 -17
  47. package/src/data-structures/heap/max-heap.ts +2 -3
  48. package/src/data-structures/linked-list/doubly-linked-list.ts +4 -4
  49. package/src/data-structures/linked-list/singly-linked-list.ts +15 -9
  50. package/src/data-structures/matrix/matrix.ts +9 -10
  51. package/src/data-structures/priority-queue/max-priority-queue.ts +2 -3
  52. package/src/data-structures/queue/deque.ts +72 -4
  53. package/src/data-structures/stack/stack.ts +1 -1
  54. package/src/data-structures/trie/trie.ts +12 -6
  55. package/src/types/data-structures/binary-tree/avl-tree.ts +1 -1
  56. package/src/types/data-structures/binary-tree/red-black-tree.ts +1 -1
  57. package/src/types/data-structures/linked-list/doubly-linked-list.ts +1 -1
  58. package/src/types/data-structures/linked-list/singly-linked-list.ts +1 -1
  59. package/src/types/data-structures/priority-queue/priority-queue.ts +1 -1
  60. package/src/types/data-structures/queue/deque.ts +7 -0
  61. package/src/types/data-structures/stack/stack.ts +1 -1
  62. package/src/utils/utils.ts +4 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "data-structure-typed",
3
- "version": "2.4.3",
3
+ "version": "2.4.5",
4
4
  "description": "Standard data structure",
5
5
  "browser": "dist/umd/data-structure-typed.min.js",
6
6
  "umd:main": "dist/umd/data-structure-typed.min.js",
@@ -105,7 +105,7 @@
105
105
  "benchmark": "^2.1.4",
106
106
  "binary-tree-typed": "^1.54.3",
107
107
  "bst-typed": "^1.54.3",
108
- "data-structure-typed": "^2.4.2",
108
+ "data-structure-typed": "^2.4.4",
109
109
  "dependency-cruiser": "^16.5.0",
110
110
  "doctoc": "^2.2.1",
111
111
  "eslint": "^9.13.0",
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Centralized error message templates.
3
+ * Keep using native Error/TypeError/RangeError — this only standardizes messages.
4
+ */
5
+ export const ERR = {
6
+ // Range / index
7
+ indexOutOfRange: (index: number, min: number, max: number, ctx?: string) =>
8
+ `${ctx ? ctx + ': ' : ''}Index ${index} is out of range [${min}, ${max}].`,
9
+
10
+ invalidIndex: (ctx?: string) =>
11
+ `${ctx ? ctx + ': ' : ''}Index must be an integer.`,
12
+
13
+ // Type / argument
14
+ invalidArgument: (reason: string, ctx?: string) =>
15
+ `${ctx ? ctx + ': ' : ''}${reason}`,
16
+
17
+ comparatorRequired: (ctx?: string) =>
18
+ `${ctx ? ctx + ': ' : ''}Comparator is required for non-number/non-string/non-Date keys.`,
19
+
20
+ invalidKey: (reason: string, ctx?: string) =>
21
+ `${ctx ? ctx + ': ' : ''}${reason}`,
22
+
23
+ notAFunction: (name: string, ctx?: string) =>
24
+ `${ctx ? ctx + ': ' : ''}${name} must be a function.`,
25
+
26
+ invalidEntry: (ctx?: string) =>
27
+ `${ctx ? ctx + ': ' : ''}Each entry must be a [key, value] tuple.`,
28
+
29
+ invalidNaN: (ctx?: string) =>
30
+ `${ctx ? ctx + ': ' : ''}NaN is not a valid key.`,
31
+
32
+ invalidDate: (ctx?: string) =>
33
+ `${ctx ? ctx + ': ' : ''}Invalid Date key.`,
34
+
35
+ reduceEmpty: (ctx?: string) =>
36
+ `${ctx ? ctx + ': ' : ''}Reduce of empty structure with no initial value.`,
37
+
38
+ callbackReturnType: (expected: string, got: string, ctx?: string) =>
39
+ `${ctx ? ctx + ': ' : ''}Callback must return ${expected}; got ${got}.`,
40
+
41
+ // State / operation
42
+ invalidOperation: (reason: string, ctx?: string) =>
43
+ `${ctx ? ctx + ': ' : ''}${reason}`,
44
+
45
+ // Matrix
46
+ matrixDimensionMismatch: (op: string) =>
47
+ `Matrix: Dimensions must be compatible for ${op}.`,
48
+
49
+ matrixSingular: () =>
50
+ 'Matrix: Singular matrix, inverse does not exist.',
51
+
52
+ matrixNotSquare: () =>
53
+ 'Matrix: Must be square for inversion.',
54
+
55
+ matrixNotRectangular: () =>
56
+ 'Matrix: Must be rectangular for transposition.',
57
+
58
+ matrixRowMismatch: (expected: number, got: number) =>
59
+ `Matrix: Expected row length ${expected}, but got ${got}.`
60
+ } as const;
@@ -1,3 +1,5 @@
1
+ export { ERR } from './error';
2
+
1
3
  export enum DFSOperation {
2
4
  VISIT = 0,
3
5
  PROCESS = 1
@@ -1,4 +1,5 @@
1
1
  import type { ElementCallback, IterableElementBaseOptions, ReduceElementCallback } from '../../types';
2
+ import { ERR } from '../../common';
2
3
 
3
4
  /**
4
5
  * Base class that makes a data structure iterable and provides common
@@ -25,7 +26,7 @@ export abstract class IterableElementBase<E, R> implements Iterable<E> {
25
26
  if (options) {
26
27
  const { toElementFn } = options;
27
28
  if (typeof toElementFn === 'function') this._toElementFn = toElementFn;
28
- else if (toElementFn) throw new TypeError('toElementFn must be a function type');
29
+ else if (toElementFn) throw new TypeError(ERR.notAFunction('toElementFn'));
29
30
  }
30
31
  }
31
32
 
@@ -35,7 +36,7 @@ export abstract class IterableElementBase<E, R> implements Iterable<E> {
35
36
  * @remarks
36
37
  * Time O(1), Space O(1).
37
38
  */
38
- protected _toElementFn?: (rawElement: R) => E;
39
+ protected readonly _toElementFn?: (rawElement: R) => E;
39
40
 
40
41
  /**
41
42
  * Exposes the current `toElementFn`, if configured.
@@ -224,12 +225,12 @@ export abstract class IterableElementBase<E, R> implements Iterable<E> {
224
225
  acc = initialValue as U;
225
226
  } else {
226
227
  const first = iter.next();
227
- if (first.done) throw new TypeError('Reduce of empty structure with no initial value');
228
+ if (first.done) throw new TypeError(ERR.reduceEmpty());
228
229
  acc = first.value as unknown as U;
229
230
  index = 1;
230
231
  }
231
232
 
232
- for (const value of iter as unknown as Iterable<E>) {
233
+ for (const value of iter) {
233
234
  acc = callbackfn(acc, value, index++, this);
234
235
  }
235
236
  return acc;
@@ -6,6 +6,7 @@
6
6
  * @license MIT License
7
7
  */
8
8
  import { getMSB } from '../../utils';
9
+ import { ERR } from '../../common';
9
10
 
10
11
  /**
11
12
  *
@@ -124,7 +125,7 @@ export class BinaryIndexedTree {
124
125
  */
125
126
  read(count: number): number {
126
127
  if (!Number.isInteger(count)) {
127
- throw new Error('Invalid count');
128
+ throw new Error(ERR.invalidArgument('count must be an integer', 'BinaryIndexedTree'));
128
129
  }
129
130
  return this._read(Math.max(Math.min(count, this.max), 0));
130
131
  }
@@ -137,7 +138,7 @@ export class BinaryIndexedTree {
137
138
  */
138
139
  lowerBound(sum: number): number {
139
140
  if (this.negativeCount > 0) {
140
- throw new Error('Sequence is not non-descending');
141
+ throw new Error(ERR.invalidOperation('Sequence is not non-descending.', 'BinaryIndexedTree'));
141
142
  }
142
143
  return this._binarySearch(sum, (x, y) => x < y);
143
144
  }
@@ -151,7 +152,7 @@ export class BinaryIndexedTree {
151
152
  */
152
153
  upperBound(sum: number): number {
153
154
  if (this.negativeCount > 0) {
154
- throw new Error('Must not be descending');
155
+ throw new Error(ERR.invalidOperation('Sequence must not be descending.', 'BinaryIndexedTree'));
155
156
  }
156
157
  return this._binarySearch(sum, (x, y) => x <= y);
157
158
  }
@@ -209,10 +210,10 @@ export class BinaryIndexedTree {
209
210
  */
210
211
  protected _checkIndex(index: number): void {
211
212
  if (!Number.isInteger(index)) {
212
- throw new Error('Invalid index: Index must be an integer.');
213
+ throw new TypeError(ERR.invalidIndex('BinaryIndexedTree'));
213
214
  }
214
215
  if (index < 0 || index >= this.max) {
215
- throw new Error('Index out of range: Index must be within the range [0, this.max).');
216
+ throw new RangeError(ERR.indexOutOfRange(index, 0, this.max - 1, 'BinaryIndexedTree'));
216
217
  }
217
218
  }
218
219
 
@@ -28,7 +28,7 @@ import { IBinaryTree } from '../../interfaces';
28
28
  import { isComparable, makeTrampoline, makeTrampolineThunk } from '../../utils';
29
29
  import { Queue } from '../queue';
30
30
  import { IterableEntryBase } from '../base';
31
- import { DFSOperation, Range } from '../../common';
31
+ import { DFSOperation, ERR, Range } from '../../common';
32
32
 
33
33
  /**
34
34
  * @template K - The type of the key.
@@ -371,13 +371,13 @@ export class BinaryTree<K = any, V = any, R = any>
371
371
  if (isMapMode !== undefined) this._isMapMode = isMapMode;
372
372
  if (isDuplicate !== undefined) this._isDuplicate = isDuplicate;
373
373
  if (typeof toEntryFn === 'function') this._toEntryFn = toEntryFn;
374
- else if (toEntryFn) throw TypeError('toEntryFn must be a function type');
374
+ else if (toEntryFn) throw new TypeError(ERR.notAFunction('toEntryFn', 'BinaryTree'));
375
375
  }
376
376
 
377
377
  if (keysNodesEntriesOrRaws) this.setMany(keysNodesEntriesOrRaws);
378
378
  }
379
379
 
380
- protected _isMapMode = true;
380
+ protected readonly _isMapMode: boolean = true;
381
381
 
382
382
  /**
383
383
  * Gets whether the tree is in Map mode.
@@ -389,7 +389,7 @@ export class BinaryTree<K = any, V = any, R = any>
389
389
  return this._isMapMode;
390
390
  }
391
391
 
392
- protected _isDuplicate = false;
392
+ protected readonly _isDuplicate: boolean = false;
393
393
 
394
394
  /**
395
395
  * Gets whether the tree allows duplicate keys.
@@ -440,7 +440,7 @@ export class BinaryTree<K = any, V = any, R = any>
440
440
  return this._size;
441
441
  }
442
442
 
443
- protected _NIL: BinaryTreeNode<K, V> = new BinaryTreeNode<K, V>(NaN as K) as unknown as BinaryTreeNode<K, V>;
443
+ protected readonly _NIL = new BinaryTreeNode<K, V>(NaN as K);
444
444
 
445
445
  /**
446
446
  * Gets the sentinel NIL node (used in self-balancing trees like Red-Black Tree).
@@ -452,7 +452,7 @@ export class BinaryTree<K = any, V = any, R = any>
452
452
  return this._NIL;
453
453
  }
454
454
 
455
- protected _toEntryFn?: ToEntryFn<K, V, R>;
455
+ protected readonly _toEntryFn?: ToEntryFn<K, V, R>;
456
456
 
457
457
  /**
458
458
  * Gets the function used to convert raw data objects (R) into [key, value] entries.
@@ -670,7 +670,7 @@ export class BinaryTree<K = any, V = any, R = any>
670
670
  if (!this._root) {
671
671
  this._setRoot(newNode);
672
672
  if (this._isMapMode && newNode !== null && newNode !== undefined) this._store.set(newNode.key, newNode);
673
- this._size = 1;
673
+ if (newNode !== null) this._size = 1;
674
674
  return true;
675
675
  }
676
676
 
@@ -708,7 +708,7 @@ export class BinaryTree<K = any, V = any, R = any>
708
708
  potentialParent.right = newNode;
709
709
  }
710
710
  if (this._isMapMode && newNode !== null && newNode !== undefined) this._store.set(newNode.key, newNode);
711
- this._size++;
711
+ if (newNode !== null) this._size++;
712
712
  return true;
713
713
  }
714
714
 
@@ -1014,7 +1014,7 @@ export class BinaryTree<K = any, V = any, R = any>
1014
1014
  ): BinaryTreeNode<K, V> | null | undefined {
1015
1015
  if (this._isMapMode && keyNodeEntryOrPredicate !== null && keyNodeEntryOrPredicate !== undefined) {
1016
1016
  if (!this._isPredicate(keyNodeEntryOrPredicate)) {
1017
- const key = this._extractKey(keyNodeEntryOrPredicate as any);
1017
+ const key = this._extractKey(keyNodeEntryOrPredicate);
1018
1018
  if (key === null || key === undefined) return;
1019
1019
  return this._store.get(key);
1020
1020
  }
@@ -1078,7 +1078,7 @@ export class BinaryTree<K = any, V = any, R = any>
1078
1078
  ): boolean {
1079
1079
  if (this._isMapMode && keyNodeEntryOrPredicate !== undefined && keyNodeEntryOrPredicate !== null) {
1080
1080
  if (!this._isPredicate(keyNodeEntryOrPredicate)) {
1081
- const key = this._extractKey(keyNodeEntryOrPredicate as any);
1081
+ const key = this._extractKey(keyNodeEntryOrPredicate);
1082
1082
  if (key === null || key === undefined) return false;
1083
1083
  return this._store.has(key);
1084
1084
  }
@@ -1605,7 +1605,7 @@ export class BinaryTree<K = any, V = any, R = any>
1605
1605
 
1606
1606
  /**
1607
1607
  * Finds all leaf nodes in the tree.
1608
- * @remarks Time O(N), visits every node. Space O(H) for recursive stack or O(N) for iterative queue.
1608
+ * @remarks Time O(N), visits every node. Space O(H) for recursive or iterative stack.
1609
1609
  *
1610
1610
  * @template C - The type of the callback function.
1611
1611
  * @param [callback=this._DEFAULT_NODE_CALLBACK] - Function to call on each leaf node.
@@ -1636,17 +1636,18 @@ export class BinaryTree<K = any, V = any, R = any>
1636
1636
 
1637
1637
  dfs(startNode);
1638
1638
  } else {
1639
- // BFS-based
1640
- const queue = new Queue([startNode]);
1639
+ // DFS-based (stack) to match recursive order
1640
+ const stack = [startNode];
1641
1641
 
1642
- while (queue.length > 0) {
1643
- const cur = queue.shift();
1642
+ while (stack.length > 0) {
1643
+ const cur = stack.pop()!;
1644
1644
  if (this.isRealNode(cur)) {
1645
1645
  if (this.isLeaf(cur)) {
1646
1646
  leaves.push(callback(cur));
1647
1647
  }
1648
- if (this.isRealNode(cur.left)) queue.push(cur.left);
1649
- if (this.isRealNode(cur.right)) queue.push(cur.right);
1648
+ // Push right first so left is processed first (LIFO)
1649
+ if (this.isRealNode(cur.right)) stack.push(cur.right);
1650
+ if (this.isRealNode(cur.left)) stack.push(cur.left);
1650
1651
  }
1651
1652
  }
1652
1653
  }
@@ -2136,7 +2137,8 @@ export class BinaryTree<K = any, V = any, R = any>
2136
2137
  * @param node - The node.
2137
2138
  * @returns The node's key or undefined.
2138
2139
  */
2139
- protected _DEFAULT_NODE_CALLBACK = (node: BinaryTreeNode<K, V> | null | undefined) => (node ? node.key : undefined);
2140
+ protected readonly _DEFAULT_NODE_CALLBACK: NodeCallback<BinaryTreeNode<K, V> | null | undefined, K | undefined> =
2141
+ (node): K | undefined => node?.key;
2140
2142
 
2141
2143
  /**
2142
2144
  * (Protected) Snapshots the current tree's configuration options.
@@ -2257,40 +2259,60 @@ export class BinaryTree<K = any, V = any, R = any>
2257
2259
  options: BinaryTreePrintOptions
2258
2260
  ): NodeDisplayLayout {
2259
2261
  const { isShowNull, isShowUndefined, isShowRedBlackNIL } = options;
2260
- const emptyDisplayLayout = <NodeDisplayLayout>[['─'], 1, 0, 0]; // Represents an empty spot
2261
-
2262
- if (node === null && !isShowNull) {
2263
- return emptyDisplayLayout;
2264
- } else if (node === undefined && !isShowUndefined) {
2265
- return emptyDisplayLayout;
2266
- } else if (this.isNIL(node) && !isShowRedBlackNIL) {
2267
- return emptyDisplayLayout;
2268
- } else if (node !== null && node !== undefined) {
2269
- // Real node
2270
- const key = node.key,
2271
- line = this.isNIL(node) ? 'S' : String(key),
2272
- width = line.length;
2273
-
2274
- return _buildNodeDisplay(
2275
- line,
2276
- width,
2277
- this._displayAux(node.left, options),
2278
- this._displayAux(node.right, options)
2279
- );
2280
- } else {
2281
- // Null or Undefined
2282
- const line = node === undefined ? 'U' : 'N',
2283
- width = line.length;
2262
+ const emptyDisplayLayout = <NodeDisplayLayout>[['─'], 1, 0, 0];
2263
+
2264
+ // Iterative post-order: compute display layout bottom-up using an explicit stack.
2265
+ // Stages: 0=process left, 1=process right, 2=merge
2266
+ type Frame = {
2267
+ node: BinaryTreeNode<K, V> | null | undefined;
2268
+ stage: 0 | 1 | 2;
2269
+ leftLayout: NodeDisplayLayout;
2270
+ rightLayout: NodeDisplayLayout;
2271
+ };
2272
+
2273
+ const newFrame = (n: BinaryTreeNode<K, V> | null | undefined): Frame => ({
2274
+ node: n, stage: 0, leftLayout: emptyDisplayLayout, rightLayout: emptyDisplayLayout
2275
+ });
2284
2276
 
2285
- // Treat as a leaf
2286
- return _buildNodeDisplay(line, width, [[''], 1, 0, 0], [[''], 1, 0, 0]);
2277
+ const stack: Frame[] = [newFrame(node)];
2278
+ let result: NodeDisplayLayout = emptyDisplayLayout;
2279
+
2280
+ const setChildResult = (layout: NodeDisplayLayout) => {
2281
+ if (stack.length === 0) { result = layout; return; }
2282
+ const parent = stack[stack.length - 1];
2283
+ if (parent.stage === 1) parent.leftLayout = layout;
2284
+ else parent.rightLayout = layout;
2285
+ };
2286
+
2287
+ while (stack.length > 0) {
2288
+ const frame = stack[stack.length - 1];
2289
+ const cur = frame.node;
2290
+
2291
+ if (frame.stage === 0) {
2292
+ // Leaf / empty node — resolve immediately
2293
+ if (this._isDisplayLeaf(cur, options)) {
2294
+ stack.pop();
2295
+ const layout = this._resolveDisplayLeaf(cur, options, emptyDisplayLayout);
2296
+ setChildResult(layout);
2297
+ continue;
2298
+ }
2299
+ frame.stage = 1;
2300
+ stack.push(newFrame(cur!.left));
2301
+ } else if (frame.stage === 1) {
2302
+ frame.stage = 2;
2303
+ stack.push(newFrame(cur!.right));
2304
+ } else {
2305
+ stack.pop();
2306
+ const line = this.isNIL(cur) ? 'S' : String(cur!.key);
2307
+ const layout = BinaryTree._buildNodeDisplay(line, line.length, frame.leftLayout, frame.rightLayout);
2308
+ setChildResult(layout);
2309
+ }
2287
2310
  }
2288
2311
 
2289
- /**
2290
- * (Inner) Builds the display lines for a node.
2291
- * @remarks Time/Space: Proportional to the width and height of the subtrees.
2292
- */
2293
- function _buildNodeDisplay(line: string, width: number, left: NodeDisplayLayout, right: NodeDisplayLayout) {
2312
+ return result;
2313
+ }
2314
+
2315
+ protected static _buildNodeDisplay(line: string, width: number, left: NodeDisplayLayout, right: NodeDisplayLayout) {
2294
2316
  const [leftLines, leftWidth, leftHeight, leftMiddle] = left;
2295
2317
  const [rightLines, rightWidth, rightHeight, rightMiddle] = right;
2296
2318
  const firstLine =
@@ -2323,7 +2345,57 @@ export class BinaryTree<K = any, V = any, R = any>
2323
2345
  Math.max(leftHeight, rightHeight) + 2,
2324
2346
  leftWidth + Math.floor(width / 2)
2325
2347
  ];
2348
+ }
2349
+
2350
+ /**
2351
+ * Check if a node is a display leaf (empty, null, undefined, NIL, or real leaf).
2352
+ */
2353
+ protected _isDisplayLeaf(
2354
+ node: BinaryTreeNode<K, V> | null | undefined,
2355
+ options: BinaryTreePrintOptions
2356
+ ): boolean {
2357
+ const { isShowNull, isShowUndefined, isShowRedBlackNIL } = options;
2358
+ // Empty/hidden nodes are always leaves
2359
+ if (node === null && !isShowNull) return true;
2360
+ if (node === undefined && !isShowUndefined) return true;
2361
+ if (this.isNIL(node) && !isShowRedBlackNIL) return true;
2362
+ // Shown null/undefined are leaves (no children to recurse into)
2363
+ if (node === null || node === undefined) return true;
2364
+ // Real node: check if it has any children that would be displayed
2365
+ const hasDisplayableLeft = this._hasDisplayableChild(node.left, options);
2366
+ const hasDisplayableRight = this._hasDisplayableChild(node.right, options);
2367
+ return !hasDisplayableLeft && !hasDisplayableRight;
2368
+ }
2369
+
2370
+ protected _hasDisplayableChild(
2371
+ child: BinaryTreeNode<K, V> | null | undefined,
2372
+ options: BinaryTreePrintOptions
2373
+ ): boolean {
2374
+ if (child === null) return !!options.isShowNull;
2375
+ if (child === undefined) return !!options.isShowUndefined;
2376
+ if (this.isNIL(child)) return !!options.isShowRedBlackNIL;
2377
+ return true; // real node is always displayable
2378
+ }
2379
+
2380
+ /**
2381
+ * Resolve a display leaf node to its layout.
2382
+ */
2383
+ protected _resolveDisplayLeaf(
2384
+ node: BinaryTreeNode<K, V> | null | undefined,
2385
+ options: BinaryTreePrintOptions,
2386
+ emptyDisplayLayout: NodeDisplayLayout
2387
+ ): NodeDisplayLayout {
2388
+ const { isShowNull, isShowUndefined, isShowRedBlackNIL } = options;
2389
+ if (node === null && !isShowNull) return emptyDisplayLayout;
2390
+ if (node === undefined && !isShowUndefined) return emptyDisplayLayout;
2391
+ if (this.isNIL(node) && !isShowRedBlackNIL) return emptyDisplayLayout;
2392
+ if (node !== null && node !== undefined) {
2393
+ const line = this.isNIL(node) ? 'S' : String(node.key);
2394
+ return BinaryTree._buildNodeDisplay(line, line.length, emptyDisplayLayout, emptyDisplayLayout);
2326
2395
  }
2396
+ // Shown null / undefined
2397
+ const line = node === undefined ? 'U' : 'N';
2398
+ return BinaryTree._buildNodeDisplay(line, line.length, [[''], 1, 0, 0], [[''], 1, 0, 0]);
2327
2399
  }
2328
2400
 
2329
2401
  /**
@@ -26,7 +26,7 @@ import { BinaryTree } from './binary-tree';
26
26
  import { IBinaryTree } from '../../interfaces';
27
27
  import { Queue } from '../queue';
28
28
  import { isComparable } from '../../utils';
29
- import { Range } from '../../common';
29
+ import { ERR, Range } from '../../common';
30
30
 
31
31
  /**
32
32
  * Represents a Node in a Binary Search Tree.
@@ -391,7 +391,7 @@ export class BST<K = any, V = any, R = any> extends BinaryTree<K, V, R> implemen
391
391
 
392
392
  * @remarks Time O(1) Space O(1)
393
393
  */
394
- protected _comparator: Comparator<K>;
394
+ protected readonly _comparator: Comparator<K>;
395
395
 
396
396
  /**
397
397
  * Gets the comparator function used by the tree.
@@ -1523,10 +1523,18 @@ export class BST<K = any, V = any, R = any> extends BinaryTree<K, V, R> implemen
1523
1523
  return 0;
1524
1524
  }
1525
1525
 
1526
+ // Date keys: compare by getTime()
1527
+ if (a instanceof Date && b instanceof Date) {
1528
+ const ta = a.getTime();
1529
+ const tb = b.getTime();
1530
+ if (Number.isNaN(ta) || Number.isNaN(tb)) throw new TypeError(ERR.invalidDate('BST'));
1531
+ return ta > tb ? 1 : ta < tb ? -1 : 0;
1532
+ }
1533
+
1526
1534
  // If keys are objects and no comparator is provided, throw an error
1527
1535
  if (typeof a === 'object' || typeof b === 'object') {
1528
- throw TypeError(
1529
- `When comparing object type keys, a custom comparator must be provided in the constructor's options!`
1536
+ throw new TypeError(
1537
+ ERR.comparatorRequired('BST')
1530
1538
  );
1531
1539
  }
1532
1540
 
@@ -13,6 +13,7 @@ import type {
13
13
  FamilyPosition, NodePredicate,
14
14
  OptNode,
15
15
  RBTNColor,
16
+ IterationType,
16
17
  RedBlackTreeOptions
17
18
  } from '../../types';
18
19
  import { BST } from './bst';
@@ -918,6 +919,25 @@ export class RedBlackTree<K = any, V = any, R = any> extends BST<K, V, R> implem
918
919
  * @param [thisArg] - See parameter type for details.
919
920
  * @returns A new RedBlackTree with mapped entries.
920
921
  */
922
+ /**
923
+ * Red-Black trees are self-balancing — `perfectlyBalance` rebuilds via
924
+ * sorted bulk insert, which naturally produces a balanced RBT.
925
+ * @remarks Time O(N), Space O(N)
926
+ */
927
+ override perfectlyBalance(iterationType?: IterationType): boolean {
928
+ // Extract sorted entries, clear, re-insert — RBT self-balances on insert
929
+ const entries: [K, V | undefined][] = [];
930
+ for (const [key, value] of this) entries.push([key, value]);
931
+ if (entries.length <= 1) return true;
932
+ this.clear();
933
+ this.setMany(
934
+ entries.map(([k]) => k),
935
+ entries.map(([, v]) => v),
936
+ true // isBalanceAdd
937
+ );
938
+ return true;
939
+ }
940
+
921
941
  override map<MK = K, MV = V, MR = any>(
922
942
  callback: EntryCallback<K, V | undefined, [MK, MV]>,
923
943
  options?: Partial<RedBlackTreeOptions<MK, MV, MR>>,
@@ -10,6 +10,7 @@
10
10
  import type { Comparator } from '../../types';
11
11
  import type { TreeMapEntryCallback, TreeMapOptions, TreeMapRangeOptions, TreeMapReduceCallback } from '../../types';
12
12
  import { RedBlackTree } from './red-black-tree';
13
+ import { ERR } from '../../common';
13
14
 
14
15
  /**
15
16
  * An ordered Map backed by a red-black tree.
@@ -58,7 +59,7 @@ export class TreeMap<K = any, V = any, R = [K, V]> implements Iterable<[K, V | u
58
59
  } else {
59
60
  // Validate entries like native Map: each item must be a 2-tuple-like value.
60
61
  if (!Array.isArray(item) || item.length < 2) {
61
- throw new TypeError('TreeMap: each entry must be a [key, value] tuple');
62
+ throw new TypeError(ERR.invalidEntry('TreeMap'));
62
63
  }
63
64
  k = item[0] as K;
64
65
  v = item[1] as V | undefined;
@@ -81,7 +82,7 @@ export class TreeMap<K = any, V = any, R = [K, V]> implements Iterable<[K, V | u
81
82
  static createDefaultComparator<K>(): Comparator<K> {
82
83
  return (a: K, b: K): number => {
83
84
  if (typeof a === 'number' && typeof b === 'number') {
84
- if (Number.isNaN(a) || Number.isNaN(b)) throw new TypeError('TreeMap: NaN is not a valid key');
85
+ if (Number.isNaN(a) || Number.isNaN(b)) throw new TypeError(ERR.invalidNaN('TreeMap'));
85
86
  const aa = Object.is(a, -0) ? 0 : a;
86
87
  const bb = Object.is(b, -0) ? 0 : b;
87
88
  return aa > bb ? 1 : aa < bb ? -1 : 0;
@@ -94,11 +95,11 @@ export class TreeMap<K = any, V = any, R = [K, V]> implements Iterable<[K, V | u
94
95
  if (a instanceof Date && b instanceof Date) {
95
96
  const ta = a.getTime();
96
97
  const tb = b.getTime();
97
- if (Number.isNaN(ta) || Number.isNaN(tb)) throw new TypeError('TreeMap: invalid Date key');
98
+ if (Number.isNaN(ta) || Number.isNaN(tb)) throw new TypeError(ERR.invalidDate('TreeMap'));
98
99
  return ta > tb ? 1 : ta < tb ? -1 : 0;
99
100
  }
100
101
 
101
- throw new TypeError('TreeMap: comparator is required for non-number/non-string/non-Date keys');
102
+ throw new TypeError(ERR.comparatorRequired('TreeMap'));
102
103
  };
103
104
  }
104
105
 
@@ -106,18 +107,18 @@ export class TreeMap<K = any, V = any, R = [K, V]> implements Iterable<[K, V | u
106
107
  if (!this.#isDefaultComparator) return;
107
108
 
108
109
  if (typeof key === 'number') {
109
- if (Number.isNaN(key)) throw new TypeError('TreeMap: NaN is not a valid key');
110
+ if (Number.isNaN(key)) throw new TypeError(ERR.invalidNaN('TreeMap'));
110
111
  return;
111
112
  }
112
113
 
113
114
  if (typeof key === 'string') return;
114
115
 
115
116
  if (key instanceof Date) {
116
- if (Number.isNaN(key.getTime())) throw new TypeError('TreeMap: invalid Date key');
117
+ if (Number.isNaN(key.getTime())) throw new TypeError(ERR.invalidDate('TreeMap'));
117
118
  return;
118
119
  }
119
120
 
120
- throw new TypeError('TreeMap: comparator is required for non-number/non-string/non-Date keys');
121
+ throw new TypeError(ERR.comparatorRequired('TreeMap'));
121
122
  }
122
123
 
123
124
  /**
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Comparator, TreeMultiMapOptions } from '../../types';
10
- import { Range } from '../../common';
10
+ import { ERR, Range } from '../../common';
11
11
  import { RedBlackTree, RedBlackTreeNode } from './red-black-tree';
12
12
  import { TreeSet } from './tree-set';
13
13
 
@@ -261,15 +261,15 @@ export class TreeMultiMap<K = any, V = any, R = any> implements Iterable<[K, V[]
261
261
  // reuse TreeSet strict validation (same policy)
262
262
  // NOTE: TreeSet._validateKey is private, so we replicate the checks.
263
263
  if (typeof key === 'number') {
264
- if (Number.isNaN(key)) throw new TypeError('TreeMultiMap: NaN is not a valid key');
264
+ if (Number.isNaN(key)) throw new TypeError(ERR.invalidNaN('TreeMultiMap'));
265
265
  return;
266
266
  }
267
267
  if (typeof key === 'string') return;
268
268
  if (key instanceof Date) {
269
- if (Number.isNaN(key.getTime())) throw new TypeError('TreeMultiMap: invalid Date key');
269
+ if (Number.isNaN(key.getTime())) throw new TypeError(ERR.invalidDate('TreeMultiMap'));
270
270
  return;
271
271
  }
272
- throw new TypeError('TreeMultiMap: comparator is required for non-number/non-string/non-Date keys');
272
+ throw new TypeError(ERR.comparatorRequired('TreeMultiMap'));
273
273
  }
274
274
 
275
275
  /**