Skip to content

Instantly share code, notes, and snippets.

@jasonrhodes
Last active November 30, 2018 04:01
Show Gist options
  • Save jasonrhodes/8c56febf0f8a87cea8ee0a6c5865faf1 to your computer and use it in GitHub Desktop.
Save jasonrhodes/8c56febf0f8a87cea8ee0a6c5865faf1 to your computer and use it in GitHub Desktop.
Compares two objects and gives you detailed diff results about added, removed, changed, and ref-only changes
/**
* flattens an object into a single dimension with dot-separated key paths
*
* e.g.
* const test = { a: 'hello', b: { xx: 'cool', yy: 'yeah' }, c: [1, 2] }
* flatten(test) => { 'a': 'hello', 'b.xx': 'cool', 'b.yy': 'yeah', 'c.0': 1, 'c.1': 2 }
*/
function flatten(o, path = []) {
return Object.entries(o).reduce((result, [key, value]) => {
const localPath = [...path, key];
if (typeof value === 'object' && value !== null) {
return { ...result, ...flatten(value, localPath) };
}
return { ...result, [localPath.join('.')]: value };
}, {});
}
/**
* Return object that includes all keys from b exclusive of a
*
* e.g.
* const joe = { name: 'joe', age: 21 }
* const jane = { name: 'jane', birthplace: 'london' }
* filterExclusiveKeys(joe, jane) => { birthplace: 'london' }
* filterExclusiveKeys(jane, joe) => { age: 21 }
*/
function filterExclusiveKeys(a, b) {
const exclusiveKeys = Object.keys(b).filter(key => !Object.keys(a).includes(key));
return exclusiveKeys.reduce((result, key) => ({ ...result, [key]: b[key] }), {});
}
/**
* Takes a flattened hash and gives you a list of all the root keys
*
* e.g.
* const colors = { 'a.b.c': 'red', 'x.y.z': 'blue' }
* findRootKeys(colors) => ['a', 'x']
*/
function findRootKeys(flatHash) {
const rootKeys = Object.keys(flatHash).map((k) => k.split('.')[0]);
return [ ...new Set(rootKeys) ]; // unique
}
function diff(p, n) {
const prev = flatten(p);
const next = flatten(n);
const added = filterExclusiveKeys(prev, next);
const removed = filterExclusiveKeys(next, prev);
const changedKeys = Object.keys(next).filter(key => {
if (Object.keys(added).includes(key)) {
return false;
}
if (Object.keys(removed).includes(key)) {
return false;
}
if (prev[key] === next[key]) {
return false;
}
return true;
});
const changed = changedKeys.reduce((result, key) => {
return { ...result, [key]: `${prev[key]} => ${next[key]}` };
}, {});
const changedRoots = [
...findRootKeys(added),
...findRootKeys(removed),
...findRootKeys(changed)
];
const refs = Object.keys(n).filter(
key =>
!changedRoots.includes(key) &&
typeof n[key] === 'object' &&
n[key] !== p[key]
);
return { added, removed, changed, refs };
}
const prevProps = {
name: {
first: "Jo",
last: "Rogers"
},
age: 39,
address: {
street: "Gum Drop Ln",
number: 123,
zip: 22222
}
}
const nextProps = {
name: {
first: "Jo",
last: "Rogers"
},
age: 40,
address: {
city: "Albuquerque",
zip: 22223
}
}
console.log(
JSON.stringify(
diff(prevProps, nextProps),
null,
2
)
);
// produces:
{
"added": {
"address.city": "Albuquerque"
},
"removed": {
"address.street": "Gum Drop Ln",
"address.number": 123
},
"changed": {
"age": "39 => 40",
"address.zip": "22222 => 22223"
},
"refs": [
"name"
]
}
// NOTE: name shows up in refs here because the deep values all stayed the same
// but the object _reference_ changed - this only works for top-level keys
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment