Compare nested objects and list out differences in JavaScript - javascript

I want the difference in such a way that the I don't return the entire nested object if any of the values is different.
I have seen solutions online and they all return the entire nested objects and it doesn't work if only 1 key-value pair is changed. i don't want to show the difference as a complete nested object. it should be easier for any user to read.
for eg:
const A = {
position: 2,
attributes: [{
code: 123,
name: "xyz",
params: {
label: "hehe",
units: "currency"
}
}],
code: 1
}
const B = {
position: 3,
attributes: [{
code: 123,
name: "xyzr",
params: {
label: "heh",
units: "currency"
}
}],
code: 1
}
I want the output to be like this:
difference: {
position: {
current: 2,
previous: 3
},
attributes: {
current : [{ name: "xyz", params: { label: "hehe" } }],
previous: [{ name: "xyzr", params: {label: "heh"}}]
}
}
The code that I tried:
const compareEditedChanges = (A: any, B: any) => {
const allKeys = _.union(_.keys(A), _.keys(B));
try {
setDifference(
_.reduce(
allKeys,
(result: any, key) => {
if (!_.isEqual(A?.[key], B?.[key])) {
result[key] = {
current: A[key],
previous: B[key]
};
}
return result;
},
{}
)
);
} catch (err) {
console.error(err);
}
return difference;
};

After giving it a lot of thought to the code, I came with my own solution for a deeply nested objects comparison and listing out the differences in an object with keys as current and previous.
I didn't use any inbuilt libraries and wrote the code with simple for loop, recursion and map
const compareEditedChanges = (
previousState,
currentState
) => {
const result = [];
for (const key in currentState) {
// if value is string or number or boolean
if (
typeof currentState[key] === 'string' ||
typeof currentState[key] === 'number' ||
typeof currentState[key] === 'boolean'
) {
if (String(currentState[key]) !== String(previousState[key])) {
result.push({
[key]: {
current: currentState[key],
previous: previousState[key]
}
});
}
}
// if an array
if (
Array.isArray(currentState[key]) ||
Array.isArray(previousState[key])
) {
console.log(currentState[key])
if (currentState[key].length > 0 || previousState[key].length > 0) {
currentState[key].map((value, index) => {
// check for array of string or number or boolean
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
if (
JSON.stringify(currentState[key]) !==
JSON.stringify(previousState[key])
) {
result.push({
[key]: {
current: currentState[key],
previous: previousState[key]
}
});
}
}
// check for array of objects
if (typeof value === 'object') {
const ans = compare(
value,
previousState[key][index]
);
result.push(ans);
}
});
}
}
}
return result;
};

You first need a object:
const [object, setObject] = useState({
number: 0,
text: "foo"
});
You need to check when the object changed with useEffect, but you also need to see the previos object, for that we will be using a helper function.
const prevObject = usePrevious(object);
const [result, setResult] = useState("");
useEffect(() => {
if (prevObject) {
if (object.number != prevObject.number) {
setResult("number changed");
}
if (object.text != prevObject.text) {
setResult("text changed");
}
}
}, [object]);
//Helper function to get previos
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
Here is the Codesandbox

Related

Recursively convert an object fields from camelCase to UPPERCASE

I have tried to recursively convert an object fields from camelCase to UPPERCASE.
For some reason it wont work, I have tried many different ways from here in stackoverflow.
Would apprecaite for all the help, thanks.
const o = {
KeyFirst: "firstVal",
KeySecond: [
{
KeyThird: "thirdVal",
},
],
KeyFourth: {
KeyFifth: [
{
KeySixth: "sixthVal",
},
],
},
};
function renameKeys(obj) {
return Object.keys(obj).reduce((acc, key) => {
const value = obj[key];
const modifiedKey = `${key[0].toLowerCase()}${key.slice(1)}`;
if (Array.isArray(value)) {
return {
...acc,
...{ [modifiedKey]: value.map(renameKeys) },
};
} else if (typeof value === "object") {
return renameKeys(value);
} else {
return {
...acc,
...{ [modifiedKey]: value },
};
}
}, {});
}
console.log(renameKeys(o));
You can recursively loop over the object and transform the keys to be uppercase.
const o = {
KeyFirst: { KeySecond: "secondVal" },
KeyThird: [{ KeyFourth: "fourthVal" }],
KeyFifth: {
KeySixth: [{ KeySeventh: "seventhVal" }],
},
};
function renameKeys(obj) {
if (Array.isArray(obj)) {
return obj.map((o) => renameKeys(o));
} else if (typeof obj === "object" && obj !== null) {
return Object.entries(obj).reduce(
(r, [k, v]) => ({ ...r, [k.toUpperCase()]: renameKeys(v) }),
{}
);
} else {
return obj;
}
}
console.log(renameKeys(o));

Getting last appearance of a deeply nested value by specified property

I've got this object as example:
const obj = {
group: {
data: {
data: [
{
id: null,
value: 'someValue',
data: 'someData'
}
]
}
}
};
My goal is to get a value by propery name.
In this case I would like to get
the value of the data propery, but the last appearance of it.
Meaning the expected output is:
someData
However, I'm using this recursive function to retreive it:
const findVal = (obj, propertyToExtract) => {
if (obj && obj[propertyToExtract]) return obj[propertyToExtract];
for (let key in obj) {
if (typeof obj[key] === 'object') {
const value = findVal(obj[key], propertyToExtract);
if (value) return value;
}
}
return false;
};
Which gives me the first appearance of data, meaning:
data: [
{
value: 'someValue',
data: 'someData'
}
]
How can I get the wanted result?
one way can be to recursively flat the object and just get the wanted index (here data)
const obj = {
group: {
data: {
data: [
{
id: null,
value: 'someValue',
data: 'someData'
}
]
}
}
};
function deepFlat(obj) {
let flatObj = {};
flat(obj,flatObj);
console.log(flatObj);
return flatObj;
}
function flat(toFlat, flatObj) {
Object.entries(toFlat).forEach(elem => {
if (elem[1] && typeof elem[1] === 'object') {
flat(elem[1], flatObj);
} else {
flatObj[elem[0]] = elem[1];
}
});
}
let result = deepFlat(obj);
console.log(result['data']);

Double for loop without mutating prop, VUE3

I have a 'data' props which say looks like this:
data = [
{
"label":"gender",
"options":[
{"text":"m","value":0},
{"text":"f","value":1},
{"text":"x", "value":null}
]
},
{
"label":"age",
"options":[
{"text":"<30", "value":0},
{"text":"<50","value":1},
{"text":">50","value":3}
]
}
]
In a computed property I want to have a new array which looks exactly like the data prop, with the difference that - for the sake of example let's say - I want to multiply the value in the options array by 2. In plain js I did this before, like this:
data.forEach(item => {
item.options.forEach(option => {
if (option.value !== null && option.value !== 0) {
option.value *= 2;
}
})
});
Now I'm trying to do this in a computed property, with .map(), so it doesn't mutate my data props, but I cant figure out how.
computed: {
doubledValues() {
var array = this.data.map((item) => {
//...
item.options.map((option) => {
//... option.value * 2;
});
});
return array;
}
}
you can use map() method, like so:
computed: {
doubledValues() {
return this.data.map(item => ({...item, options: item.options.map(obj => {
return (obj.value != null) ? { ...obj, value: obj.value * 2 } : { ...obj }
})})
);
}
}
Just copy objects/arrays. It will be something like that
computed: {
doubledValues() {
return this.data.map((item) => {
const resultItem = {...item};
resultItem.options = item.options.map((option) => {
const copyOption = {...option};
if (copyOption.value !== null && copyOption.value !== 0) {
copyOption.value *= 2;
}
return copyOption;
});
return resultItem;
});
}
}

How to validate deeply nested object structure

I have defined object with nested properties. I want to create a validator function which will check if another object has the same structure and value type as the one that I have defined!
The is the definition of the object:
const OBJECT_SCHEMA = {
name: String,
data: [{
isSelected: Boolean,
mId: String,
mSummary: String,
mMarkets: Array,
mBdd: String,
mReplaceDict: Object,
omId: String,
omnSummary: String,
omnMarkets: Array,
omnBdd: String,
omnReplaceDict: {
id: String,
text: String,
},
}],
metadata: {
emails: Array,
description: String,
},
};
And here is the function that I have for validation. Currently it works only with one nested level! I want it to validate with many nested levels.
function validateObjectStructure(schema, obj) {
let valid = true;
firstLevel: for(const k in schema) {
if(schema[k].constructor === Array) { // if prop is of type array
let i;
for(i = 0; i < schema[k].length; i++) {
for(const kk in schema[k][i]) {
if(!obj[k][i].hasOwnProperty(kk) || obj[k][i][kk].constructor !== schema[k][i][kk]) {
valid = false;
break firstLevel;
}
}
}
}
else if(schema[k].constructor === Object) { // if prop is of type object
for(const kk in schema[k]) {
if(!obj[k].hasOwnProperty(kk) || obj[k][kk].constructor !== schema[k][kk]) {
valid = false;
break firstLevel;
}
}
}
else { // if prop is simple type
if(!obj.hasOwnProperty(k) || obj[k].constructor !== schema[k]) {
valid = false;
break;
}
}
}
return valid;
}
Do you need to work with nested levels of the obj? If yes, you can do something like this instead of the last line:
Object.values(obj).reduce((accValid, value) => {
if (typeof value === 'object') {
return accValid && validateObjectStructure(schema, value);
}
return accValid;
}, valid);
return valid;
Here's a possible implementation:
function validate(obj, schema, path = '') {
let ok = true;
if (!obj)
ok = obj === schema;
else if (typeof schema === 'function')
ok = obj.constructor === schema;
else if (typeof obj !== 'object')
ok = obj === schema;
else if (Array.isArray(schema))
ok = Array.isArray(obj) && obj.every((x, k) => validate(x, schema[0], path + '[' + k + ']'));
else {
let ko = Object.keys(obj);
let ks = Object.keys(schema);
ok = ko.length === ks.length && ks.every(k => validate(obj[k], schema[k], path + '.' + k));
}
if (!ok)
throw new Error('FAILED ' + path);
return true;
}
// example:
const OBJECT_SCHEMA = {
name: String,
data: [{
isSelected: Boolean,
mId: String,
omnReplaceDict: {
id: String,
text: {
deepObj: {
deepProp: [Number]
}
},
},
}],
};
const obj = {
name: "foo",
data: [{
isSelected: true,
mId: "bar",
omnReplaceDict: {
id: "foo",
text: {
deepObj: {
deepProp: [1, 2, "???", 3]
}
},
},
}]
};
validate(obj, OBJECT_SCHEMA)
Note: although this home-made type checker appears to work correctly, it's quite limited (e.g. how to express "array of string-number pairs" or "either null or some object"?), so it might be an option to employ a real one, like Typescript. See here for a possible implementation.

Recursively find keys on an object

I have a javascript object structured like this;
brand: {
group: {
subGroup: {
items: []
},
otherSub: {
items: []
}
}
}
Given an array of keys ['brand', 'group', 'newGroup', 'newSubGroup'] I want to split the keys into found and missing keys. So for the structure above I should get back;
present = ['brand', 'group']
missing = ['newGroup', 'newSubGroup']
I'm using ES6 and have lodash available, but struggling to find a clean way to produce this.
This is not to just check existence, it's recursively find the keys and return those present and the remaining ones.
Here's a pretty sketchy way that works.
const find = (keys, obj) => {
const string = JSON.stringify(obj);
return keys.reduce(({ present, missing }, key) => {
const match = string.match(new RegExp(`"${key}":`));
if (match) {
present.push(key);
} else {
missing.push(key);
}
return { present, missing };
}, { present: [], missing: [] });
}
You can use this function made for you ;)
var getAttrs = function(obj) {
return [].concat.apply([], Object.keys(obj).map(function (key) {
var results = [key]
if (typeof obj[key] === 'object') {
Array.prototype.push.apply(results, getAttrs(obj[key]))
}
return results
}))
}
It return the list of properties and children properties.
getAttrs({brand: {
group: {
subGroup: {
items: []
},
otherSub: {
items: []
}
}
}})
> ["brand", "group", "subGroup", "items", "otherSub", "items"]
And you can use it like so:
var lookingFor = ['brand', 'group', 'newGroup', 'newSubGroup']
var existings = getAttrs(obj)
var missings = []
var presents = []
lookingFor.forEach(attr => {
if (existings.indexOf(attr) === -1) {
missings.push(attr)
} else {
presents.push(attr)
}
})
I wrote a function to recursively get unique keys from a nested object, then filtered the array of all the keys you mentioned checking which were present in the result of my function.
var thisObject = {
brand: {
group: {
subGroup: {
items: []
},
otherSub: {
items: []
}
}
}
};
var arr_full = ['brand', 'group', 'newGroup', 'newSubGroup'] ;
var key_array = [];
function addToKeyArray( key_array, object ){
for( var key in object ){
// only get unique keys
if( key_array.indexOf( key ) === -1 ){
key_array.push( key );
}
// concat the result of calling this function recurrsively on object[key]
key_array.concat( addToKeyArray( key_array, object[key] ) );
}
return key_array;
}
var test = addToKeyArray( [], thisObject );
var missing = arr_full.filter( function( el ) {
return test.indexOf( el ) < 0;
});
console.log( test );
console.log( missing )
You can create recursive function using for...in loop inside another function and return object as result..
var obj = {"brand":{"group":{"subGroup":{"items":[]},"otherSub":{"items":[]}}}}
var keys = ['brand', 'group', 'newGroup', 'newSubGroup'] ;
function findKeys(data, keys) {
keys = keys.slice();
function findPresent(data, keys) {
var result = []
for(var i in data) {
if(typeof data[i] == 'object') result.push(...findPresent(data[i], keys))
var index = keys.indexOf(i);
if(index != -1) result.push(...keys.splice(index, 1))
}
return result
}
return {present: findPresent(data, keys), missing: keys}
}
console.log(findKeys(obj, keys))
To keep things clean and readable you can use "for in", inside a nested function for your recursion.
function recur(obj) {
let preMiss = {
present: [],
missing: []
}
let root = traverse => {
for (let key in traverse) {
if (Array.isArray(traverse[key].items)) {
preMiss.missing.push(key);
}
if (typeof traverse[key] === 'object' && !Array.isArray(traverse[key].items)) {
preMiss.present.push(key);
root(traverse[key])
}
}
}
root(obj);
return preMiss;
}
const object = {
brand: {
group: {
subGroup: {
items: []
},
otherSub: {
items: []
}
}
}
}
console.log(Object.entries(recur(object)));
var toFind = ['brand', 'group', 'newGroup', 'newSubGroup'],
found = [];
var o = {
brand: {
group: {
subGroup: {
items: []
},
otherSub: {
items: []
}
}
}
}
//called with every property and its value
function process(key,value) {
var i = toFind.indexOf(key);
if(i !== -1){
found.push(key);
toFind.splice(i, 1);
}
}
function traverse(o,func) {
if(!toFind.length) return;
for (var i in o) {
func.apply(this,[i,o[i]]);
if (o[i] !== null && typeof(o[i])=="object") {
//going one step down in the object tree!!
traverse(o[i],func);
}
}
}
traverse(o,process);
console.log(found); // present
console.log(toFind); // absent
Traverse method taken from https://stackoverflow.com/a/722732/1335165
Even though this question is a bit older, I want to present a rather short solution to the problem.
const recursivelyGetKeys = obj => Object.keys(obj).map(key => typeof obj[key] === 'object'
? [...recursivelyGetKeys(obj[key]), key] : [key]).reduce((p, c) => [...p, ...c], [])
This function will return all keys in the object, so a call to the array arr with
const arr = {
brand: {
group: {
subGroup: {
items: []
},
otherSub: {
items: []
}
}
}
}
will output:
const keys = recursivelyGetKeys(arr) // = ["items", "subGroup", "items", "otherSub", "group", "brand"]
Now to find the intersection set of this and find = ['brand', 'group', 'newGroup', 'newSubGroup'], do:
const found = keys.filter(key => find.some(val === key))
const missing = keys.filter(key => find.every(val !== key))

Categories