Skip to content

Commit

Permalink
Add mergeDeep function (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
dentrado authored and guigrpa committed Oct 25, 2017
1 parent e5e0e28 commit 2d7ae1d
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 8 deletions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>): 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.
Expand Down
1 change: 1 addition & 0 deletions src/api.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -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);
62 changes: 54 additions & 8 deletions src/timm.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function clone<T: ArrayOrObject>(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);
Expand All @@ -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;
Expand Down Expand Up @@ -461,8 +464,50 @@ export function merge(
f: ?Object, ...rest: Array<?Object>
): 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>): 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>): 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()
Expand Down Expand Up @@ -497,9 +542,9 @@ export function mergeIn<T: ArrayOrObject>(
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);
}
Expand Down Expand Up @@ -569,8 +614,8 @@ export function addDefaults(
f: ?Object, ...rest: Array<?Object>
): 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);
}

// ===============================================
Expand All @@ -593,6 +638,7 @@ const timm = {
update,
updateIn,
merge,
mergeDeep,
mergeIn,
omit,
addDefaults,
Expand Down
29 changes: 29 additions & 0 deletions test/objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
//------------------------------------------------
Expand Down

0 comments on commit 2d7ae1d

Please sign in to comment.