From 2d7ae1db12322aee50b7522fd5563c59784f6f8b Mon Sep 17 00:00:00 2001 From: Martin Forsgren Date: Wed, 25 Oct 2017 09:32:52 +0200 Subject: [PATCH] Add mergeDeep function (#14) --- README.md | 33 ++++++++++++++++++++++++++ src/api.js.flow | 1 + src/timm.js | 62 ++++++++++++++++++++++++++++++++++++++++++------- test/objects.js | 29 +++++++++++++++++++++++ 4 files changed, 117 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4cdf2e4..fb13132 100755 --- a/README.md +++ b/README.md @@ -346,6 +346,39 @@ merge(obj1, { c: 3 }) === obj1 // true ``` +#### mergeDeep() +Returns a new object built as follows: the overlapping keys from the +second one overwrite the corresponding entries from the first one. +If both the first and second entries are objects they are merged recursively. +Similar to `Object.assign()`, but immutable, and deeply merging. + +Usage: + +* `mergeDeep(obj1: Object, obj2: ?Object): Object` +* `mergeDeep(obj1: Object, ...objects: Array): Object` + +The unmodified `obj1` is returned if `obj2` does not *provide something +new to* `obj1`, i.e. if either of the following +conditions are true: + +* `obj2` is `null` or `undefined` +* `obj2` is an object, but it is empty +* All attributes of `obj2` are referentially equal to the + corresponding attributes of `obj` + +```js +obj1 = { a: 1, b: 2, c: { a: 1 } } +obj2 = { b: 3, c: { b: 2 } } +obj3 = mergeDeep(obj1, obj2) +// { a: 1, b: 3, c: { a: 1, b: 2 } } +obj3 === obj1 +// false + +// The same object is returned if there are no changes: +mergeDeep(obj1, { c: { a: 1 } }) === obj1 +// true +``` + #### mergeIn() Similar to `merge()`, but merging the value at a given nested path. Note that the returned type is the same as that of the first argument. diff --git a/src/api.js.flow b/src/api.js.flow index 62ba361..5e8c5be 100755 --- a/src/api.js.flow +++ b/src/api.js.flow @@ -17,6 +17,7 @@ declare export var setIn: typeof(timm.setIn); declare export var update: typeof(timm.update); declare export var updateIn: typeof(timm.updateIn); declare export var merge: typeof(timm.merge); +declare export var mergeDeep: typeof(timm.mergeDeep); declare export var mergeIn: typeof(timm.mergeIn); declare export var omit: typeof(timm.omit); declare export var addDefaults: typeof(timm.addDefaults); diff --git a/src/timm.js b/src/timm.js index 7ea0f19..16d188f 100755 --- a/src/timm.js +++ b/src/timm.js @@ -34,7 +34,7 @@ export function clone(obj: T): T { return out; } -function doMerge(fAddDefaults: boolean, first: ArrayOrObject, ...rest: any): any { +function doMerge(fAddDefaults: boolean, fDeep: boolean, first: ArrayOrObject, ...rest: any): any { let out = first; !(out != null) && throwStr(process.env.NODE_ENV !== 'production' ? 'At least one object should be provided to merge()' : INVALID_ARGS); @@ -47,7 +47,10 @@ function doMerge(fAddDefaults: boolean, first: ArrayOrObject, ...rest: any): any for (let j = 0; j <= keys.length; j++) { const key = keys[j]; if (fAddDefaults && out[key] !== undefined) continue; - const nextVal = obj[key]; + let nextVal = obj[key]; + if (fDeep && isObject(out[key]) && isObject(nextVal)) { + nextVal = doMerge(fAddDefaults, fDeep, out[key], nextVal); + } if (nextVal === undefined || nextVal === out[key]) continue; if (!fChanged) { fChanged = true; @@ -461,8 +464,50 @@ export function merge( f: ?Object, ...rest: Array ): Object { return rest.length ? - doMerge.call(null, false, a, b, c, d, e, f, ...rest) : - doMerge(false, a, b, c, d, e, f); + doMerge.call(null, false, false, a, b, c, d, e, f, ...rest) : + doMerge(false, false, a, b, c, d, e, f); +} + + +// -- #### mergeDeep() +// -- Returns a new object built as follows: the overlapping keys from the +// -- second one overwrite the corresponding entries from the first one. +// -- If both the first and second entries are objects they are merged recursively. +// -- Similar to `Object.assign()`, but immutable, and deeply merging. +// -- +// -- Usage: +// -- +// -- * `mergeDeep(obj1: Object, obj2: ?Object): Object` +// -- * `mergeDeep(obj1: Object, ...objects: Array): Object` +// -- +// -- The unmodified `obj1` is returned if `obj2` does not *provide something +// -- new to* `obj1`, i.e. if either of the following +// -- conditions are true: +// -- +// -- * `obj2` is `null` or `undefined` +// -- * `obj2` is an object, but it is empty +// -- * All attributes of `obj2` are referentially equal to the +// -- corresponding attributes of `obj` +// -- +// -- ```js +// -- obj1 = { a: 1, b: 2, c: { a: 1 } } +// -- obj2 = { b: 3, c: { b: 2 } } +// -- obj3 = mergeDeep(obj1, obj2) +// -- // { a: 1, b: 3, c: { a: 1, b: 2 } } +// -- obj3 === obj1 +// -- // false +// -- +// -- // The same object is returned if there are no changes: +// -- mergeDeep(obj1, { c: { a: 1 } }) === obj1 +// -- // true +// -- ``` +export function mergeDeep(a: Object, + b: ?Object, c: ?Object, + d: ?Object, e: ?Object, + f: ?Object, ...rest: Array): Object { + return rest.length ? + doMerge.call(null, false, true, a, b, c, d, e, f, ...rest) : + doMerge(false, true, a, b, c, d, e, f); } // -- #### mergeIn() @@ -497,9 +542,9 @@ export function mergeIn( if (prevVal == null) prevVal = {}; let nextVal; if (rest.length) { - nextVal = doMerge.call(null, false, prevVal, b, c, d, e, f, ...rest); + nextVal = doMerge.call(null, false, false, prevVal, b, c, d, e, f, ...rest); } else { - nextVal = doMerge(false, prevVal, b, c, d, e, f); + nextVal = doMerge(false, false, prevVal, b, c, d, e, f); } return setIn(a, path, nextVal); } @@ -569,8 +614,8 @@ export function addDefaults( f: ?Object, ...rest: Array ): Object { return rest.length ? - doMerge.call(null, true, a, b, c, d, e, f, ...rest) : - doMerge(true, a, b, c, d, e, f); + doMerge.call(null, true, false, a, b, c, d, e, f, ...rest) : + doMerge(true, false, a, b, c, d, e, f); } // =============================================== @@ -593,6 +638,7 @@ const timm = { update, updateIn, merge, + mergeDeep, mergeIn, omit, addDefaults, diff --git a/test/objects.js b/test/objects.js index df291ce..7dbe29f 100755 --- a/test/objects.js +++ b/test/objects.js @@ -310,6 +310,35 @@ test('merge: multiple: should return the same object when it hasn\'t changed', ( t.is(obj2, OBJ); }); +//------------------------------------------------ +// mergeDeep() +//------------------------------------------------ +test('mergeDeep: should merge deeply', (t) => { + const obj2 = timm.mergeDeep({ a: 1, b: { a: 1, b:2 } }, { b: { b: 3 } }); + t.deepEqual(obj2, { a: 1, b: { a: 1, b: 3 } }); +}); + +test('mergeDeep: multiple: should merge deeply', (t) => { + const obj2 = timm.mergeDeep({ a: 1, b: { a: 1, b: 1 } }, { b: { b: 2, c: 2 } }, { a: 3, b: { c: 3 } }); + t.deepEqual(obj2, { a: 3, b: { a: 1, b: 2, c:3 } }); +}); + +test('mergeDeep: should return the same object when it hasn\'t changed', (t) => { + const obj1 = { a: 1, b: { a: 1, b: 2 }}; + const obj2 = timm.mergeDeep(obj1, { a: 1, b: {} }, {a: 1, b: obj1.b }); + t.is(obj1, obj2); +}); + +test('mergeDeep: multiple: should return the same object when it hasn\'t changed', (t) => { + const obj2 = timm.mergeDeep(OBJ, { b: 2 }, { d: { b: OBJ.d.b } }, { c: undefined }); + t.is(obj2, OBJ); +}); + +test('mergeDeep: with more than 6 args', (t) => { + const obj2 = timm.mergeDeep({ a: 1 }, { b: { a: 1 } }, { c: 3 }, { d: 4 }, { e: 5 }, { f: 6 }, { b: { b: 2 } }); + t.deepEqual(obj2, { a: 1, b: { a: 1, b: 2 }, c: 3, d: 4, e: 5, f: 6 }); +}); + //------------------------------------------------ // mergeIn() //------------------------------------------------