Deep Find, Remove, Update in a hierarchical array - javascript

Let's assume we have a tree like:
const items = [
{
"id": 1
},
{
"id": 2,
"items": [
{
"id": 3
},
{
"id": 4,
"items": []
}
]
}
];
I'm looking for an efficient way to remove/update an item by its unique id; the main issue is we do not know the exact path of item, and worse: the tree may have n levels, and what we have is just an id.

delete a node and its descendants
Here's an effective technique using flatMap and mutual recursion -
del accepts an array of nodes, t, a query, q, and calls del1 on each node with the query
del1 accepts a single node, t, a query, q, and calls del on a node's items
const del = (t = [], q = null) =>
t.flatMap(v => del1(v, q)) // <-- 1
const del1 = (t = {}, q = null) =>
q == t.id
? []
: { ...t, items: del(t.items, q) } // <-- 2
const data =
[{id:1},{id:2,items:[{id:3},{id:4,items:[]}]}]
const result =
del(data, 3)
console.log(result)
In the output, we see node.id 3 is removed -
[
{
"id": 1,
"items": []
},
{
"id": 2,
"items": [
{
"id": 4,
"items": []
}
]
}
]
What if we only want to delete the parent but somehow keep the parent's items in the tree? Is there a way to write it without mutual recursion? Learn more about this technique in this related Q&A.
everything is reduce
If you know filter but haven't used flatmap yet, you might be surprised to know that -
filter is a specialised form of flatmap
flatmap is a specialised form of reduce
const identity = x =>
x
const filter = (t = [], f = identity) =>
flatmap // 1
( t
, v => f(v) ? [v] : []
)
const flatmap = (t = [], f = identity) =>
t.reduce // 2
( (r, v) => r.concat(f(v))
, []
)
const input =
[ "sam", "alex", "mary", "rat", "jo", "wren" ]
const result =
filter(input, name => name.length > 3)
console.log(result)
// [ "alex", "mary", "wren" ]

Related

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

Merge arrays from different objects with same key

I have the following code:
const blueData = {
"items": [
{
"id": 35,
"revision": 1,
"updatedAt": "2021-09-10T14:29:54.595012Z",
},
]
}
const redData = {}
const greenData = {
"items": [
{
"id": 36,
"revision": 1,
"updatedAt": "2021-09-10T14:31:07.164368Z",
}
]
}
let colorData = []
colorData = blueData.items ? [colorData, ...blueData.items] : colorData
colorData = redData.items ? [colorData, ...redData.items] : colorData
colorData = greenData.items ? [colorData, ...greenData.items] : colorData
I am guessing the spread operator is not the right approache here as I'm getting some extra arrays in my final colorData array. I simply want to build a single array of 'items' that contains all of the 'items' from the 3 objects.
Here's a link to that code in es6 console: https://es6console.com/ktkhc3j2/
Put your data into an array then use flatMap to unwrap each .items:
[greenData, redData, blueData].flatMap(d => d.items ?? [])
//=> [ {id: 36, revision: 1, updatedAt: '2021-09-10T14:31:07.164368Z'}
//=> , {id: 35, revision: 1, updatedAt: '2021-09-10T14:29:54.595012Z'}]
If you fancy you could abstract d => d.items ?? [] with a bit of curry (no pun intended ;)
const take = k => o => o[k] ?? [];
Which gives us:
[greenData, redData, blueData].flatMap(take('items'))
We can even go a step further if you ever need to repeat this process with different keys:
const concatBy = fn => xs => xs.flatMap(x => fn(x));
Now it almost feels like you're expressing your intent with words instead of code:
const takeItems = concatBy(take('items'));
takeItems([greenData, redData, blueData]);
//=> [ {id: 36, revision: 1, updatedAt: '2021-09-10T14:31:07.164368Z'}
//=> , {id: 35, revision: 1, updatedAt: '2021-09-
Let's build another function:
const takeFood = concatBy(take('food'));
takeFood([{food: ['🥑', '🥕']}, {food: ['🌽', '🥦']}]);
//=> ['🥑', '🥕', '🌽', '🥦']
Addendum
This is only meant as a potentially useful learning material. My advice is to use flatMap.
This:
[[1, 2], [3, 4]].flatMap(x => x)
//=> [1, 2, 3, 4]
Can also be expressed with reduce. Slightly more verbose but does what it says on the tin:
[[1, 2], [3, 4]].reduce((xs, x) => xs.concat(x), [])
//=> [1, 2, 3, 4]
So to put it simply you could also do:
[greenData, redData, blueData].reduce((xs, x) => xs.concat(x.items ?? []), [])
You can do this using the Logical OR operator which lets you provide a default value if the items field is missing.
const blueData = { items: [ { id: 35, revision: 1, updatedAt: '2021-09-10T14:29:54.595012Z', }, ], };
const redData = {};
const greenData = { items: [ { id: 36, revision: 1, updatedAt: '2021-09-10T14:31:07.164368Z', }, ], };
const colorData = [
...(blueData.items || []),
...(redData.items || []),
...(greenData.items || []),
];
console.log(colorData);
Maybe I'm a little old-fashioned but I'd use concat for that:
The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat
const blueData = {
"items": [
{
"id": 35,
"revision": 1,
"updatedAt": "2021-09-10T14:29:54.595012Z",
},
]
}
const redData = {}
const greenData = {
"items": [
{
"id": 36,
"revision": 1,
"updatedAt": "2021-09-10T14:31:07.164368Z",
}
]
}
const colorData = [].concat(blueData.items,redData.items,greenData.items).filter(x => x)
console.log(colorData)
the last filter is for removing undefined values
Like this?
colorData = blueData.items ? [...colorData, ...blueData.items] : colorData
colorData = redData.items ? [...colorData, ...redData.items] : colorData
colorData = greenData.items ? [...colorData, ...greenData.items] : colorData
Output:
[{"id":35,"revision":1,"updatedAt":"2021-09-10T14:29:54.595012Z"},
{"id":36,"revision":1,"updatedAt":"2021-09-10T14:31:07.164368Z"}]
I think you need to add the spread operator also to the colorData array, because if not you are adding the colorData array itself, not its items.
If you want the simplest solution, you can iterate with a for-loop between all arrays. Create a temporary array that will store data found on each index. This is the fastest and the most flexible solution.
var x1 = {
"items": [
{ "testKey1": "testVal" }
]
};
var x2 = {
"items": [
{ "testKey2.0": "testVal2" },
{ "testKey2.1": "testVal2" },
{ "testKey2.2": "testVal2" },
]
};
var x3 = {
"items": [
{ "testKey3.0": "testVal3" },
{ "testKey3.1": "testVal3" }
]
};
function combineArrays(...arrays) {
var tempArray = [];
for (let index in arrays) {
let currentArray = arrays[index];
for (let innerArrayIndex in currentArray) {
tempArray.push(currentArray[innerArrayIndex]);
}
}
return tempArray;
}
var result = combineArrays(x1.items, x2.items, x3.items);
console.log(result);
The solutions using a spread operator do not take into consideration that all the objects will be cloned using a shallow copy. Have a look.

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

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

Categories

Resources