recursive-set 5.0.2 → 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/README.md +74 -99
- package/dist/cjs/index.js +262 -135
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +57 -9
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +262 -135
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -1,30 +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
|
-
//
|
|
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)
|
|
12
|
+
const floatBuffer = new ArrayBuffer(8);
|
|
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
|
+
*/
|
|
19
|
+
function hashNumber(val) {
|
|
20
|
+
// Integer Path: Handle Safe Integers correctly (up to 2^53)
|
|
21
|
+
if (Number.isSafeInteger(val)) {
|
|
22
|
+
let h = FNV_OFFSET;
|
|
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;
|
|
28
|
+
h = Math.imul(h, FNV_PRIME);
|
|
29
|
+
return h >>> 0;
|
|
30
|
+
}
|
|
31
|
+
// Float Path: IEEE 754 Bit Pattern (Little Endian Enforced)
|
|
32
|
+
view.setFloat64(0, val, true);
|
|
33
|
+
let h = FNV_OFFSET;
|
|
34
|
+
const low = view.getInt32(0, true);
|
|
35
|
+
const high = view.getInt32(4, true);
|
|
36
|
+
h ^= low;
|
|
37
|
+
h = Math.imul(h, FNV_PRIME);
|
|
38
|
+
h ^= high;
|
|
39
|
+
h = Math.imul(h, FNV_PRIME);
|
|
40
|
+
return h >>> 0;
|
|
41
|
+
}
|
|
8
42
|
function hashString(str) {
|
|
9
|
-
let
|
|
43
|
+
let h = FNV_OFFSET;
|
|
10
44
|
const len = str.length;
|
|
11
45
|
for (let i = 0; i < len; i++) {
|
|
12
|
-
|
|
13
|
-
|
|
46
|
+
h ^= str.charCodeAt(i);
|
|
47
|
+
h = Math.imul(h, FNV_PRIME);
|
|
14
48
|
}
|
|
15
|
-
return
|
|
49
|
+
return h >>> 0;
|
|
16
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
|
+
*/
|
|
17
55
|
function hashValue(val) {
|
|
18
56
|
if (typeof val === 'string')
|
|
19
57
|
return hashString(val);
|
|
20
|
-
if (typeof val === 'number')
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return hash >>> 0;
|
|
25
|
-
}
|
|
26
|
-
// Fast Path: Objects with cached hash
|
|
27
|
-
if (val && typeof val === 'object' && 'hashCode' in val) {
|
|
58
|
+
if (typeof val === 'number')
|
|
59
|
+
return hashNumber(val);
|
|
60
|
+
// Strict Fast-Path: Only trust our own Primitives
|
|
61
|
+
if (val instanceof RecursiveSet || val instanceof Tuple) {
|
|
28
62
|
return val.hashCode;
|
|
29
63
|
}
|
|
30
64
|
if (Array.isArray(val)) {
|
|
@@ -38,92 +72,167 @@ function hashValue(val) {
|
|
|
38
72
|
}
|
|
39
73
|
return 0;
|
|
40
74
|
}
|
|
41
|
-
//
|
|
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
|
+
*/
|
|
42
100
|
function compare(a, b) {
|
|
43
101
|
if (a === b)
|
|
44
102
|
return 0;
|
|
45
|
-
// 1.
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const typeA = typeof a;
|
|
54
|
-
const typeB = typeof b;
|
|
55
|
-
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)
|
|
56
111
|
return a < b ? -1 : 1;
|
|
57
|
-
if (
|
|
112
|
+
if (scoreA === 2)
|
|
58
113
|
return a < b ? -1 : 1;
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
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) {
|
|
62
121
|
return a.compare(b);
|
|
63
122
|
}
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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;
|
|
71
133
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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;
|
|
83
160
|
}
|
|
84
|
-
return 0;
|
|
85
161
|
}
|
|
86
|
-
|
|
162
|
+
throw new Error("Unsupported Type: Use number, string, Tuple, Array or RecursiveSet.");
|
|
87
163
|
}
|
|
88
|
-
//
|
|
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
|
+
*/
|
|
89
175
|
export class Tuple {
|
|
90
|
-
values;
|
|
176
|
+
#values;
|
|
91
177
|
hashCode;
|
|
178
|
+
/**
|
|
179
|
+
* Creates a new Tuple.
|
|
180
|
+
* @param values Elements of the tuple.
|
|
181
|
+
*/
|
|
92
182
|
constructor(...values) {
|
|
93
|
-
|
|
94
|
-
|
|
183
|
+
for (const v of values)
|
|
184
|
+
validateType(v);
|
|
185
|
+
this.#values = [...values];
|
|
186
|
+
Object.freeze(this.#values);
|
|
187
|
+
this.hashCode = hashValue(this.#values);
|
|
95
188
|
}
|
|
96
|
-
|
|
97
|
-
get(
|
|
98
|
-
|
|
99
|
-
|
|
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(', ')})`; }
|
|
100
195
|
[Symbol.for('nodejs.util.inspect.custom')]() { return this.toString(); }
|
|
101
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
|
+
*/
|
|
102
206
|
export class RecursiveSet {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
207
|
+
#elements = [];
|
|
208
|
+
#hashCode = null;
|
|
209
|
+
#isFrozen = false;
|
|
210
|
+
/**
|
|
211
|
+
* Exposes the static compare function used internally.
|
|
212
|
+
*/
|
|
106
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
|
+
*/
|
|
107
218
|
constructor(...elements) {
|
|
108
|
-
if (elements.length
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this
|
|
114
|
-
this._elements.sort(compare);
|
|
115
|
-
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();
|
|
116
225
|
}
|
|
117
226
|
}
|
|
118
|
-
|
|
119
|
-
if (this
|
|
227
|
+
#checkFrozen(op) {
|
|
228
|
+
if (this.#isFrozen) {
|
|
120
229
|
throw new Error(`InvalidOperation: Cannot ${op} a frozen RecursiveSet.\n` +
|
|
121
230
|
`This Set has been hashed or used in a collection (Value Semantics).\n` +
|
|
122
231
|
`Use .mutableCopy() to create a modifiable copy.`);
|
|
123
232
|
}
|
|
124
233
|
}
|
|
125
|
-
|
|
126
|
-
const arr = this
|
|
234
|
+
#unique() {
|
|
235
|
+
const arr = this.#elements;
|
|
127
236
|
const len = arr.length;
|
|
128
237
|
if (len < 2)
|
|
129
238
|
return;
|
|
@@ -136,22 +245,28 @@ export class RecursiveSet {
|
|
|
136
245
|
arr.length = write;
|
|
137
246
|
}
|
|
138
247
|
/**
|
|
139
|
-
* Calculates
|
|
248
|
+
* Calculates and caches the hash code.
|
|
249
|
+
* Freezes the set to ensure the hash remains valid.
|
|
140
250
|
*/
|
|
141
251
|
get hashCode() {
|
|
142
|
-
if (this
|
|
143
|
-
return this
|
|
252
|
+
if (this.#hashCode !== null)
|
|
253
|
+
return this.#hashCode;
|
|
144
254
|
let h = 0;
|
|
145
|
-
const arr = this
|
|
255
|
+
const arr = this.#elements;
|
|
146
256
|
const len = arr.length;
|
|
147
257
|
for (let i = 0; i < len; i++) {
|
|
148
|
-
|
|
258
|
+
// Wrap to 32-bit at each step for consistency
|
|
259
|
+
h = (Math.imul(31, h) + hashValue(arr[i])) | 0;
|
|
149
260
|
}
|
|
150
|
-
this
|
|
151
|
-
this
|
|
152
|
-
return this
|
|
261
|
+
this.#hashCode = h | 0;
|
|
262
|
+
this.#isFrozen = true;
|
|
263
|
+
return this.#hashCode;
|
|
153
264
|
}
|
|
154
|
-
|
|
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
|
+
*/
|
|
155
270
|
compare(other) {
|
|
156
271
|
if (this === other)
|
|
157
272
|
return 0;
|
|
@@ -159,8 +274,8 @@ export class RecursiveSet {
|
|
|
159
274
|
const h2 = other.hashCode;
|
|
160
275
|
if (h1 !== h2)
|
|
161
276
|
return h1 < h2 ? -1 : 1;
|
|
162
|
-
const arrA = this
|
|
163
|
-
const arrB = other
|
|
277
|
+
const arrA = this.#elements;
|
|
278
|
+
const arrB = other.#elements;
|
|
164
279
|
const len = arrA.length;
|
|
165
280
|
if (len !== arrB.length)
|
|
166
281
|
return len - arrB.length;
|
|
@@ -171,12 +286,15 @@ export class RecursiveSet {
|
|
|
171
286
|
}
|
|
172
287
|
return 0;
|
|
173
288
|
}
|
|
174
|
-
get size() { return this.
|
|
175
|
-
isEmpty() { return this.
|
|
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
|
+
*/
|
|
176
295
|
has(element) {
|
|
177
|
-
const arr = this
|
|
296
|
+
const arr = this.#elements;
|
|
178
297
|
const len = arr.length;
|
|
179
|
-
// Linear Scan (Prefetch-friendly for small sets)
|
|
180
298
|
if (len < 16) {
|
|
181
299
|
for (let i = 0; i < len; i++) {
|
|
182
300
|
if (compare(arr[i], element) === 0)
|
|
@@ -184,7 +302,6 @@ export class RecursiveSet {
|
|
|
184
302
|
}
|
|
185
303
|
return false;
|
|
186
304
|
}
|
|
187
|
-
// Binary Search
|
|
188
305
|
let low = 0, high = len - 1;
|
|
189
306
|
while (low <= high) {
|
|
190
307
|
const mid = (low + high) >>> 1;
|
|
@@ -198,25 +315,21 @@ export class RecursiveSet {
|
|
|
198
315
|
}
|
|
199
316
|
return false;
|
|
200
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Adds an element to the set.
|
|
320
|
+
* Throws if the set is frozen.
|
|
321
|
+
*/
|
|
201
322
|
add(element) {
|
|
202
|
-
this
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (!(element instanceof RecursiveSet || element instanceof Tuple || Array.isArray(element))) {
|
|
206
|
-
throw new Error("Unsupported Type: Use Tuple, Array or RecursiveSet.");
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
else if (Number.isNaN(element)) {
|
|
210
|
-
throw new Error("NaN is not supported");
|
|
211
|
-
}
|
|
212
|
-
const arr = this._elements;
|
|
323
|
+
this.#checkFrozen('add() to');
|
|
324
|
+
validateType(element);
|
|
325
|
+
const arr = this.#elements;
|
|
213
326
|
const len = arr.length;
|
|
214
|
-
// Optimization: Append to end (common
|
|
327
|
+
// Optimization: Append to end (common pattern)
|
|
215
328
|
if (len > 0) {
|
|
216
329
|
const lastCmp = compare(arr[len - 1], element);
|
|
217
330
|
if (lastCmp < 0) {
|
|
218
331
|
arr.push(element);
|
|
219
|
-
this
|
|
332
|
+
this.#hashCode = null;
|
|
220
333
|
return this;
|
|
221
334
|
}
|
|
222
335
|
if (lastCmp === 0)
|
|
@@ -224,10 +337,10 @@ export class RecursiveSet {
|
|
|
224
337
|
}
|
|
225
338
|
else {
|
|
226
339
|
arr.push(element);
|
|
227
|
-
this
|
|
340
|
+
this.#hashCode = null;
|
|
228
341
|
return this;
|
|
229
342
|
}
|
|
230
|
-
// Small Array
|
|
343
|
+
// Small Array: Linear Insert
|
|
231
344
|
if (len < 16) {
|
|
232
345
|
for (let i = 0; i < len; i++) {
|
|
233
346
|
const cmp = compare(arr[i], element);
|
|
@@ -235,14 +348,14 @@ export class RecursiveSet {
|
|
|
235
348
|
return this;
|
|
236
349
|
if (cmp > 0) {
|
|
237
350
|
arr.splice(i, 0, element);
|
|
238
|
-
this
|
|
351
|
+
this.#hashCode = null;
|
|
239
352
|
return this;
|
|
240
353
|
}
|
|
241
354
|
}
|
|
242
|
-
arr.push(element);
|
|
355
|
+
arr.push(element);
|
|
243
356
|
return this;
|
|
244
357
|
}
|
|
245
|
-
// Large Array
|
|
358
|
+
// Large Array: Binary Search Insert
|
|
246
359
|
let low = 0, high = len - 1, idx = 0;
|
|
247
360
|
while (low <= high) {
|
|
248
361
|
const mid = (low + high) >>> 1;
|
|
@@ -259,18 +372,22 @@ export class RecursiveSet {
|
|
|
259
372
|
}
|
|
260
373
|
}
|
|
261
374
|
arr.splice(idx, 0, element);
|
|
262
|
-
this
|
|
375
|
+
this.#hashCode = null;
|
|
263
376
|
return this;
|
|
264
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Removes an element from the set.
|
|
380
|
+
* Throws if the set is frozen.
|
|
381
|
+
*/
|
|
265
382
|
remove(element) {
|
|
266
|
-
this
|
|
267
|
-
const arr = this
|
|
383
|
+
this.#checkFrozen('remove() from');
|
|
384
|
+
const arr = this.#elements;
|
|
268
385
|
const len = arr.length;
|
|
269
386
|
if (len < 16) {
|
|
270
387
|
for (let i = 0; i < len; i++) {
|
|
271
388
|
if (compare(arr[i], element) === 0) {
|
|
272
389
|
arr.splice(i, 1);
|
|
273
|
-
this
|
|
390
|
+
this.#hashCode = null;
|
|
274
391
|
return this;
|
|
275
392
|
}
|
|
276
393
|
}
|
|
@@ -282,7 +399,7 @@ export class RecursiveSet {
|
|
|
282
399
|
const cmp = compare(arr[mid], element);
|
|
283
400
|
if (cmp === 0) {
|
|
284
401
|
arr.splice(mid, 1);
|
|
285
|
-
this
|
|
402
|
+
this.#hashCode = null;
|
|
286
403
|
return this;
|
|
287
404
|
}
|
|
288
405
|
if (cmp < 0)
|
|
@@ -293,21 +410,25 @@ export class RecursiveSet {
|
|
|
293
410
|
return this;
|
|
294
411
|
}
|
|
295
412
|
clear() {
|
|
296
|
-
this
|
|
297
|
-
this
|
|
298
|
-
this
|
|
413
|
+
this.#checkFrozen('clear()');
|
|
414
|
+
this.#elements = [];
|
|
415
|
+
this.#hashCode = 0;
|
|
299
416
|
return this;
|
|
300
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Creates a mutable shallow copy of this set.
|
|
420
|
+
* Useful for modifying a set after it has been frozen.
|
|
421
|
+
*/
|
|
301
422
|
mutableCopy() {
|
|
302
423
|
const s = new RecursiveSet();
|
|
303
|
-
s
|
|
424
|
+
s.#elements = this.#elements.slice();
|
|
304
425
|
return s;
|
|
305
426
|
}
|
|
306
427
|
clone() { return this.mutableCopy(); }
|
|
307
428
|
union(other) {
|
|
308
429
|
const s = new RecursiveSet();
|
|
309
|
-
const arrA = this
|
|
310
|
-
const arrB = other
|
|
430
|
+
const arrA = this.#elements;
|
|
431
|
+
const arrB = other.#elements;
|
|
311
432
|
if (arrA.length === 0)
|
|
312
433
|
return other.clone();
|
|
313
434
|
if (arrB.length === 0)
|
|
@@ -330,13 +451,13 @@ export class RecursiveSet {
|
|
|
330
451
|
res.push(arrA[i++]);
|
|
331
452
|
while (j < lenB)
|
|
332
453
|
res.push(arrB[j++]);
|
|
333
|
-
s
|
|
454
|
+
s.#elements = res;
|
|
334
455
|
return s;
|
|
335
456
|
}
|
|
336
457
|
intersection(other) {
|
|
337
458
|
const s = new RecursiveSet();
|
|
338
|
-
const arrA = this
|
|
339
|
-
const arrB = other
|
|
459
|
+
const arrA = this.#elements;
|
|
460
|
+
const arrB = other.#elements;
|
|
340
461
|
const res = [];
|
|
341
462
|
let i = 0, j = 0;
|
|
342
463
|
const lenA = arrA.length, lenB = arrB.length;
|
|
@@ -351,13 +472,13 @@ export class RecursiveSet {
|
|
|
351
472
|
j++;
|
|
352
473
|
}
|
|
353
474
|
}
|
|
354
|
-
s
|
|
475
|
+
s.#elements = res;
|
|
355
476
|
return s;
|
|
356
477
|
}
|
|
357
478
|
difference(other) {
|
|
358
479
|
const s = new RecursiveSet();
|
|
359
|
-
const arrA = this
|
|
360
|
-
const arrB = other
|
|
480
|
+
const arrA = this.#elements;
|
|
481
|
+
const arrB = other.#elements;
|
|
361
482
|
const res = [];
|
|
362
483
|
let i = 0, j = 0;
|
|
363
484
|
const lenA = arrA.length, lenB = arrB.length;
|
|
@@ -374,13 +495,13 @@ export class RecursiveSet {
|
|
|
374
495
|
}
|
|
375
496
|
while (i < lenA)
|
|
376
497
|
res.push(arrA[i++]);
|
|
377
|
-
s
|
|
498
|
+
s.#elements = res;
|
|
378
499
|
return s;
|
|
379
500
|
}
|
|
380
501
|
symmetricDifference(other) {
|
|
381
502
|
const s = new RecursiveSet();
|
|
382
|
-
const arrA = this
|
|
383
|
-
const arrB = other
|
|
503
|
+
const arrA = this.#elements;
|
|
504
|
+
const arrB = other.#elements;
|
|
384
505
|
const res = [];
|
|
385
506
|
let i = 0, j = 0;
|
|
386
507
|
const lenA = arrA.length, lenB = arrB.length;
|
|
@@ -399,7 +520,7 @@ export class RecursiveSet {
|
|
|
399
520
|
res.push(arrA[i++]);
|
|
400
521
|
while (j < lenB)
|
|
401
522
|
res.push(arrB[j++]);
|
|
402
|
-
s
|
|
523
|
+
s.#elements = res;
|
|
403
524
|
return s;
|
|
404
525
|
}
|
|
405
526
|
powerset() {
|
|
@@ -412,7 +533,7 @@ export class RecursiveSet {
|
|
|
412
533
|
const subset = new RecursiveSet();
|
|
413
534
|
for (let j = 0; j < n; j++) {
|
|
414
535
|
if (i & (1 << j))
|
|
415
|
-
subset.
|
|
536
|
+
subset.#elements.push(this.#elements[j]);
|
|
416
537
|
}
|
|
417
538
|
subsets.push(subset);
|
|
418
539
|
}
|
|
@@ -420,20 +541,23 @@ export class RecursiveSet {
|
|
|
420
541
|
}
|
|
421
542
|
cartesianProduct(other) {
|
|
422
543
|
const result = new RecursiveSet();
|
|
423
|
-
const arrA = this
|
|
424
|
-
const arrB = other
|
|
544
|
+
const arrA = this.#elements;
|
|
545
|
+
const arrB = other.#elements;
|
|
425
546
|
for (const x of arrA) {
|
|
426
547
|
for (const y of arrB) {
|
|
427
|
-
result.
|
|
548
|
+
result.#elements.push(new Tuple(x, y));
|
|
428
549
|
}
|
|
429
550
|
}
|
|
551
|
+
// Hash ordering is not monotonic, so we must resort
|
|
552
|
+
result.#elements.sort(compare);
|
|
553
|
+
result.#unique();
|
|
430
554
|
return result;
|
|
431
555
|
}
|
|
432
556
|
isSubset(other) {
|
|
433
557
|
if (this.size > other.size)
|
|
434
558
|
return false;
|
|
435
559
|
let i = 0, j = 0;
|
|
436
|
-
const arrA = this
|
|
560
|
+
const arrA = this.#elements, arrB = other.#elements;
|
|
437
561
|
while (i < arrA.length && j < arrB.length) {
|
|
438
562
|
const cmp = compare(arrA[i], arrB[j]);
|
|
439
563
|
if (cmp < 0)
|
|
@@ -450,12 +574,12 @@ export class RecursiveSet {
|
|
|
450
574
|
isSuperset(other) { return other.isSubset(this); }
|
|
451
575
|
isProperSubset(other) { return this.isSubset(other) && this.size < other.size; }
|
|
452
576
|
equals(other) { return this.compare(other) === 0; }
|
|
453
|
-
toSet() { return new Set(this
|
|
454
|
-
*[Symbol.iterator]() { yield* this
|
|
577
|
+
toSet() { return new Set(this.#elements); }
|
|
578
|
+
*[Symbol.iterator]() { yield* this.#elements; }
|
|
455
579
|
toString() {
|
|
456
580
|
if (this.isEmpty())
|
|
457
581
|
return "∅";
|
|
458
|
-
const elementsStr = this.
|
|
582
|
+
const elementsStr = this.#elements.map(el => {
|
|
459
583
|
if (Array.isArray(el))
|
|
460
584
|
return `[${el.join(', ')}]`;
|
|
461
585
|
return String(el);
|
|
@@ -464,6 +588,9 @@ export class RecursiveSet {
|
|
|
464
588
|
}
|
|
465
589
|
[Symbol.for('nodejs.util.inspect.custom')]() { return this.toString(); }
|
|
466
590
|
}
|
|
591
|
+
// ============================================================================
|
|
592
|
+
// EXPORTS
|
|
593
|
+
// ============================================================================
|
|
467
594
|
export function emptySet() { return new RecursiveSet(); }
|
|
468
595
|
export function singleton(element) { return new RecursiveSet(element); }
|
|
469
596
|
export function fromIterable(iterable) { return new RecursiveSet(...iterable); }
|