Checking for Differences Between Docs in Array Embedded Within Another Array - javascript

I am trying to compare two documents (actually the current and previous version of the same record), to determine if the contents of an array on the root of the documents is different. But what I'm specifically looking for are changes on an embedded array within that root array.
My document structure looks something like this:
{
_id: 1,
name: {
first: "John",
last: "Smith"
},
services: [
{
service: "serviceOne",
// I want to detect differences at this "history" array level
history: [
{
_id: <ObjectId>,
title: value
},
{
_id: <ObjectId>,
title: value
}
]
},
{
service: "serviceTwo",
history: [
{
_id: <ObjectId>,
title: value
},
{
_id: <ObjectId>,
title: value
}
]
},
]
}
How can I check to see if there are differences between the two documents at the level of the "history" array?

Compare the _id and title of all of them:
const compare = cb => (a, b) => a.length === b.length && a.every((el, i) => cb(el, b[i]));
const take = (key, cb) => (a, b) => cb(a[key], b[key]);
const equal = take("services", compare(
take("history", compare(
(a, b) => a._id === b._id && a.title === b.title
))
))(obj, obj2);
Or if the formatting (order of keys) doesn't change between two versions, it can be simplified to:
JSON.stringify(obj) === JSON.stringify(obj2)

You could use this:
function compareObjects(obj1,obj2,reversed){
for(var key in obj1){
if(typeof obj2[key]=="undefined") return false
if(typeof obj1[key] == "object"){
if(!compareObjects(obj1[key],obj2[key])) return false
}
else if(obj1[key]!=obj2[key]) return false
}
return reversed ? true : compareObjects(obj2,obj1,true)
}
via
var new_history = new_document.services.map(service=>service.history)
var old_history = old_document.services.map(service=>service.history)
var are_identical = compareObjects(new_history,old_history)
It will look recursively for differences, regardless of the structure.

Related

Process JavaScript JSON file structure [duplicate]

I have the following object:
const modules = {
celebrity: {
actor: {
male: 'male',
female: 'female'
},
director: 'director'
},
movie: 'movie',
user: 'user'
};
In result I want an array of string as the following:
[
"celebrity/actor/male",
"celebrity/actor/female",
"celebrity/director",
"movie",
"user"
]
I create the following function:
function getPathsList(node, path = '') {
let pathsList = [];
const childs = Object.entries(node);
for (const child of childs) {
if (typeof child[1] === 'string') {
path += `/${child[1]}`
pathsList.push(path)
} else {
path += `/${child[0]}`
pathsList = [...pathsList, ...getPathsList(child[1], path, pathsList)]
}
}
return pathsList;
}
But I got:
[
"/celebrity/actor/male",
"/celebrity/actor/male/female",
"/celebrity/actor/director",
"/celebrity/movie",
"/celebrity/movie/user"
]
I know that the path variable should be initialized somewhere, but I can't figure it out.
You could use an appraoch which works without a path, but assembles the path by iterating the nested part object.
function getPathsList(node) {
const pathsList = [];
for (const [key, value] of Object.entries(node)) {
if (value && typeof value === 'object') {
pathsList.push(...getPathsList(value).map(p => `${key}/${p}`))
} else {
pathsList.push(key);
}
}
return pathsList;
}
const modules = {
celebrity: {
actor: {
male: 'male',
female: 'female'
},
director: 'director'
},
movie: 'movie',
user: 'user'
};
console.log(getPathsList(modules));
Another way, using reduce:
const modules = {celebrity:{actor:{male:"male",female:"female"},director:"director"},movie:"movie",user:"user"};
function getPathsList(node, path = '') {
return Object.entries(node)
.reduce(
(res, [k, v]) => res.concat(
typeof v === "string" ? `${path}${v}` : getPathsList(v, `${path}${k}/`
)
), []);
}
console.log(getPathsList(modules));
You may consider a "dfs" like algorithm where you explore every path from root to leaf.
You then join your path with '/'.
Subtlety: don't put the leaf itself into the path (e.g: otherwise you would get movie/movie)
Below an example using flatMap
const modules = {"celebrity":{"actor":{"male":"male","female":"female"},"director":"director"},"movie":"movie","user":"user"}
const flatten = x => {
if (typeof(x) === 'string') { return [[]] }
// each node has for children its entries
// each node returns an array of path
return Object.entries(x).flatMap(([k, v]) => {
return flatten(v).map(path => [k , ...path])
})
}
console.log(flatten(modules).map(path => path.join('/')))
Where is the difficulty ?
const
modules =
{ celebrity:
{ actor: { male: 'male', female: 'female' }
, director: 'director'
}
, movie: 'movie'
, user: 'user'
}
, pathsList = []
;
function getPathsList( obj, path='' )
{
for (let key in obj )
{
if (typeof(obj[key]) === 'object') getPathsList( obj[key], path+'/'+key )
else pathsList.push( (path+'/'+key).substring(1) )
}
}
getPathsList( modules )
console.log( pathsList )
A very simple recursive solution using Object .entries returns an array containing only an empty string for non-objects, and otherwise, for every key-value, combines the key with the results of a recursive call to the value. The only slightly tricky part is to not insert the slash (/) before an empty string. It looks like this:
const getPathsList = (obj) => Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, v]) => getPathsList (v) .map (p => p ? k + '/' + p : k)
)
: ['']
const modules = {celebrity: {actor: {male: 'male', female: 'female'}, director: 'director'}, movie: 'movie', user: 'user'}
console .log (getPathsList (modules))
But I would prefer to do this a slightly different way, building our function atop one that gathers the result into arrays of values (e.g. [['celebrity', 'actor', 'male'], ['celebrity', 'actor', 'female'], ... ['user']]), then simply joining those new arrays together with slashes. It's quite similar:
const getPaths = (obj) => Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, v]) => getPaths (v) .map (p => [k, ...p])
)
: [[]]
const getPathsList = (obj) =>
getPaths (obj) .map (xs => xs .join ('/'))
const modules = {celebrity: {actor: {male: 'male', female: 'female'}, director: 'director'}, movie: 'movie', user: 'user'}
console .log (getPathsList (modules))
I find that intermediate array format much more helpful.
This is a slightly less sophisticated version of getPaths than I generally write. Usually I distinguish between numeric array indices and string object keys, but that's not relevant here, since we're folding them back into strings, so this version is simplified.

new Array Conditional values

I need to create a new array as you can see it has a key value that specifies the latitude and longitude.
I want the key values ​​that are equal to be set as 1 only but according to who has the highest count
[
{
"color":"green",
"coment_calification":"Califica",
"count":7,
"key":"-13.0711552&-76.3723776&Califica",
"latitud":"-13.0711552",
"longitud":"-76.3723776"
},
{
"color":"yellow",
"coment_calification":"Reporte",
"count":6,
"key":"-13.0711552&-76.3723776&Reporte",
"latitud":"-13.0711552",
"longitud":"-76.3723776"
},
{
"color":"green",
"coment_calification":"Califica",
"count":1,
"key":"-13.1711552&-76.3423776&Califica",
"latitud":"-13.1711552",
"longitud":"-76.3423776"
},
{
"color":"yellow",
"coment_calification":"Reporte",
"count":2,
"key":"-13.1711552&-76.3423776&Reporte",
"latitud":"-13.1711552",
"longitud":"-76.3423776"
}
]
let result = count.filter((e) => e && e.count && e.key == e.key);
let datas = result;
Is this what you're looking for?
const result = arr.reduce((acc, cur) => {
const matchIndex = acc.findIndex(saved =>
saved.latitud === cur.latitud
&& saved.longitud === cur.longitud
);
if (matchIndex !== -1) {
if (cur.count > acc[matchIndex].count) {
acc.splice(matchIndex, 1, cur);
}
return acc;
}
return [...acc, cur];
}, []);
It looks like the array contains objects that differ mostly by their count props but have several other duplicate props. I think I understand the OP to want to remove these almost-duplicates retaining the one with the highest count.
One way to do this is to sort the array to be descending by count, walk through them, pushing onto a result only those that aren't already in the result.
The only thing not super clear in the OP is what constitutes sameness besides the count field. Here, we guess that the coment_calification key being the same means the objects are to be treated as duplicate.
const objects = [
{
"color":"green",
"coment_calification":"Califica",
"count":7,
"key":"-13.0711552&-76.3723776&Califica",
"latitud":"-13.0711552",
"longitud":"-76.3723776"
},
{
"color":"yellow",
"coment_calification":"Reporte",
"count":6,
"key":"-13.0711552&-76.3723776&Reporte",
"latitud":"-13.0711552",
"longitud":"-76.3723776"
},
{
"color":"green",
"coment_calification":"Califica",
"count":1,
"key":"-13.1711552&-76.3423776&Califica",
"latitud":"-13.1711552",
"longitud":"-76.3423776"
},
{
"color":"yellow",
"coment_calification":"Reporte",
"count":2,
"key":"-13.1711552&-76.3423776&Reporte",
"latitud":"-13.1711552",
"longitud":"-76.3423776"
}
];
objects.sort((a,b) => b.count-a.count) // sort descending
const result = [];
for (let object of objects) {
// supposing matching coment_calification props means a match
let alreadyInResult = result.find(r => r.coment_calification === object.coment_calification)
if (!alreadyInResult) result.push(object);
}
console.log(result)

How to re-order an array of objects based on several conditions?

I'm trying to use the Array.prototype.sort() method to reorder an array or objects.
My array of objects may look something like (I have removed irrelevant properties from the real life scenario):
[
{
context: [
{ value: 'hover' }
]
},
{
context: []
},
{
context: [
{ value: 'active' }
]
},
{
context: [
{ value: 'large' }
]
}
]
I need to re-order the objects based on the values contained within the nested objects of the context property.
The conditions are:
If there are no context objects, move to beginning of array
If context object contains a hover value, move to the end of the array
So the above array of 4 objects should be re-ordered from [1, 2, 3, 4] to [2, 3, 4, 1]
I can satisfy the first condition with something like:
rules.sort((a, b) => {
return (a.context.length - b.context.length);
});
...which re-orders the array based on the number of context objects (thus satisfying the first condition), but I can't figure out how to get the second condition satisfied.
I'm able to determine if either a or b arrays contain a context object with a hover value, which feels like a good start, but I'm not sure where to go from here...
rules.sort((a, b) => {
const AIsHover = a.context.some(c => c.value === 'hover');
const BIsHover = b.context.some(c => c.value === 'hover');
return (a.context.length - b.context.length);
});
Any help would be much appreciated! Thanks
You can achieve that simply by distinguishing the cases:
rules.sort((a, b) => {
if (!a.context.length && !b.context.length) {
return 0;
}
if (a.context.length && !b.context.length) {
return 1;
}
if (!a.context.length && b.context.length) {
return -1;
}
if (a.context.some(c => c.value === 'hover') && b.context.some(c => c.value === 'hover')) {
return 0;
}
if (a.context.some(c => c.value === 'hover')) {
return 1;
}
if (b.context.some(c => c.value === 'hover')) {
return -1;
}
return 0;
});

find and modify deeply nested object in javascript array

I have an array of objects that can be of any length and any depth. I need to be able to find an object by its id and then modify that object within the array. Is there an efficient way to do this with either lodash or pure js?
I thought I could create an array of indexes that led to the object but constructing the expression to access the object with these indexes seems overly complex / unnecessary
edit1; thanks for all yours replies I will try and be more specific. i am currently finding the location of the object I am trying to modify like so. parents is an array of ids for each parent the target object has. ancestors might be a better name for this array. costCenters is the array of objects that contains the object I want to modify. this function recurses and returns an array of indexes that lead to the object I want to modify
var findAncestorsIdxs = function(parents, costCenters, startingIdx, parentsIdxs) {
var idx = startingIdx ? startingIdx : 0;
var pidx = parentsIdxs ? parentsIdxs : [];
_.each(costCenters, function(cc, ccIdx) {
if(cc.id === parents[idx]) {
console.log(pidx);
idx = idx + 1;
pidx.push(ccIdx);
console.log(pidx);
pidx = findAncestorsIdx(parents, costCenters[ccIdx].children, idx, pidx);
}
});
return pidx;
};
Now with this array of indexes how do I target and modify the exact object I want? I have tried this where ancestors is the array of indexes, costCenters is the array with the object to be modified and parent is the new value to be assigned to the target object
var setParentThroughAncestors = function(ancestors, costCenters, parent) {
var ccs = costCenters;
var depth = ancestors.length;
var ancestor = costCenters[ancestors[0]];
for(i = 1; i < depth; i++) {
ancestor = ancestor.children[ancestors[i]];
}
ancestor = parent;
console.log(ccs);
return ccs;
};
this is obviously just returning the unmodified costCenters array so the only other way I can see to target that object is to construct the expression like myObjects[idx1].children[2].grandchildren[3].ggranchildren[4].something = newValue. is that the only way? if so what is the best way to do that?
You can use JSON.stringify for this. It provides a callback for each visited key/value pair (at any depth), with the ability to skip or replace.
The function below returns a function which searches for objects with the specified ID and invokes the specified transform callback on them:
function scan(id, transform) {
return function(obj) {
return JSON.parse(JSON.stringify(obj, function(key, value) {
if (typeof value === 'object' && value !== null && value.id === id) {
return transform(value);
} else {
return value;
}
}));
}
If as the problem is stated, you have an array of objects, and a parallel array of ids in each object whose containing objects are to be modified, and an array of transformation functions, then it's just a matter of wrapping the above as
for (i = 0; i < objects.length; i++) {
scan(ids[i], transforms[i])(objects[i]);
}
Due to restrictions on JSON.stringify, this approach will fail if there are circular references in the object, and omit functions, regexps, and symbol-keyed properties if you care.
See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_native_JSON#The_replacer_parameter for more info.
As Felix Kling said, you can iterate recursively over all objects.
// Overly-complex array
var myArray = {
keyOne: {},
keyTwo: {
myId: {a: '3'}
}
};
var searchId = 'myId', // Your search key
foundValue, // Populated with the searched object
found = false; // Internal flag for iterate()
// Recursive function searching through array
function iterate(haystack) {
if (typeof haystack !== 'object' || haystack === null) return; // type-safety
if (typeof haystack[searchId] !== 'undefined') {
found = true;
foundValue = haystack[searchId];
return;
} else {
for (var i in haystack) {
// avoid circular reference infinite loop & skip inherited properties
if (haystack===haystack[i] || !haystack.hasOwnProperty(i)) continue;
iterate(haystack[i]);
if (found === true) return;
}
}
}
// USAGE / RESULT
iterate(myArray);
console.log(foundValue); // {a: '3'}
foundValue.b = 4; // Updating foundValue also updates myArray
console.log(myArray.keyTwo.myId); // {a: '3', b: 4}
All JS object assignations are passed as reference in JS. See this for a complete tutorial on objects :)
Edit: Thanks #torazaburo for suggestions for a better code.
If each object has property with the same name that stores other nested objects, you can use: https://github.com/dominik791/obj-traverse
findAndModifyFirst() method should solve your problem. The first parameter is a root object, not array, so you should create it at first:
var rootObj = {
name: 'rootObject',
children: [
{
'name': 'child1',
children: [ ... ]
},
{
'name': 'child2',
children: [ ... ]
}
]
};
Then use findAndModifyFirst() method:
findAndModifyFirst(rootObj, 'children', { id: 1 }, replacementObject)
replacementObject is whatever object that should replace the object that has id equal to 1.
You can try it using demo app:
https://dominik791.github.io/obj-traverse-demo/
Here's an example that extensively uses lodash. It enables you to transform a deeply nested value based on its key or its value.
const _ = require("lodash")
const flattenKeys = (obj, path = []) => (!_.isObject(obj) ? { [path.join('.')]: obj } : _.reduce(obj, (cum, next, key) => _.merge(cum, flattenKeys(next, [...path, key])), {}));
const registrations = [{
key: "123",
responses:
{
category: 'first',
},
}]
function jsonTransform (json, conditionFn, modifyFn) {
// transform { responses: { category: 'first' } } to { 'responses.category': 'first' }
const flattenedKeys = Object.keys(flattenKeys(json));
// Easily iterate over the flat json
for(let i = 0; i < flattenedKeys.length; i++) {
const key = flattenedKeys[i];
const value = _.get(json, key)
// Did the condition match the one we passed?
if(conditionFn(key, value)) {
// Replace the value to the new one
_.set(json, key, modifyFn(key, value))
}
}
return json
}
// Let's transform all 'first' values to 'FIRST'
const modifiedCategory = jsonTransform(registrations, (key, value) => value === "first", (key, value) => value = value.toUpperCase())
console.log('modifiedCategory --', modifiedCategory)
// Outputs: modifiedCategory -- [ { key: '123', responses: { category: 'FIRST' } } ]
I needed to modify deeply nested objects too, and found no acceptable tool for that purpose. Then I've made this and pushed it to npm.
https://www.npmjs.com/package/find-and
This small [TypeScript-friendly] lib can help with modifying nested objects in a lodash manner. E.g.,
var findAnd = require("find-and");
const data = {
name: 'One',
description: 'Description',
children: [
{
id: 1,
name: 'Two',
},
{
id: 2,
name: 'Three',
},
],
};
findAnd.changeProps(data, { id: 2 }, { name: 'Foo' });
outputs
{
name: 'One',
description: 'Description',
children: [
{
id: 1,
name: 'Two',
},
{
id: 2,
name: 'Foo',
},
],
}
https://runkit.com/embed/bn2hpyfex60e
Hope this could help someone else.
I wrote this code recently to do exactly this, as my backend is rails and wants keys like:
first_name
and my front end is react, so keys are like:
firstName
And these keys are almost always deeply nested:
user: {
firstName: "Bob",
lastName: "Smith",
email: "bob#email.com"
}
Becomes:
user: {
first_name: "Bob",
last_name: "Smith",
email: "bob#email.com"
}
Here is the code
function snakeCase(camelCase) {
return camelCase.replace(/([A-Z])/g, "_$1").toLowerCase()
}
export function snakeCasedObj(obj) {
return Object.keys(obj).reduce(
(acc, key) => ({
...acc,
[snakeCase(key)]: typeof obj[key] === "object" ? snakeCasedObj(obj[key]) : obj[key],
}), {},
);
}
Feel free to change the transform to whatever makes sense for you!

JavaScript recursive search in JSON object

I am trying to return a specific node in a JSON object structure which looks like this
{
"id":"0",
"children":[
{
"id":"1",
"children":[...]
},
{
"id":"2",
"children":[...]
}
]
}
So it's a tree-like child-parent relation. Every node has a unique ID.
I'm trying to find a specific node like this
function findNode(id, currentNode) {
if (id == currentNode.id) {
return currentNode;
} else {
currentNode.children.forEach(function (currentChild) {
findNode(id, currentChild);
});
}
}
I execute the search for example by findNode("10", rootNode). But even though the search finds a match the function always returns undefined. I have a bad feeling that the recursive function doesn't stop after finding the match and continues running an finally returns undefined because in the latter recursive executions it doesn't reach a return point, but I'm not sure how to fix this.
Please help!
When searching recursively, you have to pass the result back by returning it. You're not returning the result of findNode(id, currentChild), though.
function findNode(id, currentNode) {
var i,
currentChild,
result;
if (id == currentNode.id) {
return currentNode;
} else {
// Use a for loop instead of forEach to avoid nested functions
// Otherwise "return" will not work properly
for (i = 0; i < currentNode.children.length; i += 1) {
currentChild = currentNode.children[i];
// Search in the current child
result = findNode(id, currentChild);
// Return the result if the node has been found
if (result !== false) {
return result;
}
}
// The node has not been found and we have no more options
return false;
}
}
function findNode(id, currentNode) {
if (id == currentNode.id) {
return currentNode;
} else {
var result;
currentNode.children.forEach(function(node){
if(node.id == id){
result = node;
return;
}
});
return (result ? result : "No Node Found");
}
}
console.log(findNode("10", node));
This method will return the node if it present in the node list. But this will loop through all the child of a node since we can't successfully break the forEach flow. A better implementation would look like below.
function findNode(id, currentNode) {
if (id == currentNode.id) {
return currentNode;
} else {
for(var index in currentNode.children){
var node = currentNode.children[index];
if(node.id == id)
return node;
findNode(id, node);
}
return "No Node Present";
}
}
console.log(findNode("1", node));
I use the following
var searchObject = function (object, matchCallback, currentPath, result, searched) {
currentPath = currentPath || '';
result = result || [];
searched = searched || [];
if (searched.indexOf(object) !== -1 && object === Object(object)) {
return;
}
searched.push(object);
if (matchCallback(object)) {
result.push({path: currentPath, value: object});
}
try {
if (object === Object(object)) {
for (var property in object) {
if (property.indexOf("$") !== 0) {
//if (Object.prototype.hasOwnProperty.call(object, property)) {
searchObject(object[property], matchCallback, currentPath + "." + property, result, searched);
//}
}
}
}
}
catch (e) {
console.log(object);
throw e;
}
return result;
}
Then you can write
searchObject(rootNode, function (value) { return value != null && value != undefined && value.id == '10'; });
Now this works on circular references and you can match on any field or combination of fields you like by changing the matchCallback function.
Since this old question has been brought back up, here's a different approach. We can write a fairly generic searchTree function which we then use in a findId function. searchTree does the work of traversing the object; it accepts a callback as well as the tree; the callback determines if a node matches. As well as the node, the callback is supplied two functions, next and found, which we call with no parameters to signal, respectively, that we should proceed or that we've found our match. If no match is found, we return null.
It looks like this:
const searchTree = (fn) => (obj) =>
Array.isArray(obj)
? obj.length == 0
? null
: searchTree (fn) (obj [0]) || searchTree (fn) (obj .slice (1))
: fn (
obj,
() => searchTree (fn) (obj .children || []),
() => obj
)
const findId = (target, obj) => searchTree (
(node, next, found) => node.id == target ? found () : next(),
) (tree)
const tree = {id: 1, name: 'foo', children: [
{id: 2, name: 'bar', children: []},
{id: 3, name: 'baz', children: [
{id: 17, name: 'qux', children: []},
{id: 42, name: 'corge', children: []},
{id: 99, name: 'grault', children: []}
]}
]}
console .log (findId (42, tree))
console .log (findId (57, tree))
This code is specific to the structure where subnodes are found in an array under the property children. While we can make this more generic as necessary, I find this a common structure to support.
There is a good argument that this would be better written with mutual recursion. If we wanted, we could get the same API with this version:
const searchArray = (fn) => ([x, ...xs]) =>
x === undefined
? null
: searchTree (fn) (x) || searchArray (fn) (xs)
const searchTree = (fn) => (obj) =>
fn (
obj,
() => searchArray (fn) (obj .children || []),
(x) => x
)
This works the same way. But I find the code cleaner. Either should do the job, though.
We use object-scan for our data processing needs. It's conceptually very simple, but allows for a lot of cool stuff. Here is how you could solve your question
// const objectScan = require('object-scan');
const findNode = (id, input) => objectScan(['**'], {
abort: true,
rtn: 'value',
filterFn: ({ value }) => value.id === id
})(input);
const data = { id: '0', children: [{ id: '1', children: [ { id: '3', children: [] }, { id: '4', children: [] } ] }, { id: '2', children: [ { id: '5', children: [] }, { id: '6', children: [] } ] }] };
console.log(findNode('6', data));
// => { id: '6', children: [] }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
Similar questions were answered several times, but I just want to add a universal method that includes nested arrays
const cars = [{
id: 1,
name: 'toyota',
subs: [{
id: 43,
name: 'supra'
}, {
id: 44,
name: 'prius'
}]
}, {
id: 2,
name: 'Jeep',
subs: [{
id: 30,
name: 'wranger'
}, {
id: 31,
name: 'sahara'
}]
}]
function searchObjectArray(arr, key, value) {
let result = [];
arr.forEach((obj) => {
if (obj[key] === value) {
result.push(obj);
} else if (obj.subs) {
result = result.concat(searchObjectArray(obj.subs, key, value));
}
});
console.log(result)
return result;
}
searchObjectArray(cars, 'id', '31')
searchObjectArray(cars, 'name', 'Jeep')
I hope this helps someone
I really liked a tree search! A tree is an extremely common data structure for most of today's complex structured tasks. So I just had similar task for lunch too. I even did some deep research, but havent actually found anything new! So what I've got for you today, is "How I implemented that in modern JS syntax":
// helper
find_subid = (id, childArray) => {
for( child of childArray ) {
foundChild = find_id( i, child ); // not sub_id, but do a check (root/full search)!
if( foundChild ) // 200
return foundChild;
}
return null; // 404
}
// actual search method
find_id = (id, parent) => (id == parent.id) : parent : find_subid(id, parent.childArray);
Recursive structure search, modification, keys/values adjustments/replacement.
Usage Example:
const results = []; // to store the search results
mapNodesRecursively(obj, ({ v, key, obj, isCircular }) => {
// do something cool with "v" (or key, or obj)
// return nothing (undefined) to keep the original value
// if we search:
if (key === 'name' && v === 'Roman'){
results.push(obj);
}
// more example flow:
if (isCircular) {
delete obj[key]; // optionally - we decide to remove circular links
} else if (v === 'Russia') {
return 'RU';
} else if (key.toLocaleLowerCase() === 'foo') {
return 'BAR';
} else if (key === 'bad_key') {
delete obj[key];
obj['good_key'] = v;
} else {
return v; // or undefined, same effect
}
});
Tips and hints:
You can use it as a search callback, just return nothing (won't affect anything) and pick values you need to your Array/Set/Map.
Notice that callback is being run on every leaf/value/key (not just objects).
Or you can use the callback to adjust particular values and even change keys. Also it automatically detects circular loops and provides a flag for you to decide how to handle them.
The code
(uses ES6)
Function itself + some example demo data
function mapNodesRecursively(obj, mapCallback, { wereSet } = {}) {
if (!wereSet) {
wereSet = new Set();
}
if (obj && (obj === Object(obj) || Array.isArray(obj))) {
wereSet.add(obj);
for (let key in obj) {
if (!obj.hasOwnProperty(key)){
continue;
}
let v = obj[key];
const isCircular = wereSet.has(v);
const mapped = mapCallback({ v, key, obj, isCircular });
if (typeof (mapped) !== 'undefined') {
obj[key] = mapped;
v = mapped;
}
if (!isCircular) {
mapNodesRecursively(v, mapCallback, { wereSet });
}
}
}
return obj;
}
let obj = {
team: [
{
name: 'Roman',
country: 'Russia',
bad_key: 123,
},
{
name: 'Igor',
country: 'Ukraine',
FOO: 'what?',
},
{
someBool: true,
country: 'Russia',
},
123,
[
1,
{
country: 'Russia',
just: 'a nested thing',
a: [{
bad_key: [{
country: 'Russia',
foo: false,
}],
}],
},
],
],
};
// output the initial data
document.getElementById('jsInput').innerHTML = JSON.stringify(obj, null, 2);
// adding some circular link (to fix with our callback)
obj.team[1].loop = obj;
mapNodesRecursively(obj, ({ v, key, obj, isCircular }) => {
if (isCircular) {
delete obj[key]; // optionally - we decide to remove circular links
} else if (v === 'Russia') {
return 'RU';
} else if (key.toLocaleLowerCase() === 'foo') {
return 'BAR';
} else if (key === 'bad_key') {
delete obj[key];
obj['good_key'] = v;
} else {
return v;
}
});
// output the result - processed object
document.getElementById('jsOutput').innerHTML = JSON.stringify(obj, null, 2);
.col {
display: inline-block;
width: 40%;
}
<div>
<h3>Recursive structure modification, keys/values adjustments/replacement</h3>
<ol>
<li>
Replacing "Russia" values with "RU"
</li>
<li>
Setting the value "BAR" for keys "FOO"
</li>
<li>
Changing the key "bad_key" to "good_key"
</li>
</ol>
<div class="col">
<h4>BEFORE</h4>
<pre id="jsInput"></pre>
</div>
<div class="col">
<h4>AFTER</h4>
<pre id="jsOutput"></pre>
</div>
</div>

Categories