recursive-set 5.0.3 → 6.0.0

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.
package/dist/esm/index.js CHANGED
@@ -1,44 +1,64 @@
1
1
  /**
2
2
  * @module recursive-set
3
3
  * High-Performance Recursive Set with "Freeze-on-Hash" semantics.
4
+ * Version: 6.0.0
4
5
  */
5
- // === HASHING ENGINE (FNV-1a) ===
6
+ // ============================================================================
7
+ // HASHING ENGINE (FNV-1a with DataView & Safe Integer Split)
8
+ // ============================================================================
6
9
  const FNV_PRIME = 16777619;
7
10
  const FNV_OFFSET = 2166136261;
11
+ // Shared buffer to avoid allocation overhead (reused for all number hashing)
8
12
  const floatBuffer = new ArrayBuffer(8);
9
- const floatView = new Float64Array(floatBuffer);
10
- const intView = new Int32Array(floatBuffer);
13
+ const view = new DataView(floatBuffer);
14
+ /**
15
+ * Hashes a number using FNV-1a.
16
+ * Handles both safe integers (via high/low split) and floats (via IEEE 754 bits).
17
+ * Ensures platform consistency by enforcing Little Endian byte order.
18
+ */
11
19
  function hashNumber(val) {
12
- if (Number.isInteger(val)) {
20
+ // Integer Path: Handle Safe Integers correctly (up to 2^53)
21
+ if (Number.isSafeInteger(val)) {
13
22
  let h = FNV_OFFSET;
14
- h ^= val;
23
+ const lowU = val >>> 0;
24
+ const high = ((val - lowU) / 4294967296) | 0;
25
+ h ^= lowU;
26
+ h = Math.imul(h, FNV_PRIME);
27
+ h ^= high;
15
28
  h = Math.imul(h, FNV_PRIME);
16
29
  return h >>> 0;
17
30
  }
18
- floatView[0] = val;
31
+ // Float Path: IEEE 754 Bit Pattern (Little Endian Enforced)
32
+ view.setFloat64(0, val, true);
19
33
  let h = FNV_OFFSET;
20
- h ^= intView[0];
34
+ const low = view.getInt32(0, true);
35
+ const high = view.getInt32(4, true);
36
+ h ^= low;
21
37
  h = Math.imul(h, FNV_PRIME);
22
- h ^= intView[1];
38
+ h ^= high;
23
39
  h = Math.imul(h, FNV_PRIME);
24
40
  return h >>> 0;
25
41
  }
26
42
  function hashString(str) {
27
- let hash = FNV_OFFSET;
43
+ let h = FNV_OFFSET;
28
44
  const len = str.length;
29
45
  for (let i = 0; i < len; i++) {
30
- hash ^= str.charCodeAt(i);
31
- hash = Math.imul(hash, FNV_PRIME);
46
+ h ^= str.charCodeAt(i);
47
+ h = Math.imul(h, FNV_PRIME);
32
48
  }
33
- return hash >>> 0;
49
+ return h >>> 0;
34
50
  }
51
+ /**
52
+ * Computes a 32-bit hash code for a supported value.
53
+ * @param val The value to hash (number, string, Tuple, RecursiveSet, or Array)
54
+ */
35
55
  function hashValue(val) {
36
56
  if (typeof val === 'string')
37
57
  return hashString(val);
38
58
  if (typeof val === 'number')
39
59
  return hashNumber(val);
40
- // Fast Path: Objects with cached hash
41
- if (val && typeof val === 'object' && 'hashCode' in val) {
60
+ // Strict Fast-Path: Only trust our own Primitives
61
+ if (val instanceof RecursiveSet || val instanceof Tuple) {
42
62
  return val.hashCode;
43
63
  }
44
64
  if (Array.isArray(val)) {
@@ -52,92 +72,167 @@ function hashValue(val) {
52
72
  }
53
73
  return 0;
54
74
  }
55
- // === COMPARATOR ===
75
+ // ============================================================================
76
+ // STRICT COMPARATOR (Total Order)
77
+ // ============================================================================
78
+ /**
79
+ * Determines the sort priority of a type.
80
+ * Order: number (1) < string (2) < Sequence (3) < Set (4)
81
+ */
82
+ function getTypeScore(a) {
83
+ if (typeof a === 'number')
84
+ return 1;
85
+ if (typeof a === 'string')
86
+ return 2;
87
+ if (Array.isArray(a) || a instanceof Tuple)
88
+ return 3;
89
+ if (a instanceof RecursiveSet)
90
+ return 4;
91
+ return 0;
92
+ }
93
+ /**
94
+ * Compares two values for sorting.
95
+ * Implements a strict total order:
96
+ * 1. Semantic Type Score (Number < String < Seq < Set)
97
+ * 2. Hash Code (Short-Circuit)
98
+ * 3. Deep Structural Comparison
99
+ */
56
100
  function compare(a, b) {
57
101
  if (a === b)
58
102
  return 0;
59
- // 1. Hash Short-Circuit (Optimization)
60
- const aH = a?.hashCode;
61
- const bH = b?.hashCode;
62
- const ha = (aH !== undefined) ? aH : hashValue(a);
63
- const hb = (bH !== undefined) ? bH : hashValue(b);
64
- if (ha !== hb)
65
- return ha < hb ? -1 : 1;
66
- // 2. Structural Type Check
67
- const typeA = typeof a;
68
- const typeB = typeof b;
69
- if (typeA === 'string' && typeB === 'string')
103
+ // 1. Semantic Grouping
104
+ const scoreA = getTypeScore(a);
105
+ const scoreB = getTypeScore(b);
106
+ if (scoreA !== scoreB) {
107
+ return scoreA < scoreB ? -1 : 1;
108
+ }
109
+ // 2. PRIMITIVES: Value Sort (Human readable & Fast)
110
+ if (scoreA === 1)
70
111
  return a < b ? -1 : 1;
71
- if (typeA === 'number' && typeB === 'number')
112
+ if (scoreA === 2)
72
113
  return a < b ? -1 : 1;
73
- const isSetA = a instanceof RecursiveSet;
74
- const isSetB = b instanceof RecursiveSet;
75
- if (isSetA && isSetB) {
114
+ // 3. OBJECTS: Hash Optimization (Performance Protection)
115
+ const ha = hashValue(a);
116
+ const hb = hashValue(b);
117
+ if (ha !== hb)
118
+ return ha < hb ? -1 : 1;
119
+ // 3. Fallback / Structural Comparison
120
+ if (scoreA === 4) {
76
121
  return a.compare(b);
77
122
  }
78
- const isSeqA = Array.isArray(a) || a instanceof Tuple;
79
- const isSeqB = Array.isArray(b) || b instanceof Tuple;
80
- // Sort Order: Primitives (0) < Sequences (1) < Sets (2)
81
- if (isSetA !== isSetB || isSeqA !== isSeqB) {
82
- const scoreA = isSetA ? 2 : isSeqA ? 1 : 0;
83
- const scoreB = isSetB ? 2 : isSeqB ? 1 : 0;
84
- return scoreA - scoreB;
123
+ // Sequence Comparison (Arrays & Tuples)
124
+ const valA = (a instanceof Tuple) ? a.values : a;
125
+ const valB = (b instanceof Tuple) ? b.values : b;
126
+ const len = valA.length;
127
+ if (len !== valB.length)
128
+ return len - valB.length;
129
+ for (let i = 0; i < len; i++) {
130
+ const diff = compare(valA[i], valB[i]);
131
+ if (diff !== 0)
132
+ return diff;
85
133
  }
86
- // 3. Sequence Comparison
87
- if (isSeqA && isSeqB) {
88
- const valA = (a instanceof Tuple) ? a.values : a;
89
- const valB = (b instanceof Tuple) ? b.values : b;
90
- const len = valA.length;
91
- if (len !== valB.length)
92
- return len - valB.length;
93
- for (let i = 0; i < len; i++) {
94
- const diff = compare(valA[i], valB[i]);
95
- if (diff !== 0)
96
- return diff;
134
+ return 0;
135
+ }
136
+ // ============================================================================
137
+ // VALIDATION
138
+ // ============================================================================
139
+ /**
140
+ * Validates that an element is of a supported immutable-compatible type.
141
+ * Throws if the type is mutable (plain object) or unsupported (function, symbol).
142
+ * @throws {Error} if element type is invalid.
143
+ */
144
+ function validateType(element) {
145
+ if (typeof element === 'number') {
146
+ if (Number.isNaN(element))
147
+ throw new Error("NaN is not supported");
148
+ return;
149
+ }
150
+ if (typeof element === 'string')
151
+ return;
152
+ if (Array.isArray(element)) {
153
+ for (const item of element)
154
+ validateType(item);
155
+ return;
156
+ }
157
+ if (typeof element === 'object' && element !== null) {
158
+ if (element instanceof RecursiveSet || element instanceof Tuple) {
159
+ return;
97
160
  }
98
- return 0;
99
161
  }
100
- return a < b ? -1 : 1;
162
+ throw new Error("Unsupported Type: Use number, string, Tuple, Array or RecursiveSet.");
101
163
  }
102
- // === CLASSES ===
164
+ // ============================================================================
165
+ // CLASSES
166
+ // ============================================================================
167
+ /**
168
+ * Immutable Tuple container.
169
+ * * Guarantees structural immutability by freezing the internal array storage.
170
+ * Note: Immutability is shallow. If you store mutable Arrays inside a Tuple,
171
+ * the Tuple logic remains correct, but the content inside the Array might change.
172
+ * For strict value semantics, use Tuple<Primitive | RecursiveSet | Tuple>.
173
+ * * @template T The tuple type.
174
+ */
103
175
  export class Tuple {
104
- values;
176
+ #values;
105
177
  hashCode;
178
+ /**
179
+ * Creates a new Tuple.
180
+ * @param values Elements of the tuple.
181
+ */
106
182
  constructor(...values) {
107
- this.values = values;
108
- this.hashCode = hashValue(values);
183
+ for (const v of values)
184
+ validateType(v);
185
+ this.#values = [...values];
186
+ Object.freeze(this.#values);
187
+ this.hashCode = hashValue(this.#values);
109
188
  }
110
- get length() { return this.values.length; }
111
- get(i) { return this.values[i]; }
112
- *[Symbol.iterator]() { yield* this.values; }
113
- toString() { return `(${this.values.join(', ')})`; }
189
+ /** Returns the read-only backing array. */
190
+ get values() { return this.#values; }
191
+ get length() { return this.#values.length; }
192
+ get(i) { return this.#values[i]; }
193
+ *[Symbol.iterator]() { yield* this.#values; }
194
+ toString() { return `(${this.#values.join(', ')})`; }
114
195
  [Symbol.for('nodejs.util.inspect.custom')]() { return this.toString(); }
115
196
  }
197
+ /**
198
+ * A mathematical Set with Value Semantics.
199
+ * * Features:
200
+ * - **Strict Typing:** Supports number, string, Tuple, Array, RecursiveSet.
201
+ * - **Sorted Storage:** Elements are internally sorted for O(1) equality checks via hash.
202
+ * - **Freeze-on-Hash:** Once the hash code is accessed (e.g. when added to another Set),
203
+ * this Set becomes immutable to prevent hash corruption.
204
+ * * @template T The type of elements in the set.
205
+ */
116
206
  export class RecursiveSet {
117
- _elements;
118
- _hashCode = null;
119
- _isFrozen = false;
207
+ #elements = [];
208
+ #hashCode = null;
209
+ #isFrozen = false;
210
+ /**
211
+ * Exposes the static compare function used internally.
212
+ */
120
213
  static compare(a, b) { return compare(a, b); }
214
+ /**
215
+ * Creates a new RecursiveSet.
216
+ * @param elements Initial elements (will be sorted and deduplicated).
217
+ */
121
218
  constructor(...elements) {
122
- if (elements.length === 0) {
123
- this._elements = [];
124
- this._hashCode = 0;
125
- }
126
- else {
127
- this._elements = elements;
128
- this._elements.sort(compare);
129
- this._unique();
219
+ if (elements.length > 0) {
220
+ for (const el of elements)
221
+ validateType(el);
222
+ this.#elements = elements;
223
+ this.#elements.sort(compare);
224
+ this.#unique();
130
225
  }
131
226
  }
132
- _checkFrozen(op) {
133
- if (this._isFrozen) {
227
+ #checkFrozen(op) {
228
+ if (this.#isFrozen) {
134
229
  throw new Error(`InvalidOperation: Cannot ${op} a frozen RecursiveSet.\n` +
135
230
  `This Set has been hashed or used in a collection (Value Semantics).\n` +
136
231
  `Use .mutableCopy() to create a modifiable copy.`);
137
232
  }
138
233
  }
139
- _unique() {
140
- const arr = this._elements;
234
+ #unique() {
235
+ const arr = this.#elements;
141
236
  const len = arr.length;
142
237
  if (len < 2)
143
238
  return;
@@ -150,22 +245,28 @@ export class RecursiveSet {
150
245
  arr.length = write;
151
246
  }
152
247
  /**
153
- * Calculates/Caches hash code and FREEZES the set.
248
+ * Calculates and caches the hash code.
249
+ * Freezes the set to ensure the hash remains valid.
154
250
  */
155
251
  get hashCode() {
156
- if (this._hashCode !== null)
157
- return this._hashCode;
252
+ if (this.#hashCode !== null)
253
+ return this.#hashCode;
158
254
  let h = 0;
159
- const arr = this._elements;
255
+ const arr = this.#elements;
160
256
  const len = arr.length;
161
257
  for (let i = 0; i < len; i++) {
162
- h = Math.imul(31, h) + hashValue(arr[i]);
258
+ // Wrap to 32-bit at each step for consistency
259
+ h = (Math.imul(31, h) + hashValue(arr[i])) | 0;
163
260
  }
164
- this._hashCode = h | 0;
165
- this._isFrozen = true;
166
- return this._hashCode;
261
+ this.#hashCode = h | 0;
262
+ this.#isFrozen = true;
263
+ return this.#hashCode;
167
264
  }
168
- get isFrozen() { return this._isFrozen; }
265
+ /** Returns true if the set is frozen (hashed). */
266
+ get isFrozen() { return this.#isFrozen; }
267
+ /**
268
+ * Compares this set with another for sorting.
269
+ */
169
270
  compare(other) {
170
271
  if (this === other)
171
272
  return 0;
@@ -173,8 +274,8 @@ export class RecursiveSet {
173
274
  const h2 = other.hashCode;
174
275
  if (h1 !== h2)
175
276
  return h1 < h2 ? -1 : 1;
176
- const arrA = this._elements;
177
- const arrB = other._elements;
277
+ const arrA = this.#elements;
278
+ const arrB = other.#elements;
178
279
  const len = arrA.length;
179
280
  if (len !== arrB.length)
180
281
  return len - arrB.length;
@@ -185,12 +286,15 @@ export class RecursiveSet {
185
286
  }
186
287
  return 0;
187
288
  }
188
- get size() { return this._elements.length; }
189
- isEmpty() { return this._elements.length === 0; }
289
+ get size() { return this.#elements.length; }
290
+ isEmpty() { return this.#elements.length === 0; }
291
+ /**
292
+ * Checks if the set contains the given element.
293
+ * Uses binary search for sets > 16 elements.
294
+ */
190
295
  has(element) {
191
- const arr = this._elements;
296
+ const arr = this.#elements;
192
297
  const len = arr.length;
193
- // Linear Scan (Prefetch-friendly for small sets)
194
298
  if (len < 16) {
195
299
  for (let i = 0; i < len; i++) {
196
300
  if (compare(arr[i], element) === 0)
@@ -198,7 +302,6 @@ export class RecursiveSet {
198
302
  }
199
303
  return false;
200
304
  }
201
- // Binary Search
202
305
  let low = 0, high = len - 1;
203
306
  while (low <= high) {
204
307
  const mid = (low + high) >>> 1;
@@ -212,25 +315,21 @@ export class RecursiveSet {
212
315
  }
213
316
  return false;
214
317
  }
318
+ /**
319
+ * Adds an element to the set.
320
+ * Throws if the set is frozen.
321
+ */
215
322
  add(element) {
216
- this._checkFrozen('add() to');
217
- // Validation (Inlined for Performance)
218
- if (typeof element === 'object' && element !== null) {
219
- if (!(element instanceof RecursiveSet || element instanceof Tuple || Array.isArray(element))) {
220
- throw new Error("Unsupported Type: Use Tuple, Array or RecursiveSet.");
221
- }
222
- }
223
- else if (Number.isNaN(element)) {
224
- throw new Error("NaN is not supported");
225
- }
226
- const arr = this._elements;
323
+ this.#checkFrozen('add() to');
324
+ validateType(element);
325
+ const arr = this.#elements;
227
326
  const len = arr.length;
228
- // Optimization: Append to end (common in construction)
327
+ // Optimization: Append to end (common pattern)
229
328
  if (len > 0) {
230
329
  const lastCmp = compare(arr[len - 1], element);
231
330
  if (lastCmp < 0) {
232
331
  arr.push(element);
233
- this._hashCode = null;
332
+ this.#hashCode = null;
234
333
  return this;
235
334
  }
236
335
  if (lastCmp === 0)
@@ -238,10 +337,10 @@ export class RecursiveSet {
238
337
  }
239
338
  else {
240
339
  arr.push(element);
241
- this._hashCode = null;
340
+ this.#hashCode = null;
242
341
  return this;
243
342
  }
244
- // Small Array Strategy
343
+ // Small Array: Linear Insert
245
344
  if (len < 16) {
246
345
  for (let i = 0; i < len; i++) {
247
346
  const cmp = compare(arr[i], element);
@@ -249,14 +348,14 @@ export class RecursiveSet {
249
348
  return this;
250
349
  if (cmp > 0) {
251
350
  arr.splice(i, 0, element);
252
- this._hashCode = null;
351
+ this.#hashCode = null;
253
352
  return this;
254
353
  }
255
354
  }
256
- arr.push(element); // Should be unreachable given append check, but safe fallback
355
+ arr.push(element);
257
356
  return this;
258
357
  }
259
- // Large Array Strategy
358
+ // Large Array: Binary Search Insert
260
359
  let low = 0, high = len - 1, idx = 0;
261
360
  while (low <= high) {
262
361
  const mid = (low + high) >>> 1;
@@ -273,18 +372,22 @@ export class RecursiveSet {
273
372
  }
274
373
  }
275
374
  arr.splice(idx, 0, element);
276
- this._hashCode = null;
375
+ this.#hashCode = null;
277
376
  return this;
278
377
  }
378
+ /**
379
+ * Removes an element from the set.
380
+ * Throws if the set is frozen.
381
+ */
279
382
  remove(element) {
280
- this._checkFrozen('remove() from');
281
- const arr = this._elements;
383
+ this.#checkFrozen('remove() from');
384
+ const arr = this.#elements;
282
385
  const len = arr.length;
283
386
  if (len < 16) {
284
387
  for (let i = 0; i < len; i++) {
285
388
  if (compare(arr[i], element) === 0) {
286
389
  arr.splice(i, 1);
287
- this._hashCode = null;
390
+ this.#hashCode = null;
288
391
  return this;
289
392
  }
290
393
  }
@@ -296,7 +399,7 @@ export class RecursiveSet {
296
399
  const cmp = compare(arr[mid], element);
297
400
  if (cmp === 0) {
298
401
  arr.splice(mid, 1);
299
- this._hashCode = null;
402
+ this.#hashCode = null;
300
403
  return this;
301
404
  }
302
405
  if (cmp < 0)
@@ -307,21 +410,25 @@ export class RecursiveSet {
307
410
  return this;
308
411
  }
309
412
  clear() {
310
- this._checkFrozen('clear()');
311
- this._elements = [];
312
- this._hashCode = 0;
413
+ this.#checkFrozen('clear()');
414
+ this.#elements = [];
415
+ this.#hashCode = 0;
313
416
  return this;
314
417
  }
418
+ /**
419
+ * Creates a mutable shallow copy of this set.
420
+ * Useful for modifying a set after it has been frozen.
421
+ */
315
422
  mutableCopy() {
316
423
  const s = new RecursiveSet();
317
- s._elements = this._elements.slice();
424
+ s.#elements = this.#elements.slice();
318
425
  return s;
319
426
  }
320
427
  clone() { return this.mutableCopy(); }
321
428
  union(other) {
322
429
  const s = new RecursiveSet();
323
- const arrA = this._elements;
324
- const arrB = other._elements;
430
+ const arrA = this.#elements;
431
+ const arrB = other.#elements;
325
432
  if (arrA.length === 0)
326
433
  return other.clone();
327
434
  if (arrB.length === 0)
@@ -344,13 +451,13 @@ export class RecursiveSet {
344
451
  res.push(arrA[i++]);
345
452
  while (j < lenB)
346
453
  res.push(arrB[j++]);
347
- s._elements = res;
454
+ s.#elements = res;
348
455
  return s;
349
456
  }
350
457
  intersection(other) {
351
458
  const s = new RecursiveSet();
352
- const arrA = this._elements;
353
- const arrB = other._elements;
459
+ const arrA = this.#elements;
460
+ const arrB = other.#elements;
354
461
  const res = [];
355
462
  let i = 0, j = 0;
356
463
  const lenA = arrA.length, lenB = arrB.length;
@@ -365,13 +472,13 @@ export class RecursiveSet {
365
472
  j++;
366
473
  }
367
474
  }
368
- s._elements = res;
475
+ s.#elements = res;
369
476
  return s;
370
477
  }
371
478
  difference(other) {
372
479
  const s = new RecursiveSet();
373
- const arrA = this._elements;
374
- const arrB = other._elements;
480
+ const arrA = this.#elements;
481
+ const arrB = other.#elements;
375
482
  const res = [];
376
483
  let i = 0, j = 0;
377
484
  const lenA = arrA.length, lenB = arrB.length;
@@ -388,13 +495,13 @@ export class RecursiveSet {
388
495
  }
389
496
  while (i < lenA)
390
497
  res.push(arrA[i++]);
391
- s._elements = res;
498
+ s.#elements = res;
392
499
  return s;
393
500
  }
394
501
  symmetricDifference(other) {
395
502
  const s = new RecursiveSet();
396
- const arrA = this._elements;
397
- const arrB = other._elements;
503
+ const arrA = this.#elements;
504
+ const arrB = other.#elements;
398
505
  const res = [];
399
506
  let i = 0, j = 0;
400
507
  const lenA = arrA.length, lenB = arrB.length;
@@ -413,7 +520,7 @@ export class RecursiveSet {
413
520
  res.push(arrA[i++]);
414
521
  while (j < lenB)
415
522
  res.push(arrB[j++]);
416
- s._elements = res;
523
+ s.#elements = res;
417
524
  return s;
418
525
  }
419
526
  powerset() {
@@ -426,7 +533,7 @@ export class RecursiveSet {
426
533
  const subset = new RecursiveSet();
427
534
  for (let j = 0; j < n; j++) {
428
535
  if (i & (1 << j))
429
- subset._elements.push(this._elements[j]);
536
+ subset.#elements.push(this.#elements[j]);
430
537
  }
431
538
  subsets.push(subset);
432
539
  }
@@ -434,20 +541,23 @@ export class RecursiveSet {
434
541
  }
435
542
  cartesianProduct(other) {
436
543
  const result = new RecursiveSet();
437
- const arrA = this._elements;
438
- const arrB = other._elements;
544
+ const arrA = this.#elements;
545
+ const arrB = other.#elements;
439
546
  for (const x of arrA) {
440
547
  for (const y of arrB) {
441
- result._elements.push(new Tuple(x, y));
548
+ result.#elements.push(new Tuple(x, y));
442
549
  }
443
550
  }
551
+ // Hash ordering is not monotonic, so we must resort
552
+ result.#elements.sort(compare);
553
+ result.#unique();
444
554
  return result;
445
555
  }
446
556
  isSubset(other) {
447
557
  if (this.size > other.size)
448
558
  return false;
449
559
  let i = 0, j = 0;
450
- const arrA = this._elements, arrB = other._elements;
560
+ const arrA = this.#elements, arrB = other.#elements;
451
561
  while (i < arrA.length && j < arrB.length) {
452
562
  const cmp = compare(arrA[i], arrB[j]);
453
563
  if (cmp < 0)
@@ -464,12 +574,12 @@ export class RecursiveSet {
464
574
  isSuperset(other) { return other.isSubset(this); }
465
575
  isProperSubset(other) { return this.isSubset(other) && this.size < other.size; }
466
576
  equals(other) { return this.compare(other) === 0; }
467
- toSet() { return new Set(this._elements); }
468
- *[Symbol.iterator]() { yield* this._elements; }
577
+ toSet() { return new Set(this.#elements); }
578
+ *[Symbol.iterator]() { yield* this.#elements; }
469
579
  toString() {
470
580
  if (this.isEmpty())
471
581
  return "∅";
472
- const elementsStr = this._elements.map(el => {
582
+ const elementsStr = this.#elements.map(el => {
473
583
  if (Array.isArray(el))
474
584
  return `[${el.join(', ')}]`;
475
585
  return String(el);
@@ -478,6 +588,9 @@ export class RecursiveSet {
478
588
  }
479
589
  [Symbol.for('nodejs.util.inspect.custom')]() { return this.toString(); }
480
590
  }
591
+ // ============================================================================
592
+ // EXPORTS
593
+ // ============================================================================
481
594
  export function emptySet() { return new RecursiveSet(); }
482
595
  export function singleton(element) { return new RecursiveSet(element); }
483
596
  export function fromIterable(iterable) { return new RecursiveSet(...iterable); }