Recursively get segment of tree according to id - javascript

I'm trying to get a tree segment based on an id. The id may be at the root, or anywhere in the children. My goal is to get that entire family tree line, and not other non-related data.
I have the entire data tree, and an id.
The idea is to do this recursively, since the number of children is unknown.
import "./styles.css";
export default function App() {
const folderTree = [
{
id: "1-1",
children: [
{
id: "1-2",
parentId: "1-1",
children: []
}
]
},
{
id: "2-1",
children: [
{
id: "2-2",
parentId: "2-1",
children: [
{
id: "2-4",
parentId: "2-2",
children: []
}
]
},
{
id: "2-3",
parentId: "2-1",
children: []
}
]
}
];
const getRelatedTreeFolders = (folders, selectedFolderId) => {
//** goes top to bottom
const recursiveChildCheck = (folder, id) => {
// THIS trial failed
// let foundNested = false;
// if (folder.id === id) {
// return true;
// }
// function recurse(folder) {
// if (!folder.hasOwnProperty("children") || folder.children.length === 0)
// return;
// for (var i = 0; i < folder.children.length; i++) {
// if (folder.children[i].id === id) {
// foundNested = true;
// break;
// } else {
// if (folder.children[i].children.length > 0) {
// recurse(folder.children[i].children);
// if (foundNested) {
// break;
// }
// }
// }
// }
// }
// recurse(folder);
// return foundNested;
const aChildHasIt =
folder.children.length > 0 && folder.children.some((f) => f.id === id);
if (aChildHasIt) return true;
let nestedChildHasIt = false;
/** The problem seems to be here */
folder.children.forEach((childFolder) => {
// Is using a forEach loop the correct way?
// ideally it seems there is a simple way to do a recursive .some on the dhildren...
childFolder.children.length>0 && recursiveChildCheck(childFolder, id)
});
if (nestedChildHasIt) return true;
folder.children && folder.children.forEach(recursiveChildCheck);
};
const treeSegment = folders.reduce((result = [], folder) => {
if (
folder.id === selectedFolderId ||
recursiveChildCheck(folder, selectedFolderId)
) {
result.push(folder);
}
return result;
}, []);
return treeSegment;
};
const selectedFolderId = "2-1";
const selectedFolderId1 = "2-2";
const selectedFolderId2 = "2-4";
const selectedFolderId3 = "2-3";
const selectedFolderId4 = "3-1";
const selectedFolderId5 = "1-1";
const selectedFolderId6 = "1-2";
console.log("parent");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId));
console.log("child");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId1));
console.log("grandchild"); // this fails
console.log(getRelatedTreeFolders(folderTree, selectedFolderId2));
console.log("sibling");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId3));
console.log("not found");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId4));
console.log("other parent");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId5));
console.log("other child");
console.log(getRelatedTreeFolders(folderTree, selectedFolderId6));
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{/* <h2>{JSON.stringify(result)}</h2> */}
</div>
);
}

Some issues:
recursiveChildCheck is supposed to return a boolean, but in some cases nothing (undefined) is returned, because a return statement is missing in the following expression:
folder.children && folder.children.forEach(recursiveChildCheck);
Moreover, in the expression above, the second operand of the && operator will never be evaluated, because folder.children is an array, and arrays are always truthy, even empty arrays. To give the second operand a chance, the first operand should be folder.children.length > 0
But even with that correction, the second operand will always evaluate to undefined, as that is what .forEach returns by design. You should have a method call there that returns a boolean, like some.
nestedChildHasIt never gets any other value after its initialisation, and so the following return true will never happen:
if (nestedChildHasIt) return true;
You may have intended to set nestedChildHasIt to true in the preceding forEach loop, but it seems like you have an alternative way here to do the same as the other forEach loop you had at the end.
I think the issue you have been struggling with is that you need to both check a boolean condition (does the subtree have the id?) and you need to filter the children to the child for which this is true, creating a new node which has this unique child.
Corrected code:
function getForestSegment(nodes, id) {
function recur(nodes) {
for (const node of nodes) {
if (node.id === id) return [node];
const children = recur(node.children);
if (children.length) return [{ ...node, children}];
}
return [];
}
return recur(nodes);
}
// Example from question:
const forest = [{id: "1-1",children: [{id: "1-2",parentId: "1-1",children: []}]},{id: "2-1",children: [{id: "2-2",parentId: "2-1",children: [{id: "2-4",parentId: "2-2",children: []}]},{id: "2-3",parentId: "2-1",children: []}]}];
for (const id of ["2-1", "2-2", "2-4", "2-3", "3-1", "1-1", "1-2"]) {
console.log(id);
console.log(getForestSegment(forest, id));
}

Related

How to walk through the object/array in javascript by layers

I am converting the nested data object to the arrays, for the UI Library to show the relationship between the data.
Original
// assume that all object key is unique
{
"top":{
"test":{
"hello":"123"
},
"test2":{
"bye":"123"
"other":{
...
...
...
}
}
}
}
Preferred Result
[
{
id:"top",
parent: null,
},
{
id:"test",
parent: "top",
},
{
id:"hello",
parent: "test",
},
{
id:"test2",
parent: "top",
},
]
To do this, I write the code like this:
const test = []
const iterate = (obj, parent = null) => {
Object.keys(obj).forEach(key => {
const id = typeof obj[key] === 'object' ? key : obj[key]
const loopObj = {
id,
parent
}
test.push(loopObj)
if (typeof obj[key] === 'object') {
iterate(obj[key], id)
}
})
}
iterate(data)
console.log(test) // Done!!
It works.
However, I miss one important things, the library need the layers from the original data, to determine the type/ what function to do.
// The key name maybe duplicated in different layer
{
"top":{ // Layer 1
"test":{ // Layer 2
"hello":"123", // Layer 3
"test":"123" // Layer 3
// Maybe many many layers...
}
}
}
[
{
id:"top",
display:"0-top",
parent: null,
layer: 0
},
{
id: "1-top-test", // To prevent duplicated id, `${layer}-${parentDisplay}-${display}`
display:"test",
parent: "0-top",
parentDisplay: "top",
layer: 1
},
{
id: "3-test-test", // To prevent duplicated id,`${layer}-${parentDisplay}-${display}`
display:"test",
parent: "2-top-test",
parentDisplay: "test",
layer: 3
}
]
Editing the display or id format is very simple, just edit the function and add the field, but I don't know how to get the layer easily.
I tried to add the let count = 0 outside and do count++ when iterate function called.But I realized that it hit when the object detected, no by layers.
The original data may be very big,
So I think editing the original data structure or searching the parent id in the test[] every loop may be not a good solution.
Is there any solution to do this?
Just add the current depth as an argument that gets passed down on every recursive call (as well as the parent name).
const input = {
"top":{
"test":{
"hello":"123"
},
"test2":{
"bye":"123",
"other":{
}
}
}
};
const iterate = (obj, result = [], layer = 0, parentId = null, parentDisplay = '') => {
Object.entries(obj).forEach(([key, value]) => {
const id = `${layer}-${key}`;
result.push({
id,
display: key,
parentId,
parentDisplay,
layer,
});
if (typeof value === 'object') {
iterate(value, result, layer + 1, id, key);
}
});
return result;
}
console.log(iterate(input));
That said, your desired approach can still produce duplicate entries, if there exist two objects at the same level, with different grandparent objects, but whose parent objects use the same key, eg:
const input = {
"top1":{
"test":{
"hello":"123"
},
},
"top2": {
"test": {
"hello":"123"
}
}
};
const input = {
"top1":{
"test":{
"hello":"123"
},
},
"top2": {
"test": {
"hello":"123"
}
}
};
const iterate = (obj, result = [], layer = 0, parentId = null, parentDisplay = '') => {
Object.entries(obj).forEach(([key, value]) => {
const id = `${layer}-${key}`;
result.push({
id,
display: key,
parentId,
parentDisplay,
layer,
});
if (typeof value === 'object') {
iterate(value, result, layer + 1, id, key);
}
});
return result;
}
console.log(iterate(input));
If that's a problem, consider passing down the entire accessor string needed to access the property - eg top1.test.hello and top2.test.hello, which is guaranteed to be unique.
const input = {
"top1":{
"test":{
"hello":"123"
},
},
"top2": {
"test": {
"hello":"123"
}
}
};
const iterate = (obj, result = [], parentAccessor = '') => {
Object.entries(obj).forEach(([key, value]) => {
const accessor = `${parentAccessor}${parentAccessor ? '.' : ''}${key}`;
result.push({
id: key,
accessor,
});
if (typeof value === 'object') {
iterate(value, result, accessor);
}
});
return result;
}
console.log(iterate(input));

Where is the mistake in this find method?

The code works with a simple foreach function, but not with a find method.
Here is the forEach code:
getItemById: function (id) {
let found = null;
data.items.forEach(item => {
if (item.id === id) {
found = item;
}
});
return found;
}
Here is the code with a find mehtod:
getItemById: function (id) {
let item = data.items.find(item => {
item.id === id;
});
return item;
}
Why doesn't work the code with the find method?
also here is the array of the objects:
const data = {
items: [
{ id: 0, name: 'Steak Dinner', calories: 1200 },
{ id: 1, name: 'Sausage', calories: 1100 },
{ id: 2, name: 'Eggs', calories: 200 },
],
currentItem: null,
totalCalories: 0,
}
The find expects a predicate function as a callback and return the value that satisfy the condition. If you won't return then undefined will return by default and undefined is considered as a falsy value.
You are not returning anything from the find function. find will not consider a match if predicate function won't return true. There is not a single match that returns true in any case because all values returned by find is undefined
return item.id === id.
const data = {
items: [
{ id: 0, name: "Steak Dinner", calories: 1200 },
{ id: 1, name: "Sausage", calories: 1100 },
{ id: 2, name: "Eggs", calories: 200 },
],
currentItem: null,
totalCalories: 0,
};
const obj = {
getItemById: function (id) {
let item = data.items.find((item) => {
return item.id === id;
});
return item;
},
};
console.log(obj.getItemById(0));
You're not returning the boolean inside find's callback
let item = data.items.find(item => {
return item.id === id;
});
You just forgot the return statement in your find function. So it will run through all items and result nothing currently.
So just return the result of item.id === id and you're fine.
let item = data.items.find(item => {
return item.id === id;
});
getItemById: function (id) {
return data.items.find(item => item.id === id);
}
You don't have to use "{" and ";" in data.items.find() method. Because,the find is a predicate function
If a lambda function contains a single expression, it is returned automatically. (; and {} are not part of an expression )
Otherwise the return statement is a must, to return a value.
find accepts a predicate & such it must return a boolean.
Either do,
let item = data.items.find(item => item.id === id);
Or,
let item = data.items.find(item => {
return item.id === id;
});

How to get all the names inside a nested object?

I'm currently learning JavaScript and my teacher asked me to do an exercise that would return an array with all the names of this object:
{
name: 'grandma',
daughter: {
name: 'mother',
daughter: {
name: 'daughter',
daughter: {
name: 'granddaughter'
}
}
}
}
my question is similar to this one but the solution does not work for me because my object does not contain any arrays. The code I have so far:
function toArray(obj) {
const result = [];
for (const prop in obj) {
const value = obj[prop];
if (typeof value === 'object') {
result.push(toArray(value));
}
else {
result.push(value);
}
}
return result;
}
function nameMatrioska(target) {
return toArray(target);
}
which prints out this : [ 'grandma', [ 'mother', [ 'daughter', [Array] ] ] ]
but what my teacher wants is: ['grandma', 'mother', 'daughter', 'granddaughter']
codepen
Obviously you push an array to an array, where all nested children appears as an array.
To solve this problem, you could iterate the array and push only single items to the result set.
A different method is, to use some built-in techniques, which works with an array, and returns a single array without a nested array.
Some methods:
Array#concat, creates a new array. It works with older Javascript versions as well.
result = result.concat(toArray(value));
Array#push with an array and Function#apply for taking an array as parameter list. It works in situ and with older versions of JS.
Array.prototype.push.apply(result, toArray(value));
[].push.apply(result, toArray(value)); // needs extra empty array
Spread syntax ... for spreading an array as parameters. ES6
result.push(...toArray(value));
Spread syntax is a powerful replacement for apply with a greater use. Please the the examples as well.
Finally an example with spread syntax.
function toArray(obj) {
const result = [];
for (const prop in obj) {
const value = obj[prop];
if (value && typeof value === 'object') { // exclude null
result.push(...toArray(value));
// ^^^ spread the array
}
else {
result.push(value);
}
}
return result;
}
function nameMatrioska(target) {
return toArray(target);
}
var object = { name: 'grandma', daughter: { name: 'mother', daughter: { name: 'daughter', daughter: { name: 'granddaughter' } } } };
console.log(nameMatrioska(object));
You need .concat instead of .push. Push adds one item to an array; concat joins two arrays together.
['grandmother'].concat(['mother', 'daughter'])
-> ['grandmother', 'mother', 'daughter']
Unlike push, which modifies the array you call it on, concat creates a new array.
var a1 = [ 'grandmother' ];
a1.push( 'mother' );
console.log( a1 );
-> ['grandmother', 'mother']
var a2 = [ 'steve' ];
var result = a2.concat(['Jesus', 'Pedro']);
console.log( a1 );
-> ['steve']
console.log( result );
-> ['steve', 'Jesus', 'Pedro']
Try this
function toArray(obj) {
var result = "";
for (const prop in obj) {
const value = obj[prop];
if (typeof value === 'object') {
result = result.concat(" " + toArray(value));
}
else {
result = result.concat(value);
}
}
return result;
}
function nameMatrioska(target) {
return toArray(target).split(" ");
}
function toArray(obj) {
var result = [];
for (var prop in obj) {
var value = obj[prop];
if (typeof value === 'object') {
result = result.concat(toArray(value))
} else {
result.push(value);
}
}
return result;
}
function nameMatrioska(target) {
return toArray(target);
}
//USER
var names = {
name: 'grandma',
daughter: {
name: 'mother',
daughter: {
name: 'daughter',
daughter: {
name: 'granddaughter'
}
}
}
};
console.log(nameMatrioska(names));
//Output: ["grandma", "mother", "daughter", "granddaughter"]
You are really close.
You have to flatten your array in your last step.
Tip: In general be careful when checking for type object because e.g. null, undefined are also objects in JavaScript world!
function isObject(value) {
if(value === undefined) return "Undefined";
if(value === null) return "Null";
const string = Object.prototype.toString.call(value);
return string.slice(8, -1);
}
function collectPropertiesRec(object, propertyName) {
const result = [ ];
for(const currentPropertyName in object) {
const value = object[currentPropertyName];
if(isObject(value) === 'Object') {
result.push(collectPropertiesRec(value, propertyName));
}
else if(currentPropertyName === propertyName) {
result.push(value);
}
}
return result;
}
function flattenDeep(arr1) {
return arr1.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), [ ]);
}
//USER
const names = {
name: 'grandma',
daughter: {
name: 'mother',
daughter: {
name: 'daughter',
daughter: {
name: 'granddaughter'
}
}
}
};
var result = collectPropertiesRec(names, "name");
alert(flattenDeep(result).join(", "));

How do I swap array elements in an immutable fashion within a Redux reducer?

The relevant Redux state consists of an array of objects representing layers.
Example:
let state = [
{ id: 1 }, { id: 2 }, { id: 3 }
]
I have a Redux action called moveLayerIndex:
actions.js
export const moveLayerIndex = (id, destinationIndex) => ({
type: MOVE_LAYER_INDEX,
id,
destinationIndex
})
I would like the reducer to handle the action by swapping the position of the elements in the array.
reducers/layers.js
const layers = (state=[], action) => {
switch(action.type) {
case 'MOVE_LAYER_INDEX':
/* What should I put here to make the below test pass */
default:
return state
}
}
The test verifies that a the Redux reducer swaps an array's elements in immutable fashion.
Deep-freeze is used to check the initial state is not mutated in any way.
How do I make this test pass?
test/reducers/index.js
import { expect } from 'chai'
import deepFreeze from'deep-freeze'
const id=1
const destinationIndex=1
it('move position of layer', () => {
const action = actions.moveLayerIndex(id, destinationIndex)
const initialState = [
{
id: 1
},
{
id: 2
},
{
id: 3
}
]
const expectedState = [
{
id: 2
},
{
id: 1
},
{
id: 3
}
]
deepFreeze(initialState)
expect(layers(initialState, action)).to.eql(expectedState)
})
One of the key ideas of immutable updates is that while you should never directly modify the original items, it's okay to make a copy and mutate the copy before returning it.
With that in mind, this function should do what you want:
function immutablySwapItems(items, firstIndex, secondIndex) {
// Constant reference - we can still modify the array itself
const results= items.slice();
const firstItem = items[firstIndex];
results[firstIndex] = items[secondIndex];
results[secondIndex] = firstItem;
return results;
}
I wrote a section for the Redux docs called Structuring Reducers - Immutable Update Patterns which gives examples of some related ways to update data.
You could use map function to make a swap:
function immutablySwapItems(items, firstIndex, secondIndex) {
return items.map(function(element, index) {
if (index === firstIndex) return items[secondIndex];
else if (index === secondIndex) return items[firstIndex];
else return element;
}
}
In ES2015 style:
const immutablySwapItems = (items, firstIndex, secondIndex) =>
items.map(
(element, index) =>
index === firstIndex
? items[secondIndex]
: index === secondIndex
? items[firstIndex]
: element
)
There is nothing wrong with the other two answers, but I think there is even a simpler way to do it with ES6.
const state = [{
id: 1
}, {
id: 2
}, {
id: 3
}];
const immutableSwap = (items, firstIndex, secondIndex) => {
const result = [...items];
[result[firstIndex], result[secondIndex]] = [result[secondIndex], result[firstIndex]];
return result;
}
const swapped = immutableSwap(state, 2, 0);
console.log("Swapped:", swapped);
console.log("Original:", state);

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