Recursive iteration over deeply nested object to find parent - javascript

I have a data tree structure with children:
{ id: 1,
name: "Dog",
parent_id: null,
children: [
{
id: 2,
name: "Food",
parent_id: 1,
children: []
},
{
id: 3,
name: "Water",
parent_id: 1,
children: [
{
id: 4,
name: "Bowl",
parent_id: 3,
children: []
},
{
id: 5,
name: "Oxygen",
parent_id: 3,
children: []
},
{
id: 6,
name: "Hydrogen",
parent_id: 3,
children: []
}
]
}
]
}
Any child data object can have more children as shown in the above data. This represents a DOM structure that a user could select an item from to add a child to.
I have a known text title of the selected item from the DOM as well as the data the user wants to insert. I am having trouble finding a recursive algorithm that will allow me to add the new data to the correct level of the tree.
Here is a list of me thinking through the problem and trying to pseudo code it:
inputs:
tree (data from above)
parentTitle from clicked item in DOM
outputs:
tree with item inserted
steps:
determine highest used id to know what next unique id is
check current level of data for match with the title of parent
if matched then set id and parent_id in new data and push into children of parent
if no match then check if current level data have children
if current level has children needs to repeat steps 2+ for each until match is found
Here is my code:
function askUserForNewItem(e) {
const tree = getTree(); // returns above tree data structure
const name = prompt( 'Enter new item’s name:' ); // user input to match and insert as new item in tree
const clickedTitle = getClickedTitle(e); // returns string title of clicked on item from DOM - for example "Dog" or "Bowl"
const parent = determineParent(tree, clickedTitle);
const parent_id = parent[0].id;
// TODO - needs to set real unique id (highest unused id)
const newId = 101; // hard coded for now, needs to be dynamic
// TODO - needs to insert into correct level of children array in tree
return tree.children.push({ id: newId, name, emoji, children: [], parent_id: parent_id });
}
function determineParent(tree, clickedTitle) {
if(tree.children.length === 0) {
return false;
}
let treeLevel = tree;
let parent = [];
while(treeLevel.children.length !== 0) {
parent = treeLevel.children.filter(child => child.name === clickedTitle);
if(parent.length !== 0) {
break;
}
else {
// what to do here to make this recursive?
}
}
return parent;
}
So if a user typed "Cat" while clicking the add button for "Dog" then a new object
{
id: 7,
name: "Cat",
parent_id: 1,
children: []
}
Would be inserted into children of the first level "Dog" object in the data tree.

If you want a recursive solution, you should modify the determineParent method so it searches down the tree.
Not sure this is exactly what you are searching for, but i hope you get the general idea
function determineParent(curNode, clickedTitle) {
if(curNode.name===clickedTitle)
return curNode; // found the parent node with the correct clickedTitle
// not found yet, do a recusive search down the tree.
for(const node of curNode.children) {
return determineParent(node,clickedTitle);
}
return null; // not found.
}
the idea is to start at the topmost node (curNode) and first determine if it is the correct parent, if not then take the first children see if it matches and if not search down it's children and so on.
When dealing with recursion it may be necessary to handle the situation where you may experience circular references, consider a scenario where a node has a child that points to the nodes parent or grandparent, the recursive method will run forever (in real life it will run out of stack space and throw an exception).
One way it so include a safeguard counter that is decreased on each recursive call, and then bail out when it reaches zero.
function determineParent(curNode, clickedTitle, safeGuard) {
if(curNode.name===clickedTitle)
return curNode; // found the parent node with the correct clickedTitle
if(safeGuard===0)
return null; // bail out
// not found yet, do a recusive search down the tree.
for(const node of curNode.children) {
return determineParent(node,clickedTitle,--safeGuard);
}
return null; // not found.
}
and then call it like
this.determineParent(tree,"title",100);
to limit the number of searches to 100.

If it's ok to use Lodash+Deepdash, then:
let child = {
name: "Cat",
children: []
};
let maxUsedId=-1;
_.eachDeep([data],(val)=>{
if(val.id>maxUsedId){
maxUsedId = val.id;
}
if(val.name==parentName){
val.children.push(child);
child.parent_id = val.id;
}
},{tree:true});
child.id=maxUsedId+1;
Codepen for this

Not a big fan of reinventing the wheel, especially when it comes to algorithms. Here is how you could use object-scan to solve your problem. We use it for data processing since it is quite powerful once you wrap your head around it
// const objectScan = require('object-scan');
const insert = (tree, parentName, childName) => objectScan(['**.name'], {
abort: true,
rtn: 'bool',
filterFn: ({ value, parent }) => {
if (value === parentName) {
parent.children.push({
id: Math.max(...objectScan(['**.id'], { rtn: 'value' })(tree)) + 1,
name: childName,
parent_id: parent.id,
children: []
});
return true;
}
return false;
}
})(tree);
const data = { id: 1, name: 'Dog', parent_id: null, children: [{ id: 2, name: 'Food', parent_id: 1, children: [] }, { id: 3, name: 'Water', parent_id: 1, children: [{ id: 4, name: 'Bowl', parent_id: 3, children: [] }, { id: 5, name: 'Oxygen', parent_id: 3, children: [] }, { id: 6, name: 'Hydrogen', parent_id: 3, children: [] }] }] };
console.log(insert(data, 'Dog', 'Cat')); // true iff insert was successful
// => true
console.log(data);
// => { id: 1, name: 'Dog', parent_id: null, children: [ { id: 2, name: 'Food', parent_id: 1, children: [] }, { id: 3, name: 'Water', parent_id: 1, children: [ { id: 4, name: 'Bowl', parent_id: 3, children: [] }, { id: 5, name: 'Oxygen', parent_id: 3, children: [] }, { id: 6, name: 'Hydrogen', parent_id: 3, children: [] } ] }, { id: 7, name: 'Cat', parent_id: 1, children: [] } ] }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.7.1"></script>
Disclaimer: I'm the author of object-scan

Related

Find maximum id value in a deeply nested array of objects

I have an tree data structure with each object containing children:
const data = {
id: 1,
name: "John",
parent_id: null,
children: [{
id: 2,
name: "Tess",
parent_id: 1,
children: []
},
{
id: 3,
name: "Tom",
parent_id: 1,
children: [{
id: 4,
name: "Harry",
parent_id: 3,
children: [{
id: 7,
name: "Thabo",
parent_id: 4,
children: []
}]
},
{
id: 5,
name: "Mary",
parent_id: 3,
children: []
},
{
id: 6,
name: "Madge",
parent_id: 3,
children: []
}
]
}
]
}
Before I can add a new object to the tree, I need to determine the highest id value currently used, so I can assign the next available number as id for the new user.
To do this I created a new variable with an initial value of 0. Then I iterate over each object in the tree, and if the object's id is higher than the new id, I assign the new id the current id's value (the idea being taking the final value and adding 1 to get the new id).
let newUserID = 0;
const newID = ( root, idKey ) => {
if ( root.id > idKey ) {
idKey = root.id;
}
root.children.forEach( ( obj ) => {
newID( obj, idKey );
});
return idKey;
}
newUserID = newID( data, newUserID );
console.log( newUserID );
I expected this to return the highest id in the tree as the final value, but what actually happens is that, while the new id does increase until it matches the maximum value, it then starts decreasing again, ending on 1.
This can be seen in this JSFiddle which includes some logging to show the value of the new ID at different points in the function.
I've since solved the issue using a different approach (extracting the id values to a new array, and using Math.max() to find the highest value), but I'd like to understand why my initial approach didn't work as expected. I can see the idKey value is being updated, but then the previous value gets passed back on the recursive call, but I don't know why that's happening or how to prevent it.
First, as to why your code is broken: You just missed an assignment. Where you have
newID( obj, idKey );
you are ignoring the resulting value. You need to assign it back to idKey:
idKey = newID( obj, idKey );
That will solve your problem. We should also note that the variable name newUserID is a bit of a misnomer, since it's not the the new one you will use but the highest one found. Perhaps highestUserID would be less confusing?
However, we should point out that this can be written much more simply, using Math .max to do the heavy lifting and a dollop of recursion. Here's how I might write this:
const maxId = ({id, children = []}) =>
Math .max (id, ... children .map (maxId))
const data = {id: 1, name: "John", parent_id: null, children: [{id: 2, name: "Tess", parent_id: 1, children: []}, {id: 3, name: "Tom", parent_id: 1, children: [{id: 4, name: "Harry", parent_id: 3, children: [{id: 7, name: "Thabo", parent_id: 4, children: []}]}, {id: 5, name: "Mary", parent_id: 3, children: []}, {id: 6, name: "Madge", parent_id: 3, children: []}]}]}
console .log (maxId (data))
Simply assign the returned value of the recursive call to idKey :
let newUserID = 0;
const newID = ( root, idKey ) => {
if ( root.id > idKey ) {
idKey = root.id;
}
root.children.forEach( ( obj ) => {
idKey = newID( obj, idKey ); // <--------
});
return idKey;
}
newUserID = newID( data, newUserID );
console.log( newUserID );
Without this assignment, no matter how much you recurse, the value returned will depend only on the result of the if statement at the top. This explains the logs you were getting.
You can use recursion to solve this. Like below
const data = {
id: 1,
name: "John",
parent_id: null,
children: [
{
id: 2,
name: "Tess",
parent_id: 1,
children: [],
},
{
id: 3,
name: "Tom",
parent_id: 1,
children: [
{
id: 4,
name: "Harry",
parent_id: 3,
children: [
{
id: 7,
name: "Thabo",
parent_id: 4,
children: [],
},
],
},
{
id: 5,
name: "Mary",
parent_id: 3,
children: [],
},
{
id: 6,
name: "Madge",
parent_id: 3,
children: [],
},
],
},
],
};
const findMax = (value) => {
let max = -Infinity;
const _findMax = (data) => {
if (max < data.id) max = data.id;
data.children.forEach(_findMax);
};
_findMax(value);
return max;
};
console.log(findMax(data));
You can do:
const data = {id: 1,name: 'John',parent_id: null,children: [{ id: 2, name: 'Tess', parent_id: 1, children: [] },{id: 3,name: 'Tom',parent_id: 1,children: [{id: 4,name: 'Harry',parent_id: 3,children: [{ id: 7, name: 'Thabo', parent_id: 4, children: [] }],},{ id: 5, name: 'Mary', parent_id: 3, children: [] },{ id: 6, name: 'Madge', parent_id: 3, children: [] },],},],}
const arr = [...JSON.stringify(data).matchAll(/"id":(\d+)/g)].map(([, n]) => +n)
const result = Math.max(...arr)
console.log(result)

How to make recursive function vue.js to make data from children

I am using vue.js and v-tree + vue2vis network,
I have a tree with all my items like this one :
items: [
{
id: 1,
name: 'root',
children: [
{
id: 2,
name: 'child1',
children: [
{
id: 3,
name: 'child3',
},
{
id: 4,
name: 'child34',
},
],
},
{
id: 5,
name: 'subroot',
children: [
{
id: 6,
name: 'Mike',
children:[
{
id: 7,
name: 'Mini Mike',
}
]
},
{
id: 8,
name: 'Hunt',
},
],
},
{
id: 9,
name: 'test',
children: [
{
id: 10,
name: 'Brandon',
},
{
id: 11,
name: 'Sean',
},
],
},
],
},
],
And what i want to do is when i click on a item in the tree it will generate data for the network like this:
nodes: [{'id':1 , label: 'root'},{'id':2 , label: 'child1'},{'id':3 , label: 'child3'}]
and so on for all the children and parents
same goes for the edage i want to create conection between parent and child
edage: [{'from': 1, 'to':2},{'from': 2, 'to':3}]
I try this function for this idea but its not working
makeNetConnection(items , itemKey) {
//items -> all the tree data
//itemKey-> its the item i click on the tree that i want to create the view
for (let item of items) {
if (item.name == itemKey) {
this.nodes.push({'id':item.id , 'label':item.name});
return item;
}
if (item.children) {
let i = this.makeNetConnection(item.children ,itemKey)
if (i) {
this.nodes.push({'id':item.id , 'label':item.name});
this.edges.push({'from': item.id , 'to': i.id});
return i;
}
}
}
its duplicate the data in the arrays and not make connect with the parent
i expect to have [{'from': 1, 'to':2},{'from': 2, 'to':3},{'from': 2, 'to':4}]
and os on for all the items
but i have [{'from': 1, 'to':2},{'from': 1, 'to':3} , {'from': 1, 'to':2},{'from': 1, 'to':3}, {'from': 1, 'to':4}, {'from': 1, 'to':4}]
i dont get the middel connection
any idea how to make its work?
Maybe overkill, but use traverse
const traverse = require('traverse');
const returnData=[];
traverse.forEach(function(item){
returnData.push({id:item.id,label:item.value});
});
I would build this upon simpler functions to collect the nodes and to collect the edges. It means running two traversals of your tree, but it makes for much simpler code.
Here we have a recursive function to flatten the nodes of a tree into an array, transforming the name property to a label one as we go. And we have a second recursive function to collect the edges as from-to pairs.
Then we write a very simple function to combine them:
const collectNodes = (xs) =>
xs .flatMap (({id, name, children = []}) => [
{id, label: name},
... collectNodes (children)
])
const collectEdges = (xs) =>
xs .flatMap (({id: from, children = []}) => [
... children .map (({id: to}) => ({from, to})),
... collectEdges (children),
])
const collect = (items) => ({
nodes: collectNodes (items),
edges: collectEdges (items)
})
const items = [{id: 1, name: "root", children: [{id: 2, name: "child1", children: [{id: 3, name: "child3"}, {id: 4, name: "child34"}]}, {id: 5, name: "subroot", children: [{id: 6, name: "Mike", children: [{id: 7, name: "Mini Mike"}]}, {id: 8, name: "Hunt"}]}, {id: 9, name: "test", children: [{id: 10, name: "Brandon"}, {id: 11, name: "Sean"}]}]}]
console .log (collect (items))
.as-console-wrapper {max-height: 100% !important; top: 0}
Could we do this in a single traversal? Certainly, but I think it would make for more convoluted code. I would only bother doing so if the performance of this is not acceptable.
I ignored this: "when i click on a item in the tree". I'm assuming that you want to convert all the data. If you only want it up to a certain node, then please add a clarification to the question.

Javascript : Search hierarchical tree

i have to get child object by name from hierarchical tree in JavaScript
My Sample data is as followed
{
id: 2,
name: "Alphabet",
parent: null,
path: "Alphabet",
children: [
{ id: 3,
name: "unit3",
parent: 2,
path: "Alphabet/unit3",
children:[
{ id: 5,
name: "unit15",
parent: 3,
path: "Alphabet/unit3/unit15",
children:[]
}
]
},
{ id: 4,
name: "unit6",
parent: 2,
path: "Alphabet/unit6",
children: []
}
]
}
I have tried as followed :
getChildFromTree(treeObj,name) : any {
if(treeObj.name == name) {
return treeObj;
}
var child;
for(var i=0;i<treeObj.children.length;i++) {
if(treeObj.children[i].name == name) {
child = treeObj.children[i];
break;
} else {
child = this.getChildFromTree(treeObj.children[i],name);
}
}
if(child) {
return child;
}
}
i am getting undefined when i search "unit15" please let me know, what wrong i am doing here
You are iterating over treeObj.children with for(var i = 0, however even if you find a child when using the recursive function this.getChildFromTree, it will not be returned since for(var i = 0 is not stopped (no break nor return in else branch inside the for loop).
You can simply add a if (child) return child; inside the loop.
Not a big fan of re-inveting the wheel and I'd recommend you use a library. We use object-scan for data processing stuff. It's pretty powerful once you wrap your head around it. Here is how you could answer your question:
// const objectScan = require('object-scan');
const find = (haystack, name) => objectScan(['**.name'], {
rtn: 'parent',
abort: true,
filterFn: ({ value }) => value === name
})(haystack);
const data = { id: 2, name: 'Alphabet', parent: null, path: 'Alphabet', children: [{ id: 3, name: 'unit3', parent: 2, path: 'Alphabet/unit3', children: [{ id: 5, name: 'unit15', parent: 3, path: 'Alphabet/unit3/unit15', children: [] }] }, { id: 4, name: 'unit6', parent: 2, path: 'Alphabet/unit6', children: [] }] };
console.log(find(data, 'unit15'));
/* =>
{ id: 5,
name: 'unit15',
parent: 3,
path: 'Alphabet/unit3/unit15',
children: [] }
*/
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.7.1"></script>
Disclaimer: I'm the author of object-scan

Remove matched object from deeply nested array of objects

I have a data tree structure with children:
{ id: 1,
name: "Dog",
parent_id: null,
children: [
{
id: 2,
name: "Food",
parent_id: 1,
children: []
},
{
id: 3,
name: "Water",
parent_id: 1,
children: [
{
id: 4,
name: "Bowl",
parent_id: 3,
children: []
},
{
id: 5,
name: "Oxygen",
parent_id: 3,
children: []
},
{
id: 6,
name: "Hydrogen",
parent_id: 3,
children: []
}
]
}
]
}
This represents a DOM structure that a user could select an item from to delete by clicking the corresponding button in the DOM.
I have a known text title of the selected item for deletion from the DOM set as the variable clickedTitle. I am having trouble finding an algorithm that will allow me to delete the correct object data from the deeply nested tree.
Here is my code:
function askUserForDeleteConfirmation(e) {
const okToDelete = confirm( 'Are you sure you want to delete the item and all of its sub items?' );
if(!okToDelete) {
return;
}
const tree = getTree(); // returns the above data structure
const clickedTitle = getClickedTitle(e); // returns string title of clicked on item from DOM - for example "Dog" or "Bowl"
const updatedTree = removeFromTree(tree, tree, clickedTitle);
return updatedTree;
}
function removeFromTree(curNode, newTree, clickedTitle) {
if(curNode.name === clickedTitle) {
// this correctly finds the matched data item to delete but the next lines don't properly delete it... what to do?
const index = curNode.children.findIndex(child => child.name === clickedTitle);
newTree = curNode.children.slice(index, index + 1);
// TODO - what to do here?
}
for(const node of curNode.children) {
removeFromTree(node, newTree, clickedTitle);
}
return newTree;
}
I have tried to use the info from Removing matched object from array of objects using javascript without success.
If you don't mind modifying the parameter tree in-place, this should do the job. Note that it'll return null if you attempt to remove the root.
const tree = { id: 1, name: "Dog", parent_id: null, children: [ { id: 2, name: "Food", parent_id: 1, children: [] }, { id: 3, name: "Water", parent_id: 1, children: [ { id: 4, name: "Bowl", parent_id: 3, children: [] }, { id: 5, name: "Oxygen", parent_id: 3, children: [] }, { id: 6, name: "Hydrogen", parent_id: 3, children: [] } ] } ] };
const removeFromTree = (root, nameToDelete, parent, idx) => {
if (root.name === nameToDelete) {
if (parent) {
parent.children.splice(idx, 1);
}
else return null;
}
for (const [i, e] of root.children.entries()) {
removeFromTree(e, nameToDelete, root, i);
}
return tree;
};
console.log(removeFromTree(tree, "Oxygen"));
Your current code is very much on the right track. However:
newTree = curNode.children.slice(index, index + 1);
highlights a few issues: we need to manipulate the parent's children array to remove curNode instead of curNode's own children array. I pass parent objects and the child index recursively through the calls, saving the trouble of the linear operation findIndex.
Additionally, slicing from index to index + 1 only extracts one element and doesn't modify curNode.children. It's not obvious how to go about using newArray or returning it through the call stack. splice seems like a more appropriate tool for the task at hand: extracting one element in-place.
Note that this function will delete multiple entries matching nameToDelete.
I like #VictorNascimento's answer, but by applying map then filter, each children list would be iterated twice. Here is an alternative with reduce to avoid that:
function removeFromTree(node, name) {
return node.name == name
? undefined
: {
...node,
children: node.children.reduce(
(children, child) => children.concat(removeFromTree (child, name) || []), [])
}
}
In the case you want a way to remove the items in-place, as #ggorlen proposed, I'd recommend the following solution, that is simpler in my opinion:
function removeFromTree(node, name) {
if (node.name == name) {
node = undefined
} else {
node.children.forEach((child, id) => {
if (!removeFromTree(child, name)) node.children.splice(id, 1)
})
}
return node
}
I've built the algorithm as follows:
function omitNodeWithName(tree, name) {
if (tree.name === name) return undefined;
const children = tree.children.map(child => omitNodeWithName(child, name))
.filter(node => !!node);
return {
...tree,
children
}
}
You can use it to return a new tree without the item:
noHydrogen = omitNodeWithName(tree, "Hydrogen")
If it's ok to use Lodash+Deepdash, then:
let cleaned = _.filterDeep([tree],(item)=>item.name!='Hydrogen',{tree:true});
Here is a Codepen
We use object-scan for many data processing tasks. It's powerful once you wrap your head around it. Here is how you could answer your question
// const objectScan = require('object-scan');
const prune = (name, input) => objectScan(['**[*]'], {
rtn: 'bool',
abort: true,
filterFn: ({ value, parent, property }) => {
if (value.name === name) {
parent.splice(property, 1);
return true;
}
return false;
}
})(input);
const obj = { id: 1, name: 'Dog', parent_id: null, children: [{ id: 2, name: 'Food', parent_id: 1, children: [] }, { id: 3, name: 'Water', parent_id: 1, children: [{ id: 4, name: 'Bowl', parent_id: 3, children: [] }, { id: 5, name: 'Oxygen', parent_id: 3, children: [] }, { id: 6, name: 'Hydrogen', parent_id: 3, children: [] }] }] };
console.log(prune('Oxygen', obj)); // return true iff pruned
// => true
console.log(obj);
// => { id: 1, name: 'Dog', parent_id: null, children: [ { id: 2, name: 'Food', parent_id: 1, children: [] }, { id: 3, name: 'Water', parent_id: 1, children: [ { id: 4, name: 'Bowl', parent_id: 3, children: [] }, { id: 6, name: 'Hydrogen', parent_id: 3, 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

Tree Structure from Adjacency List

I am trying to generate a hierarchical tree object from a flat array with parent IDs.
// `parent` represents an ID and not the nesting level.
var flat = [
{ id: 1, name: "Business", parent: 0 },
{ id: 2, name: "Management", parent: 1 },
{ id: 3, name: "Leadership", parent: 2 },
{ id: 4, name: "Finance", parent: 1 },
{ id: 5, name: "Fiction", parent: 0 },
{ id: 6, name: "Accounting", parent: 1 },
{ id: 7, name: "Project Management", parent: 2 }
];
The final tree object should look as follows:
{
id: 1,
name: "Business",
children: [
{
id: 2,
name: "Management",
children: [
{ id: 3, name: "Leadership" },
{ id: 7, name: "Project Management" }
]
}
// [...]
]
}
// [...]
You can see my current work on this fiddle, but it only works on the first two levels.
I thought about collecting the orphans (objects from flat without a parent in tree yet) and then looping over them again to see if they now have a parent. But that could mean many loops over the tree object, especially with many categories over multiple levels.
I'm sure there is a more elegant solution.
Looks like you can do this in 2 passes no matter the tree depth:
var flat = [
{ id: 1, name: "Business", parent: 0 },
{ id: 2, name: "Management", parent: 1 },
{ id: 3, name: "Leadership", parent: 2 },
{ id: 4, name: "Finance", parent: 1 },
{ id: 5, name: "Fiction", parent: 0 },
{ id: 6, name: "Accounting", parent: 1 },
{ id: 7, name: "Project Management", parent: 2 }
];
var nodes = [];
var toplevelNodes = [];
var lookupList = {};
for (var i = 0; i < flat.length; i++) {
var n = {
id: flat[i].id,
name: flat[i].name,
parent_id: ((flat[i].parent == 0) ? null : flat[i].parent),
children: []
};
lookupList[n.id] = n;
nodes.push(n);
if (n.parent_id == null) {
toplevelNodes.push(n);
}
}
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i];
if (!(n.parent_id == null)) {
lookupList[n.parent_id].children = lookupList[n.parent_id].children.concat([n]);
}
}
console.log(toplevelNodes);
Update 2, (six years later)
I happened to stumble across this again and realized how much simpler this can be with a little ES6 destructuring and a straightforward recursion:
const makeForest = (id, xs) =>
xs .filter (({parent}) => parent == id)
.map (({id, parent, ...rest}) => ({id, ...rest, children: makeForest (id, xs)}))
var flat = [{id: 1, name: "Business", parent: 0}, {id: 2, name: "Management", parent: 1}, {id: 3, name: "Leadership", parent: 2}, {id: 4, name: "Finance", parent: 1}, {id: 5, name: "Fiction", parent: 0}, {id: 6, name: "Accounting", parent: 1}, {id: 7, name: "Project Management", parent: 2}];
console .log (
makeForest (0, flat)
)
.as-console-wrapper {min-height: 100% !important; top: 0}
Note the name change. We're not making a tree, but an entire forest of trees, with each direct child of the root its own node. It would be relatively trivial to change this to generate a tree, if you had a structure to use for the root. Also note that this will let us find the tree rooted at any id. It doesn't have to be the overall root.
Performance
This does, as you worried about, have a worst-case performance of O(n^2). The actual run time is something like O(n * p) where p is the number of distinct parent nodes in the list. So we only approach the worst case when we have a tree nearly as deep as the length of the list. I would use something like this unless and until I could show that it was a hot spot in my code. My guess is that I would never find a need to replace it.
Lesson
Never count out recursion. This is significantly simpler than any of the other answers here, including my two versions below.
Update 1
I was not happy with extra complexity in my original solution. I'm adding another version that reduces that complexity. It manages to build the data in a single pass. It also gives one the chance to restructure the records in the trees differently from their original format if necessary. (By default, it only removes the parent node.)
Updated Version
Available on JSFiddle.
var makeTree = (function() {
var defaultClone = function(record) {
var newRecord = JSON.parse(JSON.stringify(record));
delete newRecord.parent;
return newRecord;
};
return function(flat, clone) {
return flat.reduce(function(data, record) {
var oldRecord = data.catalog[record.id];
var newRecord = (clone || defaultClone)(record);
if (oldRecord && oldRecord.children) {
newRecord.children = oldRecord.children;
}
data.catalog[record.id] = newRecord;
if (record.parent) {
var parent = data.catalog[record.parent] =
(data.catalog[record.parent] || {id: record.parent});
(parent.children = parent.children || []).push(newRecord);
} else {
data.tree.push(newRecord);
}
return data;
}, {catalog: {}, tree: []}).tree;
}
}());
Note that this will work regardless of the order of the flat list -- the parent nodes do not have to precede their children -- although there is nothing in here to sort the nodes.
Original Version
My solution (also on JSFiddle):
var makeTree = (function() {
var isArray = function(obj) {return Object.prototype.toString.call(obj) == "[object Array]"; };
var clone = function(obj) {return JSON.parse(JSON.stringify(obj));};
var buildTree = function(catalog, structure, start) {
return (structure[start] || []).map(function(id, index) {
var record = catalog[id];
var keys = structure[start][index];
var children = isArray(keys) ? keys.map(function(key) {
return buildTree(catalog, structure, key);
}) : buildTree(catalog, structure, keys);
if (children.length) {
record.children = children;
}
return record;
})
};
return function(flat) {
var catalog = flat.reduce(function(catalog, record) {
catalog[record.id] = clone(record);
delete(catalog[record.id].parent);
return catalog;
}, {});
var structure = flat.reduce(function(tree, record) {
(tree[record.parent] = tree[record.parent] || []).push(record.id);
return tree;
}, {});
return buildTree(catalog, structure, 0); // this might be oversimplified.
}
}());

Categories

Resources