Get all parents for object in nested array JS - javascript

I have a problem on a project using vuejs.
I have an array of nested object like this :
Data
data: [
{
"id":1,
"parent_id":null,
"title":"First folder",
"children":[
{
"id":3,
"parent_id":1,
"title":"First folder in parents",
"children":[]
},
{
"id":4,
"parent_id":1,
"title":"Second folder in parents",
"children":[
{
"id":5,
"parent_id":4,
"title":"test",
"children":[]
}
],
}
]
},
{
"id":2,
"parent_id":null,
"title":"Second folder",
"children":[],
}
]
And I would like to get an array of all parents for a specific id in a computed property
computed: {
parents() {
this.getAllParents(data, currentId);
},
},
And my function
getParents(array, id, parentsId) {
for (let i = 0; i < array.length; i++) {
const child = array[i];
if (child.id === id && ((child.children.length > 0 && child.children.some((el) => el.id === parentsId)) || !parentsId)) {
return [child];
} if (child.children.length > 0) {
const x = this.getParents(child.children, child.id, id);
if (x) return Array.isArray(x) ? [child, ...x] : [child, x];
}
}
return [];
},
For example, if my currentId is 3, I would like this in my computed:
[
{"id": 1, "parent_id": null....},
{"id": 3, "parent_id": 1....}
]
If my currentId is 1, I would like this :
[
{"id": 1, "parent_id": null....},
]
If my currentId is 5, I would like this :
[
{"id": 1, "parent_id": null....},
{"id": 4, "parent_id": 1....},
{"id": 5, "parent_id": 4....},
]
Now, my function
return [
{"id": 1, "parent_id": null....},
{"id": 4, "parent_id": 1....}
]
if my current id is 3, not id:3 and I can't understand why
How to do this please ?
Thanks

You can recursively loop over the children array and keep collecting the parents.
Refer to the solution below:
const data = [
{
id: 1,
parent_id: null,
title: "First folder",
children: [
{ id: 3, parent_id: 1, title: "First folder in parents", children: [] },
{
id: 4,
parent_id: 1,
title: "Second folder in parents",
children: [{ id: 5, parent_id: 4, title: "test", children: [] }],
},
],
},
{ id: 2, parent_id: null, title: "Second folder", children: [] },
];
const getAncestors = (target, children, ancestors = []) => {
for (let node of children) {
if (node.id === target) {
return ancestors.concat(node.id);
}
const found = getAncestors(target, node.children, ancestors.concat(node.id));
if (found) {
return found;
}
}
return undefined;
};
console.log(getAncestors(5, data));
Note: I've just pushed the ids for brevity, you can update the solution to push the entire nodes.

Imagine you had a magic genie that could give you helper functions as you needed. What would you wish for in order to make this function simple to write?
To my mind, if we had a function ancestry, which accepted a predicate to check against a node and simply returned the whole ancestry tree up to that node when it matched, then we could just write this:
const findAncestryById = (targetId) =>
ancestry (({id}) => id == targetId)
Well, how would we write ancestry? Again, with a magic genie helping out, we can imagine that we had a function to walk all the elements of an array, recurring on their children nodes, and supplying the full ancestry path as it goes, then ancestry could build upon that. We probably want this to be a generator function, since those are easy to stop traversing once we've found our target values. So if we imagine we have walk, then ancestry might look like this:
const ancestry = (pred) => (obj) => {
for (let nodes of walk (obj)) {
if (pred (nodes .at (-1))) {
return nodes
}
}
}
So now we need a walk function that walks an array and all its (recursive) descendents, yielding the full path to each node on every step. Well it turns out that this is relatively simple to do, and we can put it all together like this:
function * walk (xs, path = []) {
for (let x of xs) {
const newPath = path .concat (x)
yield newPath
yield * walk (x .children || [], newPath)
}
}
const ancestry = (pred) => (obj) => {
for (let nodes of walk (obj)) {
if (pred (nodes .at (-1))) {
return nodes
}
}
return []
}
const findAncestryById = (targetId) =>
ancestry (({id}) => id == targetId)
const data = [{id: 1, parent_id: null, title: "First folder", children: [{id: 3, parent_id: 1, title: "First folder in parents", children: []}, {id: 4, parent_id: 1, title: "Second folder in parents", children: [{id: 5, parent_id: 4, title: "test", children: []}]}]}, {id: 2, parent_id: null, title: "Second folder", children: []}]
console .log (findAncestryById (5) (data))
// or to see it without the StackOverflow id-ref pairs:
console .log (JSON .stringify(
findAncestryById (5) (data)
, null, 4))
.as-console-wrapper {max-height: 100% !important; top: 0}
This magic-genie style has some strong advantages. It lets you abstract your specific problem into more generic ones. The generic ones are often easier to solve. But even if they're not, when you're done, you've built some reusable functions applicable to many other situations.
A side note: your data seems to have redundant information. The parent_id property is already encoded by the tree hierarchy. I personally would look to remove it, as there's always a worry about such data getting out of sync.

generic find
Recursion is a functional heritage and so by using functional style we yield the best results. Your code is quite good but there is some room for improvement. By decomposing the problem, we isolate concerns and make it easier for us to write individual solutions to the smaller sub-problems.
We start with higher-order find(t, func) that searches t using function, func, and returns all paths to the matched nodes.
If t is an array, for all child of t recur on the sub-problem find(child, func)
(inductive) t is not an array. If t is an object, then if the user-supplied function returns true for f, yield the singleton output, [t], otherwise for each path of the sub-problem find(t.children, func), prepend t to the path and yield.
(inductive) t is neither an array nor an object. Stop iteration.
// find (node array ∪ node, (node -> boolean)) -> (node array) generator
function* find(t, func) {
switch (true) {
case is(t, Array): // 1
for (const child of t)
yield* find(child, func)
break
case is(t, Object): // 2
if (Boolean(func(t)))
yield [t]
else for (const path of find(t.children, func))
yield [t, ...path]
break
default: // 3
return
}
}
Which depends on is -
function is(t, T) {
return t?.constructor === T
}
specialized find
Writing findById is a matter of specializing find. We assume id are unique and so return the path to the first matched node, if any. Otherwise return the empty path, [].
// findById : (node array ∪ node, number) -> node array
function findById(t, id) {
for (const path of find(t, node => node.id === id))
return path
return []
}
demo
To demonstrate proper behavior, I wrote a demo function which prunes some of the fields from the returned nodes. This allows us to us to easily confirm the output -
function demo(t, id) {
console.log(
`== id: ${id} ==`,
JSON.stringify(
findById(data, id).map(p =>
({ id: p.id, parent_id: p.parent_id, title: p.title })
)
)
)
}
demo(data, 1)
demo(data, 3)
demo(data, 5)
== id: 1 ==
[{"id":1,"parent_id":null,"title":"First folder"}]
== id: 3 ==
[{"id":1,"parent_id":null,"title":"First folder"},{"id":3,"parent_id":1,"title":"First folder in parents"}]
== id: 5 ==
[{"id":1,"parent_id":null,"title":"First folder"},{"id":4,"parent_id":1,"title":"Second folder in parents"},{"id":5,"parent_id":4,"title":"test"}]
You can verify the output below -
function is(t, T) {
return t?.constructor === T
}
function* find(t, func) {
switch (true) {
case is(t, Array):
for (const child of t)
yield* find(child, func)
break
case is(t, Object):
if (Boolean(func(t)))
yield [t]
else
for (const path of find(t.children, func))
yield [t, ...path]
break
default:
return
}
}
function findById(t, id) {
for (const path of find(t, node => node.id === id))
return path
return []
}
function demo(t, id) {
console.log(`== id: ${id} ==`, JSON.stringify(findById(data, id).map(p =>
({ id: p.id, parent_id: p.parent_id, title: p.title })
)))
}
const data = [{id:1,parent_id:null,title:"First folder",children:[{id:3,parent_id:1,title:"First folder in parents",children:[]},{id:4,parent_id:1,title:"Second folder in parents",children:[{id:5,parent_id:4,title:"test",children:[]}],}]},{id:2,parent_id:null,title:"Second folder",children:[]}]
demo(data, 1)
demo(data, 3)
demo(data, 5)

const data = [{id: 1, parent_id: null, title: "First folder", children: [{id: 3, parent_id: 1, title: "First folder in parents", children: []}, {id: 4, parent_id: 1, title: "Second folder in parents", children: [{id: 5, parent_id: 4, title: "test", children: []}]}]}, {id: 2, parent_id: null, title: "Second folder", children: []}]
function getAllParents(arr, id, res) {
for (const el of arr) {
if (el.id == id) {
res.unshift(el);
return true;
} else {
let is_parent = getAllParents(el.children, id, res);
if (is_parent) {
res.unshift(el);
return true;
}
}
}
}
let res;
for (const i of [1, 2, 3, 4, 5]) {
res = [];
getAllParents(data, i, res);
console.log(
res.map((path) => {
const { id, parent_id, title } = path;
return { id, parent_id, title };
})
);
}

Related

How do I get an Array of recursive parent Ids from a category tree?

I have the following date structure (Recursive Tree of Categories that can have 0..n subCategories)
let categoryTree = [
{
id: 1,
subCategories: [
{
id: 2,
subCategories: [
{
id: 3,
subCategories: [],
},
],
},
{
id: 4,
subCategories: [
{
id: 5,
subCategories: [],
},
],
},
],
},
{
id: 6,
subCategories: [
{
id: 7,
subCategories: [
{
id: 8,
subCategories: [],
},
],
},
{
id: 9,
subCategories: [
{
id: 10,
subCategories: [],
},
],
},
],
},
];
What is the easiest way for a function which returns me an array of all recursive parent ids?
Examples:
getParentIds(3) // Should return [1,2]
getParentIds(8) // Should return [6,7]
getParentIds(4) // Should return [1]
getParentIds(1) // Should return []
My attempt at this, however, this only works sometimes and not for all of the possible ids.
function recursiveFindParent(id, categories) {
for(let i = 0; i < categories.length; i++){
const c = categories[i];
const subCat = c.subCategories.find(s => s.id === id);
if (subCat !== undefined) {
return c;
} else {
return recursiveFindParent(id, c.subCategories);
}
}
}
function getParentIds(id) {
let foundParents = [];
let parentId = id;
let parent = null;
while (parent = recursiveFindParent(parentId, categories)) {
foundParents.push(parent.id);
parentId = parent.id;
}
return foundParents;
}
console.log(getParentIds(3, categoryTree));
This question comes up a lot and I recently answered it in this Q&A. Your specific inquiry differs enough that I'll make a new post here, however much of the work is taken directly from the original answer.
generic find
Recursion is a functional heritage and so by using functional style we yield the best results. By decomposing the problem, we isolate concerns and make it easier for us to write individual solutions to the smaller sub-problems.
We start with higher-order find(t, func) that searches t using function, func, and returns all paths to the matched nodes.
If t is an array, for all child of t recur on the sub-problem find(child, func)
(inductive) t is not an array. If t is an object, then if the user-supplied function returns true for f, yield the singleton output, [t], otherwise for each path of the sub-problem find(t.subCategories, func), prepend t to the path and yield.
(inductive) t is neither an array nor an object. Stop iteration.
// node : { id: int, subCategories: node array }
// find : (node ∪ node array, node -> boolean) -> (node array) generator
function* find(t, func) {
switch (true) {
case is(t, Array): // 1
for (const child of t)
yield* find(child, func)
break
case is(t, Object): // 2
if (Boolean(func(t)))
yield [t]
else for (const path of find(t.subCategories, func))
yield [t, ...path]
break
default: // 3
return
}
}
Which depends on is -
// is : (any, any) -> boolean
function is(t, T) {
return t?.constructor === T
}
specialized find
Writing findById is a matter of specializing find. We assume id are unique and so return the path to the first matched node, if any. Otherwise return the empty path, [].
// findById : (node ∪ node array, number) -> node array
function findById(t, id) {
for (const path of find(t, node => node.id === id))
return path
return []
}
checkpoint 1
To demonstrate proper behavior, I wrote a demo function which selects only the node id field and creates a ->-separated path. This allows us to us to easily confirm the output -
function demo(t, id) {
console.log(
`== id: ${id} ==`,
findById(data, id).map(p => p.id).join("->")
)
}
demo(data, 3)
demo(data, 8)
demo(data, 4)
demo(data, 1)
== id: 3 == 1->2->3
== id: 8 == 6->7->8
== id: 4 == 1->4
== id: 1 == 1
only the node ancestry
Your question asks specifically for the node's ancestry, which we can compute using another specialization. As we saw above, findById gets us all the way to the matched node. To select only its ancestry, we can simply slice the matched path, returning all but the last path element.
// findAncestryById : (node ∪ node array, int) -> node array
function findAncestryById(t, id) {
return findById(t, id).slice(0, -1) // ✅ all but the last
}
demo
This time we will write demo to perform a query using findAncestryById -
function demo(t, id) {
console.log(
`== id: ${id} ==`,
JSON.stringify(findAncestryById(data, id).map(p => p.id))
)
}
demo(data, 3)
demo(data, 8)
demo(data, 4)
demo(data, 1)
== id: 3 == [1,2]
== id: 8 == [6,7]
== id: 4 == [1]
== id: 1 == []
You can verify the output below -
function is(t, T) {
return t?.constructor === T
}
function* find(t, func) {
switch (true) {
case is(t, Array):
for (const child of t)
yield* find(child, func)
break
case is(t, Object):
if (Boolean(func(t)))
yield [t]
else
for (const path of find(t.subCategories, func))
yield [t, ...path]
break
default:
return
}
}
function findById(t, id) {
for (const path of find(t, node => node.id === id))
return path
return []
}
function findAncestryById(t, id) {
return findById(t, id).slice(0, -1)
}
function demo(t, id) {
console.log(
`== id: ${id} ==`,
JSON.stringify(findAncestryById(data, id).map(p => p.id))
)
}
const data = [{id:1,subCategories:[{id:2,subCategories:[{id:3,subCategories:[]}]},{id:4,subCategories:[{id:5,subCategories:[]}]}]},{id:6,subCategories:[{id:7,subCategories:[{id:8,subCategories:[]}]},{id:9,subCategories:[{id:10,subCategories:[]}]}]}];
demo(data, 3)
demo(data, 8)
demo(data, 4)
demo(data, 1)
gotcha
Did you notice findAncestryById(data, 1) returns an empty ancestry []? How would the output vary if 1 could not be found in the tree? This differs from the findById function which returns a valid empty path [] when a node is not found.
The reason findAncestryById needs a distinction is because [] is the same as [].slice(0, -1). For this particular function, you may want to return null if the node is not present in the tree. This is left as an exercise for the reader.
We can see this as a variant of a Q+A from a few days ago. I leave the explanation out, as that one covers it just fine. The only tweaks I had to make is to note the child node is called subCategories and to pull the id out of the returned list of nodes.
function * walk (xs, path = []) {
for (let x of xs) {
const newPath = path .concat (x)
yield newPath
yield * walk (x .subCategories || [], newPath)
}
}
const ancestry = (pred) => (obj) => {
for (let nodes of walk (obj)) {
if (pred (nodes .at (-1))) {
return nodes
}
}
return []
}
const findAncestryById = (tree, targetId) =>
ancestry (({id}) => id == targetId) (tree) .map (x => x .id) .slice (0, -1)
const categoryTree = [{id: 1, subCategories: [{id: 2, subCategories: [{id: 3, subCategories: []}]}, {id: 4, subCategories: [{id: 5, subCategories: []}]}]}, {id: 6, subCategories: [{id: 7, subCategories: [{id: 8, subCategories: []}]}, {id: 9, subCategories: [{id: 10, subCategories: []}]}]}];
[3, 8, 4, 1, 42] .forEach (
(n) => console .log (`findAncestryById (${n}) //=> [${findAncestryById (categoryTree, n)}]`)
)
Also, please look at Mulan's excellent answer to that same question, which works quite differently.
My preference is always to write readable and maintainable code (e.g. what if your data structure changes and you have subC and subCategories in your tree?). So if you are ok using a library, this might be a good answer.
.as-console-wrapper {max-height: 100% !important; top: 0}
<script type="module">
import objectScan from 'https://cdn.jsdelivr.net/npm/object-scan#18.2.0/lib/index.min.js';
const categoryTree = [
{ id: 1, subCategories: [{ id: 2, subCategories: [{ id: 3, subCategories: [] }] }, { id: 4, subCategories: [{ id: 5, subCategories: [] }] }] },
{ id: 6, subCategories: [{ id: 7, subCategories: [{ id: 8, subCategories: [] }] }, { id: 9, subCategories: [{ id: 10, subCategories: [] }] }] }
];
const getParentIds = objectScan(
['[*].**{subCategories[*]}'],
{
abort: true,
filterFn: ({ value, context }) => value?.id === context,
rtn: 'parents',
afterFn: ({ result }) => result
.map((p) => p?.id)
.filter((p) => p)
.reverse()
}
);
console.log(getParentIds(categoryTree, 3));
// => [ 1, 2 ]
console.log(getParentIds(categoryTree, 8));
// => [ 6, 7 ]
console.log(getParentIds(categoryTree, 4));
// => [ 1 ]
console.log(getParentIds(categoryTree, 1));
// => []
</script>
Disclaimer: I'm the author of object-scan

Creating nested array from flat array - Data Structure

I need to create a nested array using the path as reference for the children.
E.g: 4.1 is a child of 4, 4.1.1 is a child of 4.1, 4.2 is a child of 4...
I have this flat array, with all the data and paths. How would be the best approach to create a nested array where the children are nested to its parent based on its path.
Input:
const list = [
{
location: 1,
path: '4'
},
{
location: 2,
path: '4.1'
},
{
location: 3,
path: '4.1.1'
},
{
location: 4,
path: '4.1.2'
},
{
location: 5,
path: '4.2'
},
{
location: 6,
path: '4.2.1'
},
{
location: 7,
path: '4.3'
},
{
location: 8,
path: '4.3.1'
}
];
Output:
const list = [
{
location: 1,
path: '4',
children: [
{
location: 2,
path: '4.1',
children: [
{
location: 3,
path: '4.1.1'
},
{
location: 4,
path: '4.1.2'
},
]
},
{
location: 5,
path: '4.2',
children: [
{
location: 6,
path: '4.2.1'
},
]
},
{
location: 7,
path: '4.3',
children: [
{
location: 8,
path: '4.3.1'
}
]
},
]
},
];
The best approach would be something recursive.
Any suggestions for this algorithm?
I was curious if the linked answer from Scott would be able to solve this problem without modification. It does!
import { tree } from './Tree'
import { bind } from './Func'
const parent = (path = "") =>
bind
( (pos = path.lastIndexOf(".")) =>
pos === -1
? null
: path.substr(0, pos)
)
const myTree =
tree // <- make tree
( list // <- array of nodes
, node => parent(node.path) // <- foreign key
, (node, children) => // <- node reconstructor
({ ...node, children: children(node.path) }) // <- primary key
)
console.log(JSON.stringify(myTree, null, 2))
[
{
"location": 1,
"path": "4",
"children": [
{
"location": 2,
"path": "4.1",
"children": [
{
"location": 3,
"path": "4.1.1",
"children": []
},
{
"location": 4,
"path": "4.1.2",
"children": []
}
]
},
{
"location": 5,
"path": "4.2",
"children": [
{
"location": 6,
"path": "4.2.1",
"children": []
}
]
},
{
"location": 7,
"path": "4.3",
"children": [
{
"location": 8,
"path": "4.3.1",
"children": []
}
]
}
]
}
]
The Tree module is shared in this post and here's a peek at the Func module that supplies bind -
// Func.js
const identity = x => x
const bind = (f, ...args) =>
f(...args)
const raise = (msg = "") => // functional throw
{ throw Error(msg) }
// ...
export { identity, bind, raise, ... }
Expand the snippet below to verify the results in your browser -
// Func.js
const bind = (f, ...args) =>
f(...args)
// Index.js
const empty = _ =>
new Map
const update = (r, k, t) =>
r.set(k, t(r.get(k)))
const append = (r, k, v) =>
update(r, k, (all = []) => [...all, v])
const index = (all = [], indexer) =>
all.reduce
( (r, v) => append(r, indexer(v), v)
, empty()
)
// Tree.js
// import { index } from './Index'
function tree (all, indexer, maker, root = null)
{ const cache =
index(all, indexer)
const many = (all = []) =>
all.map(x => one(x))
const one = (single) =>
maker(single, next => many(cache.get(next)))
return many(cache.get(root))
}
// Main.js
// import { tree } from './Tree'
// import { bind } from './Func'
const parent = (path = "") =>
bind
( (pos = path.lastIndexOf(".")) =>
pos === -1
? null
: path.substr(0, pos)
)
const list =
[{location:1,path:'4'},{location:2,path:'4.1'},{location:3,path:'4.1.1'},{location:4,path:'4.1.2'},{location:5,path:'4.2'},{location:6,path:'4.2.1'},{location:7,path:'4.3'},{location:8,path:'4.3.1'}]
const myTree =
tree
( list // <- array of nodes
, node => parent(node.path) // <- foreign key
, (node, children) => // <- node reconstructor
({ ...node, children: children(node.path) }) // <- primary key
)
console.log(JSON.stringify(myTree, null, 2))
One way to do this is to use an intermediate index mapping paths to objects, then folding your list into a structure by looking up each node and its parent in the index. If there is no parent, then we add it to the root object. In the end, we return the children of our root object. Here's some code for that:
const restructure = (list) => {
const index = list .reduce(
(a, {path, ...rest}) => ({...a, [path]: {path, ...rest}}),
{}
)
return list .reduce((root, {path}) => {
const node = index [path]
const parent = index [path .split('.') .slice(0, -1) .join('.')] || root
parent.children = [...(parent.children || []), node]
return root
}, {children: []}) .children
}
const list = [{location: 1, path: '4'}, {location: 2, path: '4.1' }, {location: 3, path: '4.1.1'}, {location: 4, path: '4.1.2'}, {location: 5, path: '4.2'}, {location: 6, path: '4.2.1'}, {location: 7, path: '4.3'}, {location: 8, path: '4.3.1'}]
console.log (restructure (list))
.as-console-wrapper {min-height: 100% !important; top: 0}
Using the index means that we don't have to sort anything; the input can be in any order.
Finding the parent involves replacing, for instance, "4.3.1" with "4.3" and looking that up in the index. And when we try "4", it looks up the empty string, doesn't find it and uses the root node.
If you prefer regex, you could use this slightly shorter line instead:
const parent = index [path.replace (/(^|\.)[^.]+$/, '')] || root
But, you might also want to look at a more elegant technique in a recent answer on a similar question. My answer here, gets the job done (with a bit of ugly mutation) but that answer will teach you a lot about effective software development.
You can first sort the array of objects by path so that the parent will always be before it's children in the sorted array.
eg: '4' will be before '4.1'
Now, you can create an object where the keys are the paths. Let's assume '4' is already inserted in our object.
obj = {
'4': {
"location": 1,
"path": "4",
}
}
When we process '4.1', we first check if '4' is present in our object. If yes, we now go into its children (if the key 'children' isn't present, we create a new empty object) and check if '4.1' is present. If not, we insert '4.1'
obj = {
'4': {
"location": 1,
"path": "4",
"children": {
"4.1": {
"location": 2,
"path": "4.1"
}
}
}
}
We repeat this process for each element in list. Finally, we just have to recursively convert this object into an array of objects.
Final code:
list.sort(function(a, b) {
return a.path - b.path;
})
let obj = {}
list.forEach(x => {
let cur = obj;
for (let i = 0; i < x.path.length; i += 2) {
console.log(x.path.substring(0, i + 1))
if (x.path.substring(0, i + 1) in cur) {
cur = cur[x.path.substring(0, i + 1)]
if (!('children' in cur)) {
cur['children'] = {}
}
cur = cur['children']
} else {
break;
}
}
cur[x.path] = x;
})
function recurse (obj) {
let res = [];
Object.keys(obj).forEach((key) => {
if (obj[key]['children'] !== null && typeof obj[key]['children'] === 'object') {
obj[key]['children'] = recurse(obj[key]['children'])
}
res.push(obj[key])
})
return res;
}
console.log(recurse(obj));
was thinking along the same terms as Aadith but came up from an iterative approach. I think the most performant way to do it is to use a linked list structure and then flatten it though.
const list = [
{
location: 1,
path: '4'
},
{
location: 2,
path: '4.1'
},
{
location: 3,
path: '4.1.1'
},
{
location: 4,
path: '4.1.2'
},
{
location: 5,
path: '4.2'
},
{
location: 6,
path: '4.2.1'
},
{
location: 7,
path: '4.3'
},
{
location: 8,
path: '4.3.1'
}
];
let newList = [];
list.forEach((location) =>
{
console.log('Handling location ',location);
if(location.path.split('.').length==1)
{
location.children = [];
newList.push(location);
}
else
{
newList.forEach(loc => {
console.log('checking out: ',loc);
let found = false;
while(!found)
{
console.log(loc.path,'==',location.path.substring(0, location.path.lastIndexOf('.')));
found = loc.path == location.path.substring(0, location.path.lastIndexOf('.'));
if(!found)
{
for(let i=0;i<loc.children.length;i++)
{
let aloc = loc.children[i];
found = aloc.path == location.path.substring(0, location.path.lastIndexOf('.'));
if(found)
{
console.log('found it...', loc);
location.children = [];
aloc.children.push(location);
break;
}
}
}
else
{
console.log('found it...', loc);
location.children = [];
loc.children.push(location);
}
}
} );
}
});
console.log(newList);
This was my quick and dirty way of how to go about it
Thank you for all the suggestions!
I could definitely see really sophisticated solutions to my problem.
By the end of the day, I've ended up creating my own "dirty" solution.
It is definitely a slower approach, but for my application this list won't be long to the point where i should be too worry about optimization.
I had simplified the flatted list for the purpose of my question. Although, in reality the list was a little more complex then that. I could also pass the path I want to find its children.
This is the solution that worked for me.
function handleNested(list, location) {
return list.map((item) => {
if (item.children && item.children.length > 0) {
item.children = handleNested(item.children, location);
}
if (
location.path.startsWith(item.path) &&
location.path.split(".").length === item.path.split(".").length + 1
) {
return {
...item,
children: item.children ? [...item.children, location] : [location],
};
} else {
return item;
}
});
}
function locationList(path) {
// Filtering the list to remove items with different parent path, and sorting by path depthness.
const filteredList = list
.filter((location) => location.path.startsWith(path))
.sort((a, b) => a.path.length - b.path.length);
let nestedList = [];
// Looping through the filtered list and placing each item under its parent.
for (let i = 0; i < filteredList.length; i++) {
// Using a recursive function to find its parent.
let res = handleNested(nestedList, filteredList[i]);
nestedList = res;
}
return nestedList;
}
locationList("4");

Get reference to arbitrarily (deep) nested node inside JSON/JS object using value of key

I have been looking all night on SO with lots of similar issues but none that directly solves my problem at the moment. So please have a look below.
I have an object of the form:
let data = [{
"id": 777,
"name": "Level 1_section_1",
"children": [{
"id": 778,
"name": "Level 2a",
"children": [
]
},
{
"id": 783,
"name": "Level 2b",
"children": [
]
}
]
},
{
"id": 786,
"name": "Level 1_section_2",
"children": [{
"id": 781,
"name": "Level 2c",
"children": [
]
}]
}
]
Basically, children contains an array of the same structure nodes.
If I wish to get a reference to the node that contains, say, id:783, I would intuitively use recursion but I'm at a loss as to how I would ensure that it covers the entire tree recursively until it finds and returns the exact node that I want so that I could append more children to the found node.
Admittedly, despite coming from a CS background, my knowledge of recursion is rather rusty.
This is what I've tried in my jsfiddle:
https://jsfiddle.net/hanktrizz/surmf7dq/4/
Note that the data tree could be arbitrarily deep (though I don't expect it to go past 8 or 9 levels of depth) but just thought I'd point it out.
Here's one possibility, using a for loop in a recursive function:
let data=[{id:777,name:"Level 1_section_1",children:[{id:778,name:"Level 2a",children:[]},{id:783,name:"Level 2b",children:[]}]},{id:786,name:"Level 1_section_2",children:[{id:781,name:"Level 2c",children:[]}]}];
const findNode = (arr, idToFind) => {
for (const item of arr) {
if (item.id === idToFind) {
return item;
}
const possibleResult = findNode(item.children, idToFind);
if (possibleResult) {
return possibleResult;
}
}
};
console.log(findNode(data, 778));
Here's a higher-order findNode that is not limited to searching by id alone. Instead, it accepts a user-defined lambda to search nodes using any condition -
findNode (n => n.id === 778, data)
// { id: 778, name: "Level 2a" }
findNode (n => n.name === "Level 2c", data)
// { id: 781, name: "Level 2c" }
findNode (n => n.id === 999, data)
// undefined
Verify the results in your own browser below -
const data =
[{id:777,name:"Level 1_section_1",children:[{id:778,name:"Level 2a",children:[]},{id:783,name:"Level 2b",children:[]}]},{id:786,name:"Level 1_section_2",children:[{id:781,name:"Level 2c",children:[]}]}];
const None =
Symbol ()
// findNode : (node -> boolean, node array) -> node?
const findNode = (f, [ node = None, ...nodes ]) =>
node === None
? undefined
: find1 (f, node) || findNode (f, nodes)
// find1 : (node -> boolean, node) -> node?
const find1 = (f, node = {}) =>
f (node) === true
? node
: findNode (f, node.children)
console.log (findNode (n => n.id === 778, data))
// { id: 778, name: "Level 2a" }
console.log (findNode (n => n.name === "Level 2c", data))
// { id: 781, name: "Level 2c" }
console.log (findNode (n => n.id === 999, data))
// undefined
Above, destructing assignment permits an elegant expression but also creates unnecessary intermediate values. The following revision is a significant improvement -
// findNode : (node -> boolean, node array, int) -> node?
const findNode = (f, nodes = [], i = 0) =>
i >= nodes.length
? undefined
: find1 (f, nodes[i]) || findNode (f, nodes, i + 1)
// find1 : (node -> boolean, node) -> node?
const find1 = (f, node = {}) =>
f (node) === true
? node
: findNode (f, node.children)
Both version offer short-circuit evaluation and will stop iteration immediately after the first result is found
For fun, here's one that attempts to return all instances.
var data=[{id:777,name:"Level 1_section_1",children:[{id:778,name:"Level 2a",children:[]},{id:786,name:"Level 2b",children:[]}]},{id:786,name:"Level 1_section_2",children:[{id:781,name:"Level 2c",children:[]}]}]
var f = (o, pred, acc=[]) =>
pred(o) ? [o] : Object.values(o).reduce((a, b) =>
b && typeof b == 'object' ? a.concat(f(b, pred, acc)) : a, acc)
console.log(JSON.stringify(f(data, o => o.id == 781)))
console.log(JSON.stringify(f(data, o => o.id == 786)))
Here is an iterative solution using object-scan
Main advantage is that you get access to other data in filterFn and can easily do further processing. Obviously there is a trade-off in introducing a dependency
// const objectScan = require('object-scan');
const myData = [{ id: 777, name: 'Level 1_section_1', children: [{ id: 778, name: 'Level 2a', children: [] }, { id: 783, name: 'Level 2b', children: [] }] }, { id: 786, name: 'Level 1_section_2', children: [{ id: 781, name: 'Level 2c', children: [] }] }];
const treeSearch = (data, id) => objectScan(['**(^children$)'], {
useArraySelector: false,
abort: true,
rtn: 'value',
filterFn: ({ value }) => value.id === id
})(data);
console.log(treeSearch(myData, 778));
// => { id: 778, name: 'Level 2a', 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

Recursive tree search in a nested object structure in JavaScript

I'm trying to figure out how to search for a node in this JSON object recursively. I have tried something but cannot get it:
var tree = {
"id": 1,
"label": "A",
"child": [
{
"id": 2,
"label": "B",
"child": [
{
"id": 5,
"label": "E",
"child": []
},
{
"id": 6,
"label": "F",
"child": []
},
{
"id": 7,
"label": "G",
"child": []
}
]
},
{
"id": 3,
"label": "C",
"child": []
},
{
"id": 4,
"label": "D",
"child": [
{
"id": 8,
"label": "H",
"child": []
},
{
"id": 9,
"label": "I",
"child": []
}
]
}
]
};
Here is my non-working solution, which is probably because the first node is just a value while children are in arrays:
function scan(id, tree) {
if(tree.id == id) {
return tree.label;
}
if(tree.child == 0) {
return
}
return scan(tree.child);
};
Your code is just missing a loop to inspect each child of a node in the child array. This recursive function will return the label property of a node or undefined if label not present in tree:
const search = (tree, target) => {
if (tree.id === target) {
return tree.label;
}
for (const child of tree.child) {
const found = search(child, target);
if (found) {
return found;
}
}
};
const tree = {"id":1,"label":"A","child":[{"id":2,"label":"B","child":[{"id":5,"label":"E","child":[]},{"id":6,"label":"F","child":[]},{"id":7,"label":"G","child":[]}]},{"id":3,"label":"C","child":[]},{"id":4,"label":"D","child":[{"id":8,"label":"H","child":[]},{"id":9,"label":"I","child":[]}]}]};
console.log(search(tree, 1));
console.log(search(tree, 6));
console.log(search(tree, 99));
You can also do it iteratively with an explicit stack which won't cause a stack overflow (but note that the shorthand stack.push(...curr.child); can overflow the argument size for some JS engines due to the spread syntax, so use an explicit loop or concat for massive child arrays):
const search = (tree, target) => {
for (const stack = [tree]; stack.length;) {
const curr = stack.pop();
if (curr.id === target) {
return curr.label;
}
stack.push(...curr.child);
}
};
const tree = {"id":1,"label":"A","child":[{"id":2,"label":"B","child":[{"id":5,"label":"E","child":[]},{"id":6,"label":"F","child":[]},{"id":7,"label":"G","child":[]}]},{"id":3,"label":"C","child":[]},{"id":4,"label":"D","child":[{"id":8,"label":"H","child":[]},{"id":9,"label":"I","child":[]}]}]};
for (let i = 0; ++i < 12; console.log(search(tree, i)));
A somewhat more generic design would return the node itself and let the caller access the .label property if they want to, or use the object in some other manner.
Note that JSON is purely a string format for serialized (stringified, raw) data. Once you've deserialized JSON into a JavaScript object structure, as is here, it's no longer JSON.
scan can be written recursively using a third parameter that models a queue of nodes to scan
const scan = (id, tree = {}, queue = [ tree ]) =>
// if id matches node id, return node label
id === tree.id
? tree.label
// base case: queue is empty
// id was not found, return false
: queue.length === 0
? false
// inductive case: at least one node
// recur on next tree node, append node children to queue
: scan (id, queue[0], queue.slice(1).concat(queue[0].child))
Becauase JavaScript supports default arguments, the call site for scan is unaltered
console.log
( scan (1, tree) // "A"
, scan (3, tree) // "C"
, scan (9, tree) // "I"
, scan (99, tree) // false
)
Verify it works in your browser below
const scan = (id, tree = {}, queue = [ tree ]) =>
id === tree.id
? tree.label
: queue.length === 0
? false
: scan (id, queue[0], queue.slice(1).concat(queue[0].child))
const tree =
{ id: 1
, label: "A"
, child:
[ { id: 2
, label: "B"
, child:
[ { id: 5
, label: "E"
, child: []
}
, { id: 6
, label: "F"
, child: []
}
, { id: 7
, label: "G"
, child: []
}
]
}
, { id: 3
, label: "C"
, child: []
}
, { id: 4
, label: "D"
, child:
[ { id: 8
, label: "H"
, child: []
}
, { id: 9
, label: "I"
, child: []
}
]
}
]
}
console.log
( scan (1, tree) // "A"
, scan (3, tree) // "C"
, scan (9, tree) // "I"
, scan (99, tree) // false
)
Related recursive search using higher-order functions
Here is a solution using object-scan
// const objectScan = require('object-scan');
const tree = {"id":1,"label":"A","child":[{"id":2,"label":"B","child":[{"id":5,"label":"E","child":[]},{"id":6,"label":"F","child":[]},{"id":7,"label":"G","child":[]}]},{"id":3,"label":"C","child":[]},{"id":4,"label":"D","child":[{"id":8,"label":"H","child":[]},{"id":9,"label":"I","child":[]}]}]};
const search = (obj, id) => objectScan(['**.id'], {
abort: true,
filterFn: ({ value, parent, context }) => {
if (value === id) {
context.push(parent.label);
return true;
}
return false;
}
})(obj, [])[0];
console.log(search(tree, 1));
// => A
console.log(search(tree, 6));
// => F
console.log(search(tree, 99));
// => undefined
.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

How do I recursively use Array.prototype.find() while returning a single object?

The bigger problem I am trying to solve is, given this data:
var data = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4, children: [
{ id: 6 },
{ id: 7, children: [
{id: 8 },
{id: 9 }
]}
]},
{ id: 5 }
]
I want to make a function findById(data, id) that returns { id: id }. For example, findById(data, 8) should return { id: 8 }, and findById(data, 4) should return { id: 4, children: [...] }.
To implement this, I used Array.prototype.find recursively, but ran into trouble when the return keeps mashing the objects together. My implementation returns the path to the specific object.
For example, when I used findById(data, 8), it returns the path to { id: 8 }:
{ id: 4, children: [ { id: 6 }, { id: 7, children: [ { id: 8}, { id: 9] } ] }
Instead I would like it to simply return
{ id: 8 }
Implementation (Node.js v4.0.0)
jsfiddle
var data = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4, children: [
{ id: 6 },
{ id: 7, children: [
{id: 8 },
{id: 9 }
]}
]},
{ id: 5 }
]
function findById(arr, id) {
return arr.find(a => {
if (a.children && a.children.length > 0) {
return a.id === id ? true : findById(a.children, id)
} else {
return a.id === id
}
})
return a
}
console.log(findById(data, 8)) // Should return { id: 8 }
// Instead it returns the "path" block: (to reach 8, you go 4->7->8)
//
// { id: 4,
// children: [ { id: 6 }, { id: 7, children: [ {id: 8}, {id: 9] } ] }
The problem what you have, is the bubbling of the find. If the id is found inside the nested structure, the callback tries to returns the element, which is interpreted as true, the value for the find.
The find method executes the callback function once for each element present in the array until it finds one where callback returns a true value. [MDN]
Instead of find, I would suggest to use a recursive style for the search with a short circuit if found.
var data = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4, children: [{ id: 6 }, { id: 7, children: [{ id: 8 }, { id: 9 }] }] }, { id: 5 }];
function findById(data, id) {
function iter(a) {
if (a.id === id) {
result = a;
return true;
}
return Array.isArray(a.children) && a.children.some(iter);
}
var result;
data.some(iter);
return result
}
console.log(findById(data, 8));
Let's consider the implementation based on recursive calls:
function findById(tree, nodeId) {
for (let node of tree) {
if (node.id === nodeId) return node
if (node.children) {
let desiredNode = findById(node.children, nodeId)
if (desiredNode) return desiredNode
}
}
return false
}
Usage
var data = [
{ id: 1 }, { id: 2 }, { id: 3 },
{ id: 4, children: [
{ id: 6 },
{ id: 7,
children: [
{ id: 8 },
{ id: 9 }
]}]},
{ id: 5 }
]
findById(data, 7 ) // {id: 7, children: [{id: 8}, {id: 9}]}
findById(data, 5 ) // {id: 5}
findById(data, 9 ) // {id: 9}
findById(data, 11) // false
To simplify the picture, imagine that:
you are the monkey sitting on the top of a palm tree;
and searching for a ripe banana, going down the tree
you are in the end and searches aren't satisfied you;
come back to the top of the tree and start again from the next branch;
if you tried all bananas on the tree and no one is satisfied you, you just assert that ripe bananas don't grow on this this palm;
but if the banana was found you come back to the top and get pleasure of eating it.
Now let's try apply it to our recursive algorithm:
Start iteration from the top nodes (from the top of the tree);
Return the node if it was found in the iteration (if a banana is ripe);
Go deep until item is found or there will be nothing to deep. Hold the result of searches to the variable (hold the result of searches whether it is banana or just nothing and come back to the top);
Return the searches result variable if it contains the desired node (eat the banana if it is your find, otherwise just remember not to come back down by this branch);
Keep iteration if node wasn't found (if banana wasn't found keep testing other branches);
Return false if after all iterations the desired node wasn't found (assert that ripe bananas doesn't grow on this tree).
Keep learning recursion it seems not easy at the first time, but this technique allows you to solve daily issues in elegant way.
I would just use a regular loop and recursive style search:
function findById(data, id) {
for(var i = 0; i < data.length; i++) {
if (data[i].id === id) {
return data[i];
} else if (data[i].children && data[i].children.length && typeof data[i].children === "object") {
findById(data[i].children, id);
}
}
}
//findById(data, 4) => Object {id: 4, children: Array[2]}
//findById(data, 8) => Object {id: 8}
I know this is an old question, but as another answer recently revived it, I'll another version into the mix.
I would separate out the tree traversal and testing from the actual predicate that we want to test with. I believe that this makes for much cleaner code.
A reduce-based solution could look like this:
const nestedFind = (pred) => (xs) =>
xs .reduce (
(res, x) => res ? res : pred(x) ? x : nestedFind (pred) (x.children || []),
undefined
)
const findById = (testId) =>
nestedFind (({id}) => id == testId)
const data = [{id: 1}, {id: 2}, {id: 3}, {id: 4, children: [{id: 6}, {id: 7, children: [{id: 8}, {id: 9}]}]}, {id: 5}]
console .log (findById (8) (data))
console .log (findById (4) (data))
console .log (findById (42) (data))
.as-console-wrapper {min-height: 100% !important; top: 0}
There are ways we could replace that reduce with an iteration on our main list. Something like this would do the same:
const nestedFind = (pred) => ([x = undefined, ...xs]) =>
x == undefined
? undefined
: pred (x)
? x
: nestedFind (pred) (x.children || []) || nestedFind (pred) (xs)
And we could make that tail-recursive without much effort.
While we could fold the two functions into one in either of these, and achieve shorter code, I think the flexibility offered by nestedFind will make other similar problems easier. However, if you're interested, the first one might look like this:
const findById = (id) => (xs) =>
xs .reduce (
(res, x) => res ? res : x.id === id ? x : findById (id) (x.children || []),
undefined
)
const data = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{
id: 4,
children: [{ id: 6 }, { id: 7, children: [{ id: 8 }, { id: 9 }] }]
},
{ id: 5 }
];
// use Array.flatMap() and Optional chaining to find children
// then Filter undefined results
const findById = (id) => (arr) => {
if (!arr.length) return null;
return (
arr.find((obj) => obj.id === id) ||
findById(id)(arr.flatMap((el) => el?.children).filter(Boolean))
);
};
const findId = (id) => findById(id)(data);
console.log(findId(12)); /* null */
console.log(findId(8)); /* { id: 8 } */
Based on Purkhalo Alex solution,
I have made a modification to his function to be able to find the ID recursively based on a given dynamic property and returning whether the value you want to find or an array of indexes to recursively reach to the object or property afterwards.
This is like find and findIndex together through arrays of objects with nested arrays of objects in a given property.
findByIdRecursive(tree, nodeId, prop = '', byIndex = false, arr = []) {
for (let [index, node] of tree.entries()) {
if (node.id === nodeId) return byIndex ? [...arr, index] : node;
if (prop.length && node[prop].length) {
let found = this.findByIdRecursive(node[prop], nodeId, prop, byIndex, [
...arr,
index
]);
if (found) return found;
}
}
return false;
}
Now you can control the property and the type of finding and get the proper result.
This can be solved with reduce.
const foundItem = data.reduce(findById(8), null)
function findById (id) {
const searchFunc = (found, item) => {
const children = item.children || []
return found || (item.id === id ? item : children.reduce(searchFunc, null))
}
return searchFunc
}
You can recursively use Array.prototype.find() in combination with Array.prototype.flatMap()
const findById = (a, id, p = "children", u) =>
a.length ? a.find(o => o.id === id) || findById(a.flatMap(o => o[p] || []), id) : u;
const tree = [{id:1}, {id:2}, {id:3}, {id:4, children:[{id: 6}, {id:7, children:[{id:8}, {id:9}]}]}, {id:5}];
console.log(findById(tree, 9)); // {id:9}
console.log(findById(tree, 10)); // undefined
If one wanted to use Array.prototype.find this is the option I chose:
findById( my_big_array, id ) {
var result;
function recursiveFind( haystack_array, needle_id ) {
return haystack_array.find( element => {
if ( !Array.isArray( element ) ) {
if( element.id === needle_id ) {
result = element;
return true;
}
} else {
return recursiveFind( element, needle_id );
}
} );
}
recursiveFind( my_big_array, id );
return result;
}
You need the result variable, because without it, the function would return the top level element in the array that contains the result, instead of a reference to the deeply nested object containing the matching id, meaning you would need to then filter it out further.
Upon looking through the other answers, my approach seems very similar to Nina Scholz's but instead uses find() instead of some().
Here is a solution that is not the shortest, but divides the problem into recursive iteration and finding an item in an iterable (not necessarily an array).
You could define two generic functions:
deepIterator: a generator that traverses a forest in pre-order fashion
iFind: a finder, like Array#find, but that works on an iterable
function * deepIterator(iterable, children="children") {
if (!iterable?.[Symbol.iterator]) return;
for (let item of iterable) {
yield item;
yield * deepIterator(item?.[children], children);
}
}
function iFind(iterator, callback, thisArg) {
for (let item of iterator) if (callback.call(thisArg, item)) return item;
}
// Demo
var data = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4, children: [{ id: 6 }, { id: 7, children: [{ id: 8 }, { id: 9 }] }] }, { id: 5 }];
console.log(iFind(deepIterator(data), ({id}) => id === 8));
In my opinion, if you want to search recursively by id, it is better to use an algorithm like this one:
function findById(data, id, prop = 'children', defaultValue = null) {
for (const item of data) {
if (item.id === id) {
return item;
}
if (Array.isArray(item[prop]) && item[prop].length) {
const element = this.findById(item[prop], id, prop, defaultValue);
if (element) {
return element;
}
}
}
return defaultValue;
}
findById(data, 2);
But I strongly suggest using a more flexible function, which can search by any key-value pair/pairs:
function findRecursive(data, keyvalues, prop = 'children', defaultValue = null, _keys = null) {
const keys = _keys || Object.keys(keyvalues);
for (const item of data) {
if (keys.every(key => item[key] === keyvalues[key])) {
return item;
}
if (Array.isArray(item[prop]) && item[prop].length) {
const element = this.findRecursive(item[prop], keyvalues, prop, defaultValue, keys);
if (element) {
return element;
}
}
}
return defaultValue;
}
findRecursive(data, {id: 2});
you can use this function:
If it finds the item so the item returns. But if it doesn't find the item, tries to find the item in sublist.
list: the main/root list
keyName: the key that you need to find the result up to it for example 'id'
keyValue: the value that must be searched
subListName: the name of 'child' array
callback: your callback function which you want to execute when item is found
function recursiveSearch(
list,
keyName = 'id',
keyValue,
subListName = 'children',
callback
) {
for (let i = 0; i < list.length; i++) {
const x = list[i]
if (x[keyName] === keyValue) {
if (callback) {
callback(list, keyName, keyValue, subListName, i)
}
return x
}
if (x[subListName] && x[subListName].length > 0) {
const item = this.recursiveSearch(
x[subListName],
keyName,
keyValue,
subListName,
callback
)
if (!item) continue
return item
}
}
},
Roko C. Buljan's solution, but more readable one:
function findById(data, id, prop = 'children', defaultValue = null) {
if (!data.length) {
return defaultValue;
}
return (
data.find(el => el.id === id) ||
findById(
data.flatMap(el => el[prop] || []),
id
)
);
}

Categories

Resources