@tim-code/my-util 0.5.2 → 0.5.3
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/package.json +1 -1
- package/src/object.js +65 -7
- package/src/object.test.js +191 -0
package/package.json
CHANGED
package/src/object.js
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns if the argument is an object.
|
|
3
|
+
* @param {any} thing
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function isObject(thing) {
|
|
7
|
+
return typeof thing === "object" && thing !== null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new object with values created by calling callback on each of argument's values.
|
|
12
|
+
* @param {Object} object
|
|
13
|
+
* @param {Function} callback (value, key, object) => newValue // note if not changing value, should return value
|
|
14
|
+
* @returns {Object}
|
|
15
|
+
*/
|
|
16
|
+
export function mapValues(object, callback) {
|
|
17
|
+
const result = {}
|
|
18
|
+
const keys = Object.keys(object)
|
|
19
|
+
for (const key of keys) {
|
|
20
|
+
result[key] = callback(object[key], key, object)
|
|
21
|
+
}
|
|
22
|
+
return result
|
|
23
|
+
}
|
|
24
|
+
|
|
1
25
|
/**
|
|
2
26
|
* Mutates the passed in object by calling callback on each of its values.
|
|
3
27
|
* @param {Object} object
|
|
@@ -56,10 +80,31 @@ export function like(template) {
|
|
|
56
80
|
}
|
|
57
81
|
}
|
|
58
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Copies the source recursively.
|
|
85
|
+
* Does not preserve constructors of source or constructors of its keys' values.
|
|
86
|
+
* @template T
|
|
87
|
+
* @param {T} source
|
|
88
|
+
* @returns {T}
|
|
89
|
+
*/
|
|
90
|
+
export function deepCopy(source) {
|
|
91
|
+
if (Array.isArray(source)) {
|
|
92
|
+
return source.map(deepCopy)
|
|
93
|
+
}
|
|
94
|
+
if (isObject(source)) {
|
|
95
|
+
return mapValues(source, deepCopy)
|
|
96
|
+
}
|
|
97
|
+
// primitive or function
|
|
98
|
+
return source
|
|
99
|
+
}
|
|
100
|
+
|
|
59
101
|
/**
|
|
60
102
|
* Deeply merges one or more source objects into a target object.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
103
|
+
* Specifically:
|
|
104
|
+
* If the target object has the key and the target's key's value is an non-array object AND
|
|
105
|
+
* the source object's value is a non-array object, recursively merges the source into the target.
|
|
106
|
+
* Otherwise, assigns source's key's value into the target's key.
|
|
107
|
+
* This means that arrays are never merged into arrays or other objects.
|
|
63
108
|
* @param {Object} target The target object that will receive the merged properties.
|
|
64
109
|
* @param {...Object} sources The source objects whose properties will be merged into the target.
|
|
65
110
|
* @returns {Object} The target object with the merged properties from all source objects.
|
|
@@ -76,10 +121,9 @@ export function deepMerge(target, ...sources) {
|
|
|
76
121
|
if (Array.isArray(sourceValue)) {
|
|
77
122
|
target[key] = sourceValue
|
|
78
123
|
} else if (
|
|
79
|
-
targetValue &&
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
typeof sourceValue === "object"
|
|
124
|
+
isObject(targetValue) &&
|
|
125
|
+
isObject(sourceValue) &&
|
|
126
|
+
!Array.isArray(targetValue)
|
|
83
127
|
) {
|
|
84
128
|
deepMerge(targetValue, sourceValue)
|
|
85
129
|
} else {
|
|
@@ -90,6 +134,20 @@ export function deepMerge(target, ...sources) {
|
|
|
90
134
|
return target
|
|
91
135
|
}
|
|
92
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Produces a deep merge of a deep copy of each source object. See deepCopy() and deepMerge() documentation for caveats.
|
|
139
|
+
* @param {...Object} sources The source objects whose properties will be merged into the returned object
|
|
140
|
+
* @returns {Object}
|
|
141
|
+
*/
|
|
142
|
+
export function deepMergeCopy(...sources) {
|
|
143
|
+
if (!sources.length) {
|
|
144
|
+
return {}
|
|
145
|
+
}
|
|
146
|
+
const copies = sources.map(deepCopy)
|
|
147
|
+
const result = deepMerge(...copies)
|
|
148
|
+
return result
|
|
149
|
+
}
|
|
150
|
+
|
|
93
151
|
/**
|
|
94
152
|
* Deeply compares two values to determine if they are equal.
|
|
95
153
|
* Objects and arrays are compared recursively by their properties and elements.
|
|
@@ -105,7 +163,7 @@ export function deepEqual(a, b) {
|
|
|
105
163
|
if (a === b) {
|
|
106
164
|
return true
|
|
107
165
|
}
|
|
108
|
-
if (
|
|
166
|
+
if (!isObject(a) || !isObject(b)) {
|
|
109
167
|
return false
|
|
110
168
|
}
|
|
111
169
|
const keysA = Object.keys(a)
|
package/src/object.test.js
CHANGED
|
@@ -1,14 +1,89 @@
|
|
|
1
1
|
/* eslint-disable no-restricted-syntax */
|
|
2
2
|
import { jest } from "@jest/globals"
|
|
3
3
|
import {
|
|
4
|
+
deepCopy,
|
|
4
5
|
deepEqual,
|
|
5
6
|
deepMerge,
|
|
7
|
+
deepMergeCopy,
|
|
6
8
|
deleteUndefinedValues,
|
|
9
|
+
isObject,
|
|
7
10
|
like,
|
|
11
|
+
mapValues,
|
|
8
12
|
mutateValues,
|
|
9
13
|
via,
|
|
10
14
|
} from "./object.js"
|
|
11
15
|
|
|
16
|
+
// --- isObject ---
|
|
17
|
+
describe("isObject", () => {
|
|
18
|
+
it("returns true for plain objects", () => {
|
|
19
|
+
expect(isObject({})).toBe(true)
|
|
20
|
+
expect(isObject({ a: 1 })).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("returns true for arrays", () => {
|
|
24
|
+
expect(isObject([])).toBe(true)
|
|
25
|
+
expect(isObject([1, 2])).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("returns false for null", () => {
|
|
29
|
+
expect(isObject(null)).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("returns false for primitives", () => {
|
|
33
|
+
expect(isObject(1)).toBe(false)
|
|
34
|
+
expect(isObject("str")).toBe(false)
|
|
35
|
+
expect(isObject(true)).toBe(false)
|
|
36
|
+
expect(isObject(undefined)).toBe(false)
|
|
37
|
+
expect(isObject(Symbol("blah"))).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("returns false for functions", () => {
|
|
41
|
+
expect(isObject(() => {})).toBe(false)
|
|
42
|
+
// eslint-disable-next-line func-names, prefer-arrow-callback, no-empty-function
|
|
43
|
+
expect(isObject(function () {})).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// --- mapValues ---
|
|
48
|
+
describe("mapValues", () => {
|
|
49
|
+
it("maps values in the object using the callback", () => {
|
|
50
|
+
const obj = { a: 1, b: 2 }
|
|
51
|
+
const result = mapValues(obj, (v) => v * 2)
|
|
52
|
+
expect(result).toEqual({ a: 2, b: 4 })
|
|
53
|
+
// original object is not mutated
|
|
54
|
+
expect(obj).toEqual({ a: 1, b: 2 })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("callback receives value, key, and object", () => {
|
|
58
|
+
const obj = { x: 1 }
|
|
59
|
+
const cb = jest.fn((v) => v + 1)
|
|
60
|
+
mapValues(obj, cb)
|
|
61
|
+
expect(cb).toHaveBeenCalledWith(1, "x", obj)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("returns a new object", () => {
|
|
65
|
+
const obj = { foo: "bar" }
|
|
66
|
+
const returned = mapValues(obj, (v) => v)
|
|
67
|
+
expect(returned).not.toBe(obj)
|
|
68
|
+
expect(returned).toEqual(obj)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("handles empty object", () => {
|
|
72
|
+
const obj = {}
|
|
73
|
+
expect(mapValues(obj, (v) => v)).toEqual({})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("does not map inherited enumerable properties", () => {
|
|
77
|
+
const proto = { inherited: 1 }
|
|
78
|
+
const obj = Object.create(proto)
|
|
79
|
+
obj.own = 2
|
|
80
|
+
const result = mapValues(obj, (v) => v + 1)
|
|
81
|
+
expect(result).toEqual({ own: 3 })
|
|
82
|
+
expect(result.inherited).toBeUndefined()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// --- mutateValues ---
|
|
12
87
|
describe("mutateValues", () => {
|
|
13
88
|
it("mutates values in the object using the callback", () => {
|
|
14
89
|
const obj = { a: 1, b: 2 }
|
|
@@ -45,6 +120,7 @@ describe("mutateValues", () => {
|
|
|
45
120
|
})
|
|
46
121
|
})
|
|
47
122
|
|
|
123
|
+
// --- deleteUndefinedValues ---
|
|
48
124
|
describe("deleteUndefinedValues", () => {
|
|
49
125
|
it("removes keys with undefined values", () => {
|
|
50
126
|
const obj = { a: 1, b: undefined, c: 3 }
|
|
@@ -84,6 +160,7 @@ describe("deleteUndefinedValues", () => {
|
|
|
84
160
|
})
|
|
85
161
|
})
|
|
86
162
|
|
|
163
|
+
// --- via ---
|
|
87
164
|
describe("via", () => {
|
|
88
165
|
it("returns a function that accesses the given key", () => {
|
|
89
166
|
const getFoo = via("foo")
|
|
@@ -110,6 +187,7 @@ describe("via", () => {
|
|
|
110
187
|
})
|
|
111
188
|
})
|
|
112
189
|
|
|
190
|
+
// --- like ---
|
|
113
191
|
describe("contains", () => {
|
|
114
192
|
it("returns true when object contains all template keys with same values", () => {
|
|
115
193
|
const template = { a: 1, b: 2 }
|
|
@@ -167,6 +245,66 @@ describe("contains", () => {
|
|
|
167
245
|
})
|
|
168
246
|
})
|
|
169
247
|
|
|
248
|
+
// --- deepCopy ---
|
|
249
|
+
describe("deepCopy", () => {
|
|
250
|
+
it("copies primitives as is", () => {
|
|
251
|
+
expect(deepCopy(1)).toBe(1)
|
|
252
|
+
expect(deepCopy("str")).toBe("str")
|
|
253
|
+
expect(deepCopy(null)).toBe(null)
|
|
254
|
+
expect(deepCopy(undefined)).toBe(undefined)
|
|
255
|
+
const sym = Symbol("x")
|
|
256
|
+
expect(deepCopy(sym)).toBe(sym)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it("copies arrays recursively", () => {
|
|
260
|
+
const arr = [1, { a: 2 }, [3, 4]]
|
|
261
|
+
const copy = deepCopy(arr)
|
|
262
|
+
expect(copy).toEqual(arr)
|
|
263
|
+
expect(copy).not.toBe(arr)
|
|
264
|
+
expect(copy[1]).not.toBe(arr[1])
|
|
265
|
+
expect(copy[2]).not.toBe(arr[2])
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it("copies objects recursively", () => {
|
|
269
|
+
const obj = { a: { b: 2 }, c: [1, 2] }
|
|
270
|
+
const copy = deepCopy(obj)
|
|
271
|
+
expect(copy).toEqual(obj)
|
|
272
|
+
expect(copy).not.toBe(obj)
|
|
273
|
+
expect(copy.a).not.toBe(obj.a)
|
|
274
|
+
expect(copy.c).not.toBe(obj.c)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it("does not preserve constructors", () => {
|
|
278
|
+
function Foo() {
|
|
279
|
+
this.x = 1
|
|
280
|
+
}
|
|
281
|
+
const foo = new Foo()
|
|
282
|
+
const copy = deepCopy(foo)
|
|
283
|
+
expect(copy).toEqual({ x: 1 })
|
|
284
|
+
// The constructor is not preserved
|
|
285
|
+
expect(copy instanceof Foo).toBe(false)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it("copies empty object/array", () => {
|
|
289
|
+
expect(deepCopy({})).toEqual({})
|
|
290
|
+
expect(deepCopy([])).toEqual([])
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it("copies nested structures", () => {
|
|
294
|
+
const obj = { a: [{ b: 2 }, { c: [3] }] }
|
|
295
|
+
const copy = deepCopy(obj)
|
|
296
|
+
expect(copy).toEqual(obj)
|
|
297
|
+
expect(copy.a[0]).not.toBe(obj.a[0])
|
|
298
|
+
expect(copy.a[1].c).not.toBe(obj.a[1].c)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it("copies functions as is (does not clone)", () => {
|
|
302
|
+
const fn = () => 42
|
|
303
|
+
expect(deepCopy(fn)).toBe(fn)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
// --- deepMerge ---
|
|
170
308
|
describe("deepMerge", () => {
|
|
171
309
|
it("merges flat objects", () => {
|
|
172
310
|
const target = { a: 1, b: 2 }
|
|
@@ -263,8 +401,61 @@ describe("deepMerge", () => {
|
|
|
263
401
|
const result = deepMerge(target, { b: 2 })
|
|
264
402
|
expect(result).toBe(target)
|
|
265
403
|
})
|
|
404
|
+
|
|
405
|
+
it("assigns object over array if source value is object and target value is array", () => {
|
|
406
|
+
const target = { a: [1, 2] }
|
|
407
|
+
const source = { a: { x: 3 } }
|
|
408
|
+
expect(deepMerge(target, source)).toEqual({ a: { x: 3 } })
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it("assigns array over object if source value is array and target value is object", () => {
|
|
412
|
+
const target = { a: { x: 1 } }
|
|
413
|
+
const source = { a: [2, 3] }
|
|
414
|
+
expect(deepMerge(target, source)).toEqual({ a: [2, 3] })
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it("recursively merges only non-array objects", () => {
|
|
418
|
+
const target = { a: { b: 1 }, arr: { x: 1 } }
|
|
419
|
+
const source = { a: { c: 2 }, arr: [1, 2] }
|
|
420
|
+
expect(deepMerge(target, source)).toEqual({ a: { b: 1, c: 2 }, arr: [1, 2] })
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
// --- deepMergeCopy ---
|
|
425
|
+
describe("deepMergeCopy", () => {
|
|
426
|
+
it("deeply merges deep copies of sources", () => {
|
|
427
|
+
const s1 = { a: { b: 1 } }
|
|
428
|
+
const s2 = { a: { c: 2 } }
|
|
429
|
+
const merged = deepMergeCopy(s1, s2)
|
|
430
|
+
expect(merged).toEqual({ a: { b: 1, c: 2 } })
|
|
431
|
+
expect(merged).not.toBe(s1)
|
|
432
|
+
expect(merged).not.toBe(s2)
|
|
433
|
+
expect(merged.a).not.toBe(s1.a)
|
|
434
|
+
expect(merged.a).not.toBe(s2.a)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it("does not mutate source objects", () => {
|
|
438
|
+
const s1 = { a: 1 }
|
|
439
|
+
const s2 = { b: 2 }
|
|
440
|
+
const orig1 = JSON.stringify(s1)
|
|
441
|
+
const orig2 = JSON.stringify(s2)
|
|
442
|
+
deepMergeCopy(s1, s2)
|
|
443
|
+
expect(JSON.stringify(s1)).toBe(orig1)
|
|
444
|
+
expect(JSON.stringify(s2)).toBe(orig2)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it("handles arrays and primitives", () => {
|
|
448
|
+
const s1 = { a: [1, 2] }
|
|
449
|
+
const s2 = { a: [3, 4], b: 5 }
|
|
450
|
+
expect(deepMergeCopy(s1, s2)).toEqual({ a: [3, 4], b: 5 })
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it("returns {} if called with no sources", () => {
|
|
454
|
+
expect(deepMergeCopy()).toEqual({})
|
|
455
|
+
})
|
|
266
456
|
})
|
|
267
457
|
|
|
458
|
+
// --- deepEqual ---
|
|
268
459
|
describe("deepEqual", () => {
|
|
269
460
|
it("returns true for strictly equal primitives", () => {
|
|
270
461
|
expect(deepEqual(1, 1)).toBe(true)
|