@superhero/deep 4.2.0 → 4.4.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 +122 -70
- package/assign.js +5 -0
- package/clone.js +59 -12
- package/clone.test.js +42 -1
- package/index.js +7 -6
- package/intersect.js +165 -0
- package/intersect.test.js +129 -0
- package/merge.js +8 -1
- package/merge.test.js +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -101,7 +101,40 @@ console.log(clone === obj) // false
|
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
104
|
-
## 5. **
|
|
104
|
+
## 5. **DeepIntersect**
|
|
105
|
+
|
|
106
|
+
### Purpose:
|
|
107
|
+
Performs a deep intersection between two or more values. Supports objects, arrays, and nested structures, and handles circular references. Values are strictly compared when considered intersecting.
|
|
108
|
+
|
|
109
|
+
### Features:
|
|
110
|
+
- Intersects arrays by values.
|
|
111
|
+
- Intersects objects only when the key exists in all sources and the values intersect deeply.
|
|
112
|
+
- Preserves intersected restrictive property descriptors (`writable`, `configurable`, `enumerable`).
|
|
113
|
+
- Detects and throws on circular references.
|
|
114
|
+
|
|
115
|
+
### Example:
|
|
116
|
+
```javascript
|
|
117
|
+
import deepintersect from '@superhero/deep/intersect'
|
|
118
|
+
|
|
119
|
+
const a = { foo: { bar: 1 }, arr: [1, 2, 3] }
|
|
120
|
+
const b = { foo: { bar: 1, baz: 2 }, arr: [1, 4, 3] }
|
|
121
|
+
|
|
122
|
+
const result = deepintersect(a, b)
|
|
123
|
+
|
|
124
|
+
console.log(result)
|
|
125
|
+
// { foo: { bar: 1 }, arr: [1, 3] }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Note:
|
|
129
|
+
- If two values are loosely equal, but of different types, they do not intersect.
|
|
130
|
+
|
|
131
|
+
- Arrays with nested objects can only be compared if the nested structure shares the same index between the compared arrays.
|
|
132
|
+
|
|
133
|
+
- A circular structure throws a CircularReferenceError (ReferenceError) with the code `E_DEEP_INTERSECT_CIRCULAR_REFERENCE`.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 6. **Deep**
|
|
105
138
|
|
|
106
139
|
### Purpose:
|
|
107
140
|
Makes the functions accessible through the imported default object.
|
|
@@ -117,16 +150,18 @@ import deep from '@superhero/deep'
|
|
|
117
150
|
deep.assign(/* ... */)
|
|
118
151
|
deep.clone(/* ... */)
|
|
119
152
|
deep.freeze(/* ... */)
|
|
153
|
+
deep.intersect(/* ... */)
|
|
120
154
|
deep.merge(/* ... */)
|
|
121
155
|
```
|
|
122
156
|
|
|
123
157
|
### Example:
|
|
124
158
|
```javascript
|
|
125
|
-
import { assign, clone, freeze, merge } from '@superhero/deep'
|
|
159
|
+
import { assign, clone, freeze, intersect, merge } from '@superhero/deep'
|
|
126
160
|
|
|
127
161
|
assign(/* ... */)
|
|
128
162
|
clone(/* ... */)
|
|
129
163
|
freeze(/* ... */)
|
|
164
|
+
intersect(/* ... */)
|
|
130
165
|
merge(/* ... */)
|
|
131
166
|
```
|
|
132
167
|
|
|
@@ -145,87 +180,104 @@ npm test
|
|
|
145
180
|
|
|
146
181
|
```
|
|
147
182
|
▶ @superhero/deep/assign
|
|
148
|
-
✔ Assigns arrays correctly (3.
|
|
149
|
-
✔ Assigns objects correctly (0.
|
|
150
|
-
✔ Overwrites non-object properties correctly (
|
|
151
|
-
✔ Handles undefined values correctly (0.
|
|
183
|
+
✔ Assigns arrays correctly (3.552823ms)
|
|
184
|
+
✔ Assigns objects correctly (0.702394ms)
|
|
185
|
+
✔ Overwrites non-object properties correctly (1.106746ms)
|
|
186
|
+
✔ Handles undefined values correctly (0.592877ms)
|
|
152
187
|
|
|
153
188
|
▶ Descriptor properties
|
|
154
189
|
▶ Retains
|
|
155
|
-
✔ non-writable, non-configurable and non-enumarable (0.
|
|
156
|
-
✔ writable but non-configurable and non-enumarable (0.
|
|
157
|
-
✔ writable and configurable but non-enumarable (0.
|
|
158
|
-
✔ Retains (
|
|
190
|
+
✔ non-writable, non-configurable and non-enumarable (0.774514ms)
|
|
191
|
+
✔ writable but non-configurable and non-enumarable (0.490026ms)
|
|
192
|
+
✔ writable and configurable but non-enumarable (0.314062ms)
|
|
193
|
+
✔ Retains (2.377181ms)
|
|
159
194
|
|
|
160
195
|
▶ Assigns
|
|
161
|
-
✔ non-writable, non-configurable and non-enumarable (0.
|
|
162
|
-
✔ Assigns (0.
|
|
163
|
-
✔ Descriptor properties (3.
|
|
164
|
-
|
|
165
|
-
✔ Merges nested arrays correctly (
|
|
166
|
-
✔ Merges nested objects correctly (0.
|
|
167
|
-
✔ Does not alter objects with no conflicts (0.
|
|
168
|
-
✔ @superhero/deep/assign (
|
|
196
|
+
✔ non-writable, non-configurable and non-enumarable (0.415235ms)
|
|
197
|
+
✔ Assigns (0.824789ms)
|
|
198
|
+
✔ Descriptor properties (3.881836ms)
|
|
199
|
+
|
|
200
|
+
✔ Merges nested arrays correctly (6.642858ms)
|
|
201
|
+
✔ Merges nested objects correctly (0.731397ms)
|
|
202
|
+
✔ Does not alter objects with no conflicts (0.27423ms)
|
|
203
|
+
✔ @superhero/deep/assign (20.322649ms)
|
|
169
204
|
|
|
170
205
|
▶ @superhero/deep/clone
|
|
171
|
-
✔ Clones simple objects (
|
|
172
|
-
✔ Clones nested objects (0.
|
|
173
|
-
✔
|
|
174
|
-
✔
|
|
175
|
-
✔
|
|
176
|
-
✔
|
|
206
|
+
✔ Clones simple objects (6.103605ms)
|
|
207
|
+
✔ Clones nested objects (0.771236ms)
|
|
208
|
+
✔ Preserves descriptors (1.57539ms)
|
|
209
|
+
✔ Clones arrays (1.604074ms)
|
|
210
|
+
✔ Handles circular references (0.507477ms)
|
|
211
|
+
✔ Clones objects with null prototype (1.230476ms)
|
|
212
|
+
✔ @superhero/deep/clone (14.513864ms)
|
|
177
213
|
|
|
178
214
|
▶ @superhero/deep/freeze
|
|
179
|
-
✔ Freezes a simple object (
|
|
180
|
-
✔ Freezes nested objects recursively (0.
|
|
181
|
-
✔ Handles circular references gracefully (0.
|
|
182
|
-
✔ Freezes objects with symbols (0.
|
|
183
|
-
✔ Handles already frozen objects without error (0.
|
|
184
|
-
✔ Freezes objects with non-enumerable properties (0.
|
|
185
|
-
✔ Freezes arrays (0.
|
|
186
|
-
✔ Handles objects with null prototype (0.
|
|
187
|
-
✔ Freezes objects with multiple property types (0.
|
|
188
|
-
✔ @superhero/deep/freeze (
|
|
215
|
+
✔ Freezes a simple object (2.735609ms)
|
|
216
|
+
✔ Freezes nested objects recursively (0.40638ms)
|
|
217
|
+
✔ Handles circular references gracefully (0.781639ms)
|
|
218
|
+
✔ Freezes objects with symbols (0.455776ms)
|
|
219
|
+
✔ Handles already frozen objects without error (0.342712ms)
|
|
220
|
+
✔ Freezes objects with non-enumerable properties (0.455515ms)
|
|
221
|
+
✔ Freezes arrays (0.61924ms)
|
|
222
|
+
✔ Handles objects with null prototype (0.57424ms)
|
|
223
|
+
✔ Freezes objects with multiple property types (0.984788ms)
|
|
224
|
+
✔ @superhero/deep/freeze (12.988297ms)
|
|
189
225
|
|
|
190
226
|
▶ @superhero/deep
|
|
191
|
-
✔ All functions are accessible as a member to the default import object (1.
|
|
192
|
-
✔ All functions are accessible to import from the default import object (0.
|
|
193
|
-
✔ @superhero/deep (
|
|
227
|
+
✔ All functions are accessible as a member to the default import object (1.908797ms)
|
|
228
|
+
✔ All functions are accessible to import from the default import object (0.603152ms)
|
|
229
|
+
✔ @superhero/deep (7.176949ms)
|
|
230
|
+
|
|
231
|
+
▶ @superhero/deep/intersect
|
|
232
|
+
✔ Intersects arrays by value and position (5.556412ms)
|
|
233
|
+
✔ Intersects nested arrays (0.312899ms)
|
|
234
|
+
✔ Handles empty array intersection (0.297251ms)
|
|
235
|
+
✔ Intersects objects with matching keys and values (0.554782ms)
|
|
236
|
+
✔ Deeply intersects nested objects (0.568448ms)
|
|
237
|
+
✔ Intersection stops at type mismatch (0.278157ms)
|
|
238
|
+
✔ Throws on circular references (1.779685ms)
|
|
239
|
+
✔ Intersects arrays with undefined positions (0.702569ms)
|
|
240
|
+
✔ Handles intersection of primitive types (0.525931ms)
|
|
241
|
+
✔ Returns undefined for non-intersecting primitives (0.867342ms)
|
|
242
|
+
✔ Handles multiple sequential intersections (0.836957ms)
|
|
243
|
+
✔ @superhero/deep/intersect (15.753992ms)
|
|
194
244
|
|
|
195
245
|
▶ @superhero/deep/merge
|
|
196
|
-
✔ Merges arrays with unique values (
|
|
197
|
-
✔ Merges arrays with order preserved (0.
|
|
198
|
-
✔ Handles empty arrays correctly (0.
|
|
199
|
-
✔ Handles arrays with duplicate values (0.
|
|
200
|
-
✔ Merges objects and prioritizes restrictive descriptors (0.
|
|
201
|
-
✔ Merges objects with non-enumerable properties (0.
|
|
202
|
-
✔ Handles nested object merging (0.
|
|
203
|
-
✔ Stops at circular references (0.
|
|
204
|
-
✔ Stops when nested and with circular references (
|
|
205
|
-
✔ Returns second value for non-object types (
|
|
206
|
-
✔ Handles multiple merges sequentially (0.
|
|
207
|
-
✔ @superhero/deep/merge (
|
|
208
|
-
|
|
209
|
-
tests
|
|
210
|
-
suites
|
|
211
|
-
pass
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
file
|
|
215
|
-
|
|
216
|
-
assign.js
|
|
217
|
-
assign.test.js
|
|
218
|
-
clone.js
|
|
219
|
-
clone.test.js
|
|
220
|
-
freeze.js
|
|
221
|
-
freeze.test.js
|
|
222
|
-
index.js
|
|
223
|
-
index.test.js
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
246
|
+
✔ Merges arrays with unique values (4.014593ms)
|
|
247
|
+
✔ Merges arrays with order preserved (0.431737ms)
|
|
248
|
+
✔ Handles empty arrays correctly (0.409507ms)
|
|
249
|
+
✔ Handles arrays with duplicate values (0.501899ms)
|
|
250
|
+
✔ Merges objects and prioritizes restrictive descriptors (0.804508ms)
|
|
251
|
+
✔ Merges objects with non-enumerable properties (0.802831ms)
|
|
252
|
+
✔ Handles nested object merging (0.709563ms)
|
|
253
|
+
✔ Stops at circular references (0.768717ms)
|
|
254
|
+
✔ Stops when nested and with circular references (1.386111ms)
|
|
255
|
+
✔ Returns second value for non-object types (1.438328ms)
|
|
256
|
+
✔ Handles multiple merges sequentially (0.494506ms)
|
|
257
|
+
✔ @superhero/deep/merge (14.798621ms)
|
|
258
|
+
|
|
259
|
+
tests 50
|
|
260
|
+
suites 9
|
|
261
|
+
pass 50
|
|
262
|
+
|
|
263
|
+
------------------------------------------------------------------------
|
|
264
|
+
file | line % | branch % | funcs % | uncovered lines
|
|
265
|
+
------------------------------------------------------------------------
|
|
266
|
+
assign.js | 97.80 | 96.15 | 100.00 | 15-16
|
|
267
|
+
assign.test.js | 100.00 | 100.00 | 100.00 |
|
|
268
|
+
clone.js | 95.83 | 93.33 | 100.00 | 22-23
|
|
269
|
+
clone.test.js | 100.00 | 100.00 | 100.00 |
|
|
270
|
+
freeze.js | 100.00 | 100.00 | 100.00 |
|
|
271
|
+
freeze.test.js | 100.00 | 100.00 | 100.00 |
|
|
272
|
+
index.js | 100.00 | 100.00 | 100.00 |
|
|
273
|
+
index.test.js | 100.00 | 100.00 | 100.00 |
|
|
274
|
+
intersect.js | 94.77 | 91.18 | 100.00 | 70-71 85-86 134-137
|
|
275
|
+
intersect.test.js | 100.00 | 100.00 | 100.00 |
|
|
276
|
+
merge.js | 98.72 | 96.30 | 100.00 | 81-82
|
|
277
|
+
merge.test.js | 100.00 | 100.00 | 100.00 |
|
|
278
|
+
------------------------------------------------------------------------
|
|
279
|
+
all files | 98.84 | 96.81 | 100.00 |
|
|
280
|
+
------------------------------------------------------------------------
|
|
229
281
|
```
|
|
230
282
|
|
|
231
283
|
---
|
package/assign.js
CHANGED
package/clone.js
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
export default function clone(input)
|
|
1
|
+
export default function clone(input, options = {})
|
|
2
2
|
{
|
|
3
3
|
const seen = new WeakSet()
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
options.preservesImutable = options.preservesImutable ?? false
|
|
6
|
+
options.preservesEnumerable = options.preservesEnumerable ?? true
|
|
7
|
+
options.fallback = options.fallback ?? cloneFallback.bind(null, options, seen)
|
|
8
|
+
|
|
9
|
+
return deepClone(input, options, seen)
|
|
5
10
|
}
|
|
6
11
|
|
|
7
|
-
function deepClone(value, seen)
|
|
12
|
+
function deepClone(value, options, seen)
|
|
8
13
|
{
|
|
9
14
|
switch(Object.prototype.toString.call(value))
|
|
10
15
|
{
|
|
11
|
-
case '[object Array]' : return cloneArray(value, seen)
|
|
12
|
-
case '[object Object]' : return cloneObject(value, seen)
|
|
16
|
+
case '[object Array]' : return cloneArray(value, options, seen)
|
|
17
|
+
case '[object Object]' : return cloneObject(value, options, seen)
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
return
|
|
20
|
+
return options.fallback(value)
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
function cloneArray(array, seen)
|
|
23
|
+
function cloneArray(array, options, seen)
|
|
19
24
|
{
|
|
20
25
|
if(seen.has(array))
|
|
21
26
|
{
|
|
@@ -23,10 +28,10 @@ function cloneArray(array, seen)
|
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
seen.add(array)
|
|
26
|
-
return array.map((item) => deepClone(item, seen))
|
|
31
|
+
return array.map((item) => deepClone(item, options, seen))
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
function cloneObject(obj, seen)
|
|
34
|
+
function cloneObject(obj, options, seen)
|
|
30
35
|
{
|
|
31
36
|
if(seen.has(obj))
|
|
32
37
|
{
|
|
@@ -39,10 +44,52 @@ function cloneObject(obj, seen)
|
|
|
39
44
|
|
|
40
45
|
for(const key of Object.getOwnPropertyNames(obj))
|
|
41
46
|
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
if(options.preservesImutable)
|
|
48
|
+
{
|
|
49
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, key)
|
|
50
|
+
Object.defineProperty(output, key,
|
|
51
|
+
{
|
|
52
|
+
enumerable : options.preservesEnumerable ? descriptor.enumerable : true,
|
|
53
|
+
writable : descriptor.writable,
|
|
54
|
+
configurable : descriptor.configurable,
|
|
55
|
+
value : deepClone(descriptor.value, options, seen)
|
|
56
|
+
})
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
else if(options.preservesEnumerable)
|
|
60
|
+
{
|
|
61
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, key)
|
|
62
|
+
Object.defineProperty(output, key,
|
|
63
|
+
{
|
|
64
|
+
enumerable : descriptor.enumerable,
|
|
65
|
+
writable : true,
|
|
66
|
+
configurable : true,
|
|
67
|
+
value : deepClone(descriptor.value, options, seen)
|
|
68
|
+
})
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
else
|
|
72
|
+
{
|
|
73
|
+
output[key] = deepClone(obj[key], options, seen)
|
|
74
|
+
}
|
|
45
75
|
}
|
|
46
76
|
|
|
47
77
|
return output
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cloneFallback(options, seen, value)
|
|
81
|
+
{
|
|
82
|
+
if(typeof value !== 'object'
|
|
83
|
+
|| value === null)
|
|
84
|
+
{
|
|
85
|
+
return value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const clone = Object.create(Object.getPrototypeOf(value))
|
|
89
|
+
for(const key in value)
|
|
90
|
+
{
|
|
91
|
+
clone[key] = deepClone(value[key], options, seen)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return clone
|
|
48
95
|
}
|
package/clone.test.js
CHANGED
|
@@ -24,6 +24,32 @@ suite('@superhero/deep/clone', () =>
|
|
|
24
24
|
assert.notStrictEqual(cloned.foo, obj.foo, 'Not the same reference as the original')
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
+
test('Do not preserves descriptors', () =>
|
|
28
|
+
{
|
|
29
|
+
const origin = {}
|
|
30
|
+
|
|
31
|
+
Object.defineProperty(origin, 'foo', { value: {}, enumerable: true, writable: true, configurable: true })
|
|
32
|
+
Object.defineProperty(origin, 'bar', { value: {}, enumerable: false, writable: true, configurable: true })
|
|
33
|
+
Object.defineProperty(origin, 'baz', { value: {}, enumerable: false, writable: false, configurable: true })
|
|
34
|
+
Object.defineProperty(origin, 'qux', { value: {}, enumerable: false, writable: false, configurable: false })
|
|
35
|
+
|
|
36
|
+
const cloned = deepclone(origin, { preservesImutable: false, preservesEnumerable: false })
|
|
37
|
+
|
|
38
|
+
assert.notStrictEqual(cloned, origin, 'Not the same reference as the original')
|
|
39
|
+
assert.notStrictEqual(cloned.foo, origin.foo, 'Cloned nested object should not share reference with the original')
|
|
40
|
+
|
|
41
|
+
const
|
|
42
|
+
clonedDescriptors = Object.getOwnPropertyDescriptors(cloned),
|
|
43
|
+
originDescriptors = Object.getOwnPropertyDescriptors(origin)
|
|
44
|
+
|
|
45
|
+
for(const key in originDescriptors)
|
|
46
|
+
{
|
|
47
|
+
assert.equal(clonedDescriptors[key].enumerable, true)
|
|
48
|
+
assert.equal(clonedDescriptors[key].writable, true)
|
|
49
|
+
assert.equal(clonedDescriptors[key].configurable, true)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
27
53
|
test('Preserves descriptors', () =>
|
|
28
54
|
{
|
|
29
55
|
const origin = {}
|
|
@@ -33,7 +59,7 @@ suite('@superhero/deep/clone', () =>
|
|
|
33
59
|
Object.defineProperty(origin, 'baz', { value: {}, enumerable: false, writable: false, configurable: true })
|
|
34
60
|
Object.defineProperty(origin, 'qux', { value: {}, enumerable: false, writable: false, configurable: false })
|
|
35
61
|
|
|
36
|
-
const cloned = deepclone(origin)
|
|
62
|
+
const cloned = deepclone(origin, { preservesImutable: true, preservesEnumerable: true })
|
|
37
63
|
|
|
38
64
|
assert.deepStrictEqual(cloned, origin, 'Cloned nested object should be equal to the original')
|
|
39
65
|
assert.notStrictEqual(cloned, origin, 'Not the same reference as the original')
|
|
@@ -51,6 +77,21 @@ suite('@superhero/deep/clone', () =>
|
|
|
51
77
|
}
|
|
52
78
|
})
|
|
53
79
|
|
|
80
|
+
test('Does not preserve frozen object state', () =>
|
|
81
|
+
{
|
|
82
|
+
const origin = { foo:'bar', baz: 42, qux: true }
|
|
83
|
+
|
|
84
|
+
Object.freeze(origin)
|
|
85
|
+
|
|
86
|
+
assert.ok(Object.isFrozen(origin), 'Origin object should be frozen')
|
|
87
|
+
|
|
88
|
+
const cloned = deepclone(origin)
|
|
89
|
+
|
|
90
|
+
assert.deepStrictEqual(cloned, origin, 'Cloned nested object should be equal to the original')
|
|
91
|
+
assert.notStrictEqual(cloned, origin, 'Not the same reference as the original')
|
|
92
|
+
assert.ok(false === Object.isFrozen(cloned), 'Cloned object should not be frozen')
|
|
93
|
+
})
|
|
94
|
+
|
|
54
95
|
test('Clones arrays', () =>
|
|
55
96
|
{
|
|
56
97
|
const
|
package/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import assign
|
|
2
|
-
import clone
|
|
3
|
-
import freeze
|
|
4
|
-
import
|
|
1
|
+
import assign from '@superhero/deep/assign'
|
|
2
|
+
import clone from '@superhero/deep/clone'
|
|
3
|
+
import freeze from '@superhero/deep/freeze'
|
|
4
|
+
import intersect from '@superhero/deep/intersect'
|
|
5
|
+
import merge from '@superhero/deep/merge'
|
|
5
6
|
|
|
6
|
-
export { assign, clone, freeze, merge }
|
|
7
|
-
export default { assign, clone, freeze, merge }
|
|
7
|
+
export { assign, clone, freeze, intersect, merge }
|
|
8
|
+
export default { assign, clone, freeze, intersect, merge }
|
package/intersect.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When intersecting two arrays [object Array], the result is a new array
|
|
3
|
+
* containing only values present in both arrays. Duplicate values are removed.
|
|
4
|
+
*
|
|
5
|
+
* @example intersecting [1, 2, 3] and [2, 3, 4] results in [2, 3].
|
|
6
|
+
*
|
|
7
|
+
* ----------------------------------------------------------------------------
|
|
8
|
+
*
|
|
9
|
+
* When intersecting two objects [object Object], the result is a new object
|
|
10
|
+
* with properties that exist in both objects and whose values are also
|
|
11
|
+
* intersecting. The descriptor of the intersected property is defined by
|
|
12
|
+
* the most restrictive rules, same as the merge strategy.
|
|
13
|
+
*
|
|
14
|
+
* @example if key exists in both objects and the values are primitives and
|
|
15
|
+
* equal, that value is kept.
|
|
16
|
+
*
|
|
17
|
+
* @example if the values are nested objects or arrays, the intersection
|
|
18
|
+
* continues recursively.
|
|
19
|
+
*
|
|
20
|
+
* ----------------------------------------------------------------------------
|
|
21
|
+
*
|
|
22
|
+
* When the types differ, no value is intersected.
|
|
23
|
+
*
|
|
24
|
+
* @example intersecting a string and an object results in no match.
|
|
25
|
+
*/
|
|
26
|
+
export default function intersect(a, b, ...c)
|
|
27
|
+
{
|
|
28
|
+
const
|
|
29
|
+
seen = new WeakMap,
|
|
30
|
+
output = intersectAandB(a, b, seen)
|
|
31
|
+
|
|
32
|
+
return c.length
|
|
33
|
+
? intersect(output, ...c)
|
|
34
|
+
: output
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function intersectAandB(a, b, seen)
|
|
38
|
+
{
|
|
39
|
+
if(Object.is(a, b))
|
|
40
|
+
{
|
|
41
|
+
return a
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const
|
|
45
|
+
aType = Object.prototype.toString.call(a),
|
|
46
|
+
bType = Object.prototype.toString.call(b)
|
|
47
|
+
|
|
48
|
+
if(aType !== bType)
|
|
49
|
+
{
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if('[object Array]' === aType)
|
|
54
|
+
{
|
|
55
|
+
return intersectArray(a, b, seen)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if('[object Object]' === aType)
|
|
59
|
+
{
|
|
60
|
+
return intersectObject(a, b, seen)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function intersectArray(a, b, seen)
|
|
67
|
+
{
|
|
68
|
+
if(hasSeen(a, b, seen))
|
|
69
|
+
{
|
|
70
|
+
return seen.get(a).get(b)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const
|
|
74
|
+
values = a.map((value, i) =>
|
|
75
|
+
{
|
|
76
|
+
if(b.includes(value))
|
|
77
|
+
{
|
|
78
|
+
return value
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if('object' === typeof value)
|
|
82
|
+
{
|
|
83
|
+
return intersectAandB(value, b[i], seen)
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
86
|
+
output = values.filter((value) => value !== undefined)
|
|
87
|
+
|
|
88
|
+
seen.get(a).set(b, output)
|
|
89
|
+
|
|
90
|
+
return output
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function intersectObject(a, b, seen)
|
|
94
|
+
{
|
|
95
|
+
if(hasSeen(a, b, seen))
|
|
96
|
+
{
|
|
97
|
+
return seen.get(a).get(b)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const output = {}
|
|
101
|
+
|
|
102
|
+
for(const key of Object.getOwnPropertyNames(a))
|
|
103
|
+
{
|
|
104
|
+
if(false === (key in b))
|
|
105
|
+
{
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const value = intersectAandB(a[key], b[key], seen)
|
|
110
|
+
|
|
111
|
+
if(undefined === value)
|
|
112
|
+
{
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const
|
|
117
|
+
descriptorA = Object.getOwnPropertyDescriptor(a, key),
|
|
118
|
+
descriptorB = Object.getOwnPropertyDescriptor(b, key)
|
|
119
|
+
|
|
120
|
+
Object.defineProperty(output, key,
|
|
121
|
+
{
|
|
122
|
+
configurable : descriptorA.configurable && descriptorB.configurable,
|
|
123
|
+
enumerable : descriptorA.enumerable && descriptorB.enumerable,
|
|
124
|
+
writable : descriptorA.writable && descriptorB.writable,
|
|
125
|
+
value
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
seen.get(a).set(b, output)
|
|
130
|
+
|
|
131
|
+
return output
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hasSeen(a, b, seen)
|
|
135
|
+
{
|
|
136
|
+
if(false === seen.has(a))
|
|
137
|
+
{
|
|
138
|
+
seen.set(a, new WeakMap)
|
|
139
|
+
}
|
|
140
|
+
else if(seen.get(a).has(b))
|
|
141
|
+
{
|
|
142
|
+
if(CircularReferenceError === seen.get(a).get(b))
|
|
143
|
+
{
|
|
144
|
+
throw new CircularReferenceError('Circular reference detected')
|
|
145
|
+
}
|
|
146
|
+
else
|
|
147
|
+
{
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Avoid circular references by initiating the weak map with a
|
|
153
|
+
// ReferenceError that is written over later when the object is
|
|
154
|
+
// fully processed and the reference is not circular, and can
|
|
155
|
+
// then instead be set to the output to act as a cache layer.
|
|
156
|
+
seen.get(a).set(b, CircularReferenceError)
|
|
157
|
+
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
class CircularReferenceError extends ReferenceError
|
|
162
|
+
{
|
|
163
|
+
name = 'CircularReferenceError'
|
|
164
|
+
code = 'E_DEEP_INTERSECT_CIRCULAR_REFERENCE'
|
|
165
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { suite, test } from 'node:test'
|
|
3
|
+
import deepintersect from '@superhero/deep/intersect'
|
|
4
|
+
|
|
5
|
+
suite('@superhero/deep/intersect', () =>
|
|
6
|
+
{
|
|
7
|
+
test('Intersects arrays by value and position', () =>
|
|
8
|
+
{
|
|
9
|
+
const
|
|
10
|
+
a = [1, 2, 3],
|
|
11
|
+
b = [2, 3, 4],
|
|
12
|
+
expected = [2, 3]
|
|
13
|
+
|
|
14
|
+
const result = deepintersect(a, b)
|
|
15
|
+
assert.deepStrictEqual(result, expected, 'Arrays should intersect by value and position')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('Intersects nested arrays', () =>
|
|
19
|
+
{
|
|
20
|
+
const
|
|
21
|
+
a = [1, [2, 3], 4],
|
|
22
|
+
b = [1, [2, 4], 4],
|
|
23
|
+
expected = [1, [2], 4]
|
|
24
|
+
|
|
25
|
+
const result = deepintersect(a, b)
|
|
26
|
+
assert.deepStrictEqual(result, expected, 'Nested arrays should intersect deeply')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('Handles empty array intersection', () =>
|
|
30
|
+
{
|
|
31
|
+
const
|
|
32
|
+
a = [1, 2, 3],
|
|
33
|
+
b = [],
|
|
34
|
+
expected = []
|
|
35
|
+
|
|
36
|
+
const result = deepintersect(a, b)
|
|
37
|
+
assert.deepStrictEqual(result, expected, 'Intersection with empty array results in empty array')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('Intersects objects with matching keys and values', () =>
|
|
41
|
+
{
|
|
42
|
+
const
|
|
43
|
+
a = { foo: 1, bar: 2 },
|
|
44
|
+
b = { foo: 1, baz: 3 },
|
|
45
|
+
expected = { foo: 1 }
|
|
46
|
+
|
|
47
|
+
const result = deepintersect(a, b)
|
|
48
|
+
assert.deepStrictEqual(result, expected, 'Only matching keys with same values should intersect')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('Deeply intersects nested objects', () =>
|
|
52
|
+
{
|
|
53
|
+
const
|
|
54
|
+
a = { foo: { bar: 1, baz: 2 } },
|
|
55
|
+
b = { foo: { bar: 1, baz: 3 } },
|
|
56
|
+
expected = { foo: { bar: 1 } }
|
|
57
|
+
|
|
58
|
+
const result = deepintersect(a, b)
|
|
59
|
+
assert.deepStrictEqual(result, expected, 'Nested objects should deeply intersect')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('Intersection stops at type mismatch', () =>
|
|
63
|
+
{
|
|
64
|
+
const
|
|
65
|
+
a = { foo: [1, 2] },
|
|
66
|
+
b = { foo: { bar: 1 } },
|
|
67
|
+
expected = {}
|
|
68
|
+
|
|
69
|
+
const result = deepintersect(a, b)
|
|
70
|
+
assert.deepStrictEqual(result, expected, 'Type mismatch should result in empty intersection')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('Throws on circular references', () =>
|
|
74
|
+
{
|
|
75
|
+
const a = {}
|
|
76
|
+
const b = {}
|
|
77
|
+
a.self = a
|
|
78
|
+
b.self = b
|
|
79
|
+
|
|
80
|
+
assert.throws(() => deepintersect(a, b), {
|
|
81
|
+
name: 'CircularReferenceError',
|
|
82
|
+
code: 'E_DEEP_INTERSECT_CIRCULAR_REFERENCE'
|
|
83
|
+
}, 'Circular references should throw CircularReferenceError')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('Intersects arrays with undefined positions', () =>
|
|
87
|
+
{
|
|
88
|
+
const
|
|
89
|
+
a = [1, undefined, 3],
|
|
90
|
+
b = [1, 2, 3],
|
|
91
|
+
expected = [1, 3]
|
|
92
|
+
|
|
93
|
+
const result = deepintersect(a, b)
|
|
94
|
+
assert.deepStrictEqual(result, expected, 'Undefined positions should be excluded')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('Handles intersection of primitive types', () =>
|
|
98
|
+
{
|
|
99
|
+
const
|
|
100
|
+
a = 'string',
|
|
101
|
+
b = 'string',
|
|
102
|
+
expected = 'string'
|
|
103
|
+
|
|
104
|
+
const result = deepintersect(a, b)
|
|
105
|
+
assert.strictEqual(result, expected, 'Matching primitive types should intersect')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('Returns undefined for non-intersecting primitives', () =>
|
|
109
|
+
{
|
|
110
|
+
const
|
|
111
|
+
a = 'string',
|
|
112
|
+
b = 'different'
|
|
113
|
+
|
|
114
|
+
const result = deepintersect(a, b)
|
|
115
|
+
assert.strictEqual(result, undefined, 'Non-matching primitive types should return undefined')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('Handles multiple sequential intersections', () =>
|
|
119
|
+
{
|
|
120
|
+
const
|
|
121
|
+
a = { foo: 1, bar: 2 },
|
|
122
|
+
b = { bar: 2, baz: 3 },
|
|
123
|
+
c = { bar: 2, qux: 4 },
|
|
124
|
+
expected = { bar: 2 }
|
|
125
|
+
|
|
126
|
+
const result = deepintersect(a, b, c)
|
|
127
|
+
assert.deepStrictEqual(result, expected, 'Multiple objects should intersect sequentially')
|
|
128
|
+
})
|
|
129
|
+
})
|
package/merge.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import deepclone from '@superhero/deep/clone'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* When merging two objects [object Object], a new object is created with the
|
|
3
5
|
* properties of both objects defined. The descriptor of the property in the
|
|
@@ -64,7 +66,7 @@ export default function merge(a, b, ...c)
|
|
|
64
66
|
|
|
65
67
|
return c.length
|
|
66
68
|
? merge(output, ...c)
|
|
67
|
-
: output
|
|
69
|
+
: deepclone(output, { preservesImutable: true })
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
function mergeAandB(a, b, seen)
|
|
@@ -74,6 +76,11 @@ function mergeAandB(a, b, seen)
|
|
|
74
76
|
return a
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
if(Object.is(a, b))
|
|
80
|
+
{
|
|
81
|
+
return a
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
const
|
|
78
85
|
aType = Object.prototype.toString.call(a),
|
|
79
86
|
bType = Object.prototype.toString.call(b)
|
package/merge.test.js
CHANGED
|
@@ -134,7 +134,7 @@ suite('@superhero/deep/merge', () =>
|
|
|
134
134
|
|
|
135
135
|
const result = deepmerge(a, b)
|
|
136
136
|
|
|
137
|
-
assert.
|
|
137
|
+
assert.deepStrictEqual(result.self, b.self, 'Circular references should not merge further')
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
test('Stops when nested and with circular references', () =>
|
|
@@ -149,8 +149,8 @@ suite('@superhero/deep/merge', () =>
|
|
|
149
149
|
resultA = deepmerge(a, b),
|
|
150
150
|
resultB = deepmerge(b, a)
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
assert.deepStrictEqual(resultA.foo.bar.foo.bar, b, 'Circular references should not interfare with the merged result')
|
|
153
|
+
assert.deepStrictEqual(resultB.foo.bar.foo.bar, 'baz', 'Circular references should not interfare with the merged result')
|
|
154
154
|
})
|
|
155
155
|
|
|
156
156
|
test('Returns second value for non-object types', () =>
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superhero/deep",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "A collection of deep structure operations",
|
|
5
|
-
"keywords": ["deep", "assign", "clone", "freeze", "merge"],
|
|
5
|
+
"keywords": ["deep", "assign", "clone", "freeze", "intersect", "merge"],
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"type": "module",
|