JS Tree search fails - javascript

I have Javascript tree structure
const tree = [
{id: 120 , children:[]},
{id: 110 , children:[
{id: 12 , children:[
{id: 3 , children:[]},
{id: 4 , children:[]}
]}
]},
{id: 10 , children:[
{id: 13 , children:[]}
]}
]
and i have this function to find the parent of given node
const _findParent = (tree, component, _parent) => {
let parent = null
// iterate
tree.forEach(node => {
if(node.id === component.id){
return _parent
}
parent = node. children ? _findParent(node.children, component, node) : parent
})
return parent
}
but it returns null, I cant find where i miss the parent object.

Basically you check the children, but your children is always an array which is a truthy value. In this case, you could check if children is an array.
The use of Array#forEach does not work with a return value for using outside of the callback.
I suggest to use Array#some, which allows an early exit, which is necessary if a node is found.
Then I suggest to use a second variable for getting a nested result and if truthy, then assign to parent for return and exit the iteration.
const _findParent = (tree, component, _parent) => {
let parent;
tree.some(node => {
var temp;
if (node.id === component.id) {
parent = _parent;
return true;
}
temp = Array.isArray(node.children) && _findParent(node.children, component, node);
if (temp) {
parent = temp;
return true;
}
});
return parent;
}
const tree = [{ id: 120, children: [] }, { id: 110, children: [{ id: 12, children: [{ id: 3, children: [] }, { id: 4, children: [] }] }] }, { id: 10, children: [{ id: 13, children: [] }] }];
console.log(_findParent(tree, { id: 4 }));
console.log(_findParent(tree, { id: 42 }));

Related

Running trough every instance of a muti-layer object in JavaScript

I have an object that contains the following object template:
0:{
id: 1,
name: Name 1
children: 0:{
id: 2,
name: Name 2
children: {}
},
1:{
id: 3,
name: Name 3
children: {}
}
},
1:{
id: 4,
name: Name 4
children: 0:{
id: 5,
name: Name 5
children: {}
}
},
However as you can see every "children" key can also have an exact copy of an object, an so can the children of these children and so on. How can I convert every instance of "children", including the children inside the children and so on, into an array of objects like the example below?
0:{
id: 1,
name: Name 1
children: [
{
id: 2,
name: Name 2
children: {}
},
{
id: 3,
name: Name 3
children: {}
}
]
},
1:{
id: 4,
name: Name 4
children: [
{
id: 5,
name: Name 5
children: {}
}
]
},
Something like this should work:
function flatObject(arg) {
const result = [];
const goTo = (o, index = 0) => {
// If no more objects, return
if (typeof o[index] === "undefined")
return;
const e = o[index];
// Push this object
result.push({ id: e.id, name: e.name });
// goto childrens and append them
goTo(e.children);
// Goto next item
goTo(o, index + 1);
}
// Call function
goTo(arg);
return result;
}
It uses a recursive function to go through each item, and it goes through each children key in order to append them in the result array.
EDIT FOR MODIFIED QUESTION
What you are looking for is the Object.entries() method, that according to the MDN
The Object.entries() method returns an array of a given object's own enumerable string-keyed property [key, value] pairs.
function flatObject(arg) {
// Iterate all
Object.keys(arg).forEach(key => {
const ObjectToArray = (o) => {
// Convert children to array
o.children = Object.entries(o.children).map(e => e[1]);
// Convert children of children
o.children.forEach(child => ObjectToArray(child));
};
// Push childrens
ObjectToArray(arg[key]);
});
return arg;
}

Converting flat array of objects into nested array

I have an array as below, inside this array creditUsage array has object which has parent-child relationship but all are present at same level
let subCredits =[
{
creditNr : "A001"
creditUsage: [
{
id: 1,
parentId :null
},
{
id: 2,
parentId :null
},{
id: 22,
parentId :2
},{
id: 11,
parentId :1
},
]
},
{
creditNr : "A002"
creditUsage:[
{
id: 1,
parentId :null
},
{
id: 2,
parentId :null
},{
id: 22,
parentId :2
},{
id: 11,
parentId :1
},
]
}
]
I want to transform above array as below by introducing children array inside parent one
let subCredit = [
{
creditNr : "A001"
creditUsage:[
{
id: 1,
parentId :null
children:[
{id: 11,
parentId :1}
]
},
{
id: 2,
parentId :null
children:[
{id: 22,
parentId :2}
]
}
]
},
]
I'm filtering all rootCredits and storing it first then trying to add children nodes to it, but couldn't achieve
export const displayCredit(subCredits)=>
{
let root = filterRootCredits(subCredits);
let usage=[];
const treeData = root.map(r=>{
subCredits.map(s=>{
usage = r.creditUsage.map(node => {
const children =s.creditUsages.filter(item=>item.parentId ===node.id).map(item =>{return item;})
return {
...node,
children
}
})
})
return subCredit;
})
return treeData;
}
This can be solved easily with a recursive function (akin reduce, with an accumulator).
This is a pretty rush draft:
function nest_them(creds, acc) {
if (creds.length == 0) {
return acc
}
var [c, ...rest] = creds
if (c.parentId) {
let parent = acc.find(x => x.id == c.parentId);
(parent.children = parent.children || []).push(c)
return nest_them(rest, acc)
} else {
return nest_them(rest, [...acc, c])
}
}
And you can use like this:
subCredits.map(x => ({...x, creditUsage: nest_them(x.creditUsage, [])}))

Javascript array gets populated and i don't understand how

While searching a solution for an issue i had about Tree Structure from Adjacency List. i came across an answer that solved it for me but i cannot understand how it's working and i feel like i'm missing some basic understanding about javascript.
The answer by #TreyE in this question gives the next solution for sorting the tree ->
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);
This works just fine but i just cannot understand how all this logic gets ordered under toplevelNodes if the only thing that was pushed to it was the first level elements. The rest is done on nodes and lookuplist. Again i feel like i'm missing some basic understanding of javascript. how does everything sorts into a nice tree of childrens under topLevelNodes array ?
The secret lies in lookupList which is an Object. Items (or live items) stored inside that Object are not deep clones, those are just "pointers" (not exactly to memory addresses like in C), to existing items in memory, as reference. If a node changes (i.e: by adding more children to a Node) that "reference" to that element will be modified as well, since it's actually that same Node.
Right in here:
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]);
}
}
Here's a banally simplified example:
const node_1 = {id: 1, name: "foo", children: []};
const node_2 = {id: 2, name: "bar", children: []};
const lookupList = {};
// store node_1 into lookup - as reference:
lookupList[node_1.id] = node_1;
// (Unrelated to lookupList) add node_2 as child of node_1
node_1.children.push(node_2);
// Let's sniff lookup for node_1 (id: 1)
console.log(lookupList["1"]); // There's also node_2 as child of node_1
console.log(lookupList["1"] === node_1); // true (it's the same Node)
In JS variables that point to object or array hold reference to that object and not a copy of it.
If you have multiple variables pointing to the same object and mutate the object via one variable, the object under the other reference would be mutated as well (because both variables point to the same object):
var a = {hello: 'a'}
var b = a
b.hello = 'b'
console.log(a.hello)
// prints 'b'
The same thing happens in your code: nodes, toplevelNodes, lookupList all hold references to the same node objects inside them, when you mutate one in one place, it's also updated in other places.
Basically this is the line that does the magic:
lookupList[n.parent_id].children = ...
If you are looking for deeper understanding of this topic, please check this section: https://eloquentjavascript.net/04_data.html#h_C3n45IkMhg
lookupList maps ids to their nodes, since thats whats added to it in the first loop: lookupList[n.id] = n
In the second loop, every node that isn't a top level node is added to its parent's children array. Its parent is retrieved looking up the node's parent_id property in lookupList (lookupList[n.parent_id])
Maybe another approach would be more easily understood? This will modify nodes (née flat) in-place, though:
/**
Augment each node in the "nodes" list with a `children` property,
and return the top-level nodes.
*/
function treeify(nodes) {
const children = {};
// Walk through the flat list of nodes once...
nodes.forEach((n) => {
// Retrieve or initialize a list for `n.parent`'s children.
const childList = children[n.parent] || (children[n.parent] = []);
// Add this node there.
childList.push(n);
});
// ... and walk through it again, to assign the new `children` property.
nodes.forEach((n) => {
// Pick each "children" property from the children map,
// or in case there is none, come up with an empty list.
n.children = children[n.id] || [];
});
// Nodes with parent 0 are top-level; return them.
return children[0];
}
const nodes = [
{ 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 },
];
const tree = treeify(nodes);
// Node.js specific printing stuff...
const util = require("util");
console.log(util.inspect(tree, null, { depth: 5 }));
This outputs
[
{
id: 1,
name: 'Business',
parent: 0,
children: [
{
id: 2,
name: 'Management',
parent: 1,
children: [
{ id: 3, name: 'Leadership', parent: 2, children: [] },
{
id: 7,
name: 'Project Management',
parent: 2,
children: []
}
]
},
{ id: 4, name: 'Finance', parent: 1, children: [] },
{ id: 6, name: 'Accounting', parent: 1, children: [] }
]
},
{ id: 5, name: 'Fiction', parent: 0, children: [] }
]

javascript find deeply nested objects

I need to filter objects recursively in a deeply nested array of objects using javascript, maybe with the help of lodash.
What is the cleanest way to do it, If I don't know how many nested object there will be in my array?
Let's say I have the following structure
[
{
label: "first",
id: 1,
children: []
},
{
label: "second",
id: 2,
children: [
{
label: "third",
id: 3,
children: [
{
label: "fifth",
id: 5,
children: []
},
{
label: "sixth",
id: 6,
children: [
{
label: "seventh",
id: 7,
children: []
}
]
}
]
},
{
label: "fourth",
id: 4,
children: []
}
]
}
];
I want to find the one with id 6, and if it has children return true otherwise false.
Of course If I have a similar data structure but with different number of items it should work too.
Since you only want a true of false answer you can use some() on the recursion, effectively doing a depth-first search, and make it pretty succinct:
let arr = [{label: "first",id: 1,children: []},{label: "second",id: 2,children: [{label: "third",id: 3,children: [{label: "fifth",id: 5,children: []},{label: "sixth",id: 6,children: [{label: "seventh",id: 7,children: []}]}]},{label: "fourth",id: 4,children: []}]}];
function findNested(arr, id) {
let found = arr.find(node => node.id === id)
return found
? found.children.length > 0
: arr.some((c) => findNested(c.children, id))
}
console.log(findNested(arr, 6)) // True: found with children
console.log(findNested(arr, 7)) // False: found no children
console.log(findNested(arr, 97)) // False: not found
Perhaps a recursive solution along the lines of this might work for you? Here, the node with supplied id is recursively searched for through the 'children' of the supplied input data. If a child node with matching id is found, a boolean result is returned based on the existence of data in that nodes children array:
function nodeWithIdHasChildren(children, id) {
for(const child of children) {
// If this child node matches supplied id, then check to see if
// it has data in it's children array and return true/false accordinly
if(child.id === id) {
if(Array.isArray(child.children) && child.children.length > 0) {
return true
}
else {
return false
}
}
else {
const result = nodeWithIdHasChildren(child.children, id);
// If result returned from this recursion branch is not undefined
// then assume it's true or false from a node matching the supplied
// id. Pass the return result up the call stack
if(result !== undefined) {
return result
}
}
}
}
const data = [
{
label: "first",
id: 1,
children: []
},
{
label: "second",
id: 2,
children: [
{
label: "third",
id: 3,
children: [
{
label: "fifth",
id: 5,
children: []
},
{
label: "sixth",
id: 6,
children: [
{
label: "seventh",
id: 7,
children: []
}
]
}
]
},
{
label: "fourth",
id: 4,
children: []
}
]
}
];
console.log('node 6 has children:', nodeWithIdHasChildren( data, 6 ) )
console.log('node 7 has children:', nodeWithIdHasChildren( data, 7 ) )
console.log('node 100 has children:', nodeWithIdHasChildren( data, 7 ), '(because node 100 does not exist)' )
Here is another solution using recursion and doing it via only one Array.find:
const data = [ { label: "first", id: 1, children: [] }, { label: "second", id: 2, children: [ { label: "third", id: 3, children: [ { label: "fifth", id: 5, children: [] }, { label: "sixth", id: 6, children: [ { label: "seventh", id: 7, children: [] } ] } ] }, { label: "fourth", id: 4, children: [] } ] } ];
const search = (data, id) => {
var f, s = (d, id) => d.find(x => x.id == id ? f = x : s(x.children, id))
s(data, id)
return f ? f.children.length > 0 : false
}
console.log(search(data, 6)) // True: found with children
console.log(search(data, 7)) // False: found but has no children
console.log(search(data, 15)) // False: not found at all
The idea is to have a recursive function which when finds the id remembers the object.
Once we have the found (or we know we do not have an entry found) just return the children array length or return false.
If you want to actually return the found object instead of the boolean for children.length:
const data = [ { label: "first", id: 1, children: [] }, { label: "second", id: 2, children: [ { label: "third", id: 3, children: [ { label: "fifth", id: 5, children: [] }, { label: "sixth", id: 6, children: [ { label: "seventh", id: 7, children: [] } ] } ] }, { label: "fourth", id: 4, children: [] } ] } ];
const search = (data, id) => {
var f, s = (d, id) => d.find(x => x.id == id ? f = x : s(x.children, id))
s(data, id)
return f
}
console.log(search(data, 6)) // returns only the object with id:6
console.log(search(data, 7)) // returns only the object with id: 7
console.log(search(data, 71)) // returns undefined since nothing was found
You can use "recursion" like below to check if id has children or not
let arr = [{label: "first",id: 1,children: []},{label: "second",id: 2,children: [{label: "third",id: 3,children: [{label: "fifth",id: 5,children: []},{label: "sixth",id: 6,children: [{label: "seventh",id: 7,children: []}]}]},{label: "fourth",id: 4,children: []}]}];
function hasChildren(arr, id) {
let res = false
for (let d of arr) {
if(d.id == id) return d.children.length > 0
res = res || hasChildren(d.children, id)
if(res) return true
}
return res
}
console.log('id 4 has children? ', hasChildren(arr, 4))
console.log('id 6 has children? ', hasChildren(arr, 6))
You can do it using three simple javascript functions:
// Function to Flatten results
var flattenAll = function(data) {
var result = [];
var flatten = function(arr) {
_.forEach(arr, function(a) {
result.push(a);
flatten(a.children);
});
};
flatten(data);
return result;
};
// Function to search on flattened array
var search = function(flattened, id) {
var found = _.find(flattened, function(d) {
return d.id == id;
});
return found;
};
// Function to check if element is found and have children
var hasChildren = function(element) {
return element && element.children && element.children.length > 0;
}
// Usage, search for id = 6
hasChildren(search(flattenAll(your_data_object), 6))
Plunker
You can use a generator function to iterate the nodes recursively and simplify your logic for checking existence by using Array.prototype.some():
const data = [{label:'first',id:1,children:[]},{label:'second',id:2,children:[{label:'third',id:3,children:[{label:'fifth',id:5,children:[]},{label:'sixth',id:6,children:[{label:'seventh',id:7,children:[]}]}]},{label:'fourth',id:4,children:[]}]}];
function * nodes (array) {
for (const node of array) {
yield node;
yield * nodes(node.children);
}
}
const array = Array.from(nodes(data));
console.log(array.some(node => node.id === 6 && node.children.length > 0));
console.log(array.some(node => node.id === 7 && node.children.length > 0));
The JSON.parse reviver parameter or the JSON.stringify replacer parameter can be used to check all values, and generate flat id lookup object with references to the nodes :
var lookup = {}, json = '[{"label":"first","id":1,"children":[]},{"label":"second","id":2,"children":[{"label":"third","id":3,"children":[{"label":"fifth","id":5,"children":[]},{"label":"sixth","id":6,"children":[{"label":"seventh","id":7,"children":[]}]}]},{"label":"fourth","id":4,"children":[]}]}]'
var result = JSON.parse(json, (key, val) => val.id ? lookup[val.id] = val : val);
console.log( 'id: 2, children count:', lookup[2].children.length )
console.log( 'id: 6, children count:', lookup[6].children.length )
console.log( lookup )
I suggest to use deepdash extension for lodash:
var id6HasChildren = _.filterDeep(obj,
function(value, key, parent) {
if (key == 'children' && parent.id == 6 && value.length) return true;
},
{ leavesOnly: false }
).length>0;
Here is a docs for filterDeep.
And this a full test for your case.
We now use object-scan for data processing needs like this. It's very powerful once you wrap your head around it. This is how you could solve your questions
// const objectScan = require('object-scan');
const hasChildren = (e) => e instanceof Object && Array.isArray(e.children) && e.children.length !== 0;
const find = (id, input) => {
const match = objectScan(['**'], {
abort: true,
rtn: 'value',
filterFn: ({ value }) => value.id === id
})(input);
return hasChildren(match);
};
const data = [{ label: 'first', id: 1, children: [] }, { label: 'second', id: 2, children: [{ label: 'third', id: 3, children: [{ label: 'fifth', id: 5, children: [] }, { label: 'sixth', id: 6, children: [{ label: 'seventh', id: 7, children: [] }] }] }, { label: 'fourth', id: 4, children: [] }] }];
console.log(find(6, data));
// => true
console.log(find(2, data));
// => true
console.log(find(7, data));
// => false
console.log(find(999, data));
// => false
.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

Lodash - Filter nested collection by array of prop value

Say the original collection is:
var data = [{
id: 1,
children: [{
id: 2
}, {
id: 3
}]
}, {
id: 4,
children: [{
id: 5
}]
}]
Want to filter it with this given values of id property:
var selectedIds = [1, 3, 4]
The results should be:
var result = [{
id: 1,
children: [{
id: 3
}]
}, {
id: 4,
children: []
}]
How to accomplish this using Lodash methods?
You can use map and filter together to apply filter on nested element,
_.map(data, elem => {
elem.children = _.filter(elem.children, child => selectedIds.includes(child.id));
return elem;
});
NOTE: Assuming the filter has to be applied only on children property.
To not modify the original data,
_.map(data, elem => {
let children = _.filter(elem.children, child => selectedIds.includes(child.id));
return Object.assign({}, elem, {children});
});
You'll need to recursively walk your collection (and each item's children). I don't believe lodash has a native way of implementing this. I wrote a function called matcher that takes the collection and selected ids, and returns items that match those ids, and passes the children in and does the same thing. I wasn't sure how to handle a situation where a parent isn't selected but the children are.... I just assumed you'd throw them all away.
Point is, you have a very particular structure (items and children that are items) that requires you to write a specific recursive function that knows how to walk that structure.
const matcher = (collection, selected) => _.reduce(collection, (result, item) => {
if (selected.includes(item.id)) {
result.push({
id: item.id,
children: matcher(item.children, selected)
})
}
return result
}, [])
const result = matcher(data, selectedIds)
You could check for id and take only children with wanted id.
function getParts(array, ids) {
return array.reduce((r, { id, children }) =>
r.concat(ids.includes(id)
? { id, children: getParts(children || [], ids) }
: []
), []);
}
var data = [{ id: 1, children: [{ id: 2 }, { id: 3 }] }, { id: 4, children: [{ id: 5 }] }];
console.log(getParts(data, [1, 3, 4]));
console.log(getParts(data, [1, 3, 5]));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Categories