Recursive tree search in a nested object structure in JavaScript - 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

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

Get all parents for object in nested array JS

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 };
})
);
}

creating a parent / child json object from a large array of path data in javascript

I have a Javascript array of the form:
var array = [Company_stock_content\\LightRhythmVisuals\\LRV_HD", "Big Media Test\\ArtificiallyAwake\\AA_HD", "Big Media Test\\Company\\TestCards_3840x2160\\TestCards_1920x1080",...]
I need to construct a JSON object of the form:
[
{
"data" : "parent",
"children" : [
{
"data" : "child1",
"children" : [ ]
}
]
}
]
so for each top level node in the array it can have multiple children that all have children.
for example if we take the array snipper provided,
the corresponding JSON object would look as such
[{
"data": "Company_stock_content",
"children": [{
"data": "LightRhythmVisuals",
"children": [{
"data": "LRV_HD"
}]
}]
}, {
"data": "Big Media Test",
"children": [{
"data": "ArtificiallyAwake",
"children": [{
"data": "AA_HD"
}]
}, {
"data": "Company",
"children": [{
"data": "TestCards_3840x2160"
}]
}]
}]
How can I this structure from the given data bearing in mind the original array can have tens of thousands of entries?
Here's a way to convert lineages into a hierarchy. A simple guard against cycles is to keep a parent pointer and take a lazy approach when setting parent-child relations (if conflicting relations are described, the last one wins).
Consider the lineages:
["A\\E", "A\\F", "B\\G\\I", "B\\G\\J", "C\\H"]
Describing this tree:
A B C
| | |
E F G H
|
I J
const array = ["A\\E", "A\\F", "B\\G\\I", "B\\G\\J", "C\\H"];
const lineages = array.map(string => string.split("\\"));
let nodes = {}
function createParentChild(parentName, childName) {
const nodeNamed = name => {
if (nodes[name]) return nodes[name];
nodes[name] = { name, children: [], closed: false };
return nodes[name];
};
let parent = nodeNamed(parentName)
let child = nodeNamed(childName)
if (child.parent) {
// if this child already has a parent, we have an ill-formed input
// fix by undoing the existing parent relation, remove the child from its current parent
child.parent.children = child.parent.children.filter(c => c.name !== childName);
}
child.parent = parent
if (!parent.children.includes(child))
parent.children.push(child);
}
lineages.forEach(lineage => {
for (i=0; i<lineage.length-1; i++) {
createParentChild(lineage[i], lineage[i+1]);
}
})
let roots = Object.values(nodes).filter(node => !node.parent)
Object.values(nodes).forEach(node => delete node.parent)
console.log(roots)
This approach also works for the (maybe pathological) input like this
// Here, "F" appears as a descendant of "A" and "B"
["A\\E", "A\\F", "B\\G\\I", "B\\G\\F", "C\\H"]
Last one wins:
A B C
| | |
E G H
|
I F
Something like this:
const array = ["Company_stock_content\\LightRhythmVisuals\\LRV_HD", "Big Media Test\\ArtificiallyAwake\\AA_HD", "Big Media Test\\Company\\TestCards_3840x2160\\TestCards_1920x1080"];
const result = array.reduce((acc, item) => {
item.split('\\').forEach((entry, index, splits) => {
acc[entry] = acc[entry] || { data: entry, children: [] };
if (index > 0) {
if (!acc[splits[index-1]].children.some(child => child.data === entry)) {
acc[splits[index-1]].children.push(acc[entry]);
}
} else {
if (!acc.children.some(root => root.data === entry)) {
acc.children.push(acc[entry]);
}
}
});
return acc;
}, { children: []}).children;
console.log(result);

How to find Object in Array which is nested as unknown times [duplicate]

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

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");

Categories

Resources