Creating nested array from flat array - Data Structure - javascript

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

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

Recursively Add Elements To Array Based on Length

I'm trying to create a recursive function that will return a list of strings representing every attribute for a given schema. It must merge this with the attributes for a given document, to include elements for each time an array element occurs in the document.
For example. If you run the following code, I expect the result to include friends.1.addresses.1.country. However it does not. I think this is due to the fact that it's not recursively including all the array element possibilities. Since only 0 is set for the other array possibilities in the parentKey & postKey variables.
Any ideas how to fix this?
const schemaAttributes = [
'id',
'friends',
'friends.0',
'friends.0.name',
'friends.0.addresses',
'friends.0.addresses.0',
'friends.0.addresses.0.country',
'friends.0.addresses.0.zip'
];
const myDocument = {
"id": 1,
"friends": [
{
"name": "Bob",
"addresses": [
{"country": "world"}
]
},
{
"name": "Tim",
"addresses": [
{"country": "moon"},
{"zip": 12345}
]
}
]
};
console.log(main()); // Should print `friends.1.addresses.1.country` as one of the elements in the array but does not.
function main() {
const result = schemaAttributes.reduce((accumulator, currentValue) => {
accumulator.push(currentValue);
const attributeParts = currentValue.split(".");
attributeParts.forEach((a, index) => {
const isLastPartNumber = !isNaN(parseInt(a));
if (isLastPartNumber) {
const parentKey = attributeParts.slice(0, index).join(".");
const postKey = attributeParts.slice(index + 1).join(".");
const numberOfItems = get(myDocument, parentKey).length;
for (let i = 1; i < numberOfItems; i++) {
accumulator.push([parentKey, i, postKey].filter((a) => Boolean(a)).join("."));
}
}
});
return accumulator;
}, []);
return [...new Set(result)];
}
function get(object, key) {
const keyParts = key.split(".");
let returnValue = object;
keyParts.forEach((part) => {
if (returnValue) {
returnValue = returnValue[part];
}
});
return returnValue;
}
Expected Result (order does not matter):
[
"id",
"friends",
"friends.0",
"friends.1",
"friends.0.name",
"friends.1.name",
"friends.0.addresses",
"friends.1.addresses",
"friends.0.addresses.0",
"friends.1.addresses.0",
"friends.1.addresses.1",
"friends.0.addresses.0.country",
"friends.1.addresses.0.country",
"friends.1.addresses.1.country",
"friends.0.addresses.0.zip",
"friends.1.addresses.0.zip",
"friends.1.addresses.1.zip"
]
Below traversing all paths matching your schemaAttribute, assuming a zero in your schemaAttribute is a wildcard for the position in array.
const schemaAttributes = [
'id',
'friends',
'friends.0',
'friends.0.name',
'friends.0.addresses',
'friends.0.addresses.0',
'friends.0.addresses.0.country',
'friends.0.addresses.0.zip'
];
const myDocument = {
"id": 1,
"friends": [
{
"name": "Bob",
"addresses": [
{"country": "world"}
]
},
{
"name": "Tim",
"addresses": [
{"country": "moon"},
{"zip": 12345}
]
}
]
};
const out = new Set()
schemaAttributes.forEach(attr => {
out.add(attr)
traverse(myDocument, attr.split('.'), 0, [], path => out.add(path.join('.')))
})
function traverse (node, attrPath, idxAttrPath, outPath, cb) {
if (idxAttrPath === attrPath.length) {
return cb(outPath)
}
if (!node) { // can not explore further
return
}
const attr = attrPath[idxAttrPath]
if (attr === '0') {
if (!Array.isArray(node)) { // can not explore further
return
}
node.forEach((n, i) => {
outPath.push(i)
traverse(node[i], attrPath, idxAttrPath + 1, outPath, cb)
outPath.pop()
})
} else {
outPath.push(attr)
traverse(node[attr], attrPath, idxAttrPath + 1, outPath, cb)
outPath.pop()
}
}
console.log('out', [...out].sort((a, b) => a.localeCompare(b)))
An alternative (in the spirit more efficient) would be to consider a trie so we don't explore every schemaAttribute from the start. A 'nice' property being that the fields are printed in order and we don't have to sort as done in the first approach (although it does not matter to you)
Note that the traverse function is almost identical
Note2: Notice the cb(outPath) done for every traversal, not only for leaves.
const schemaAttributes = [
'id',
'friends',
'friends.0',
'friends.0.name',
'friends.0.addresses',
'friends.0.addresses.0',
'friends.0.addresses.0.country',
'friends.0.addresses.0.zip'
];
const myDocument = {
"id": 1,
"friends": [
{
"name": "Bob",
"addresses": [
{"country": "world"}
]
},
{
"name": "Tim",
"addresses": [
{"country": "moon"},
{"zip": 12345}
]
}
]
};
function f2 (schemaAttributes, doc) {
// build a tree out of schema attributes
const root = {}
schemaAttributes.forEach(sa => {
node = root
sa.split('.').forEach(attr => {
node[attr] = node[attr] || {}
node = node[attr]
})
})
// explore the tree
function traverse (node, treeNode, outPath, cb) {
cb(outPath)
if (Object.keys(treeNode).length === 0) { // a leaf
return // cb(outPath)
}
if (!node) {
return
}
Object.keys(treeNode).forEach(attr => {
if (attr === '0') {
if (!Array.isArray(node)) { // can not explore further
return
}
node.forEach((n, i) => {
outPath.push(i)
traverse(node[i], treeNode[attr], outPath, cb)
outPath.pop()
})
} else {
outPath.push(attr)
traverse(node[attr], treeNode[attr], outPath, cb)
outPath.pop()
}
})
}
const out = []
traverse(doc, root, [], p => out.push(p.join('.')))
return out.slice(1) // discard the empty string
}
console.log(f2(schemaAttributes, myDocument))
Regarding the presence of friends.2.addresses.0.zip the underlying idea is that the path to the leaf should be present even if path in the document is at some point undefined.
So the adaptation is to fake the path on the document so we can continue traversing it up until the tree leaf is reached
const schemaAttributes = [
'id',
'friends',
'friends.0',
'friends.0.name',
'friends.0.addresses',
'friends.0.addresses.0',
'friends.0.addresses.0.country',
'friends.0.addresses.0.zip',
'bob.moran.everywhere' // for properties as well
];
const myDocument = {
"id": 1,
"friends": [
{
"name": "Bob",
"addresses": [
{"country": "world"}
]
},
{
"name": "Tim",
"addresses": [
{"country": "moon"},
{"zip": 12345}
]
},
{
"name": "Pumba",
"addresses": [] // empty addresses! should show ..friends.2.adresses.0....
}
]
};
function f3 (schemaAttributes, doc) {
// build a tree out of schema attributes
const root = {}
schemaAttributes.forEach(sa => {
node = root
sa.split('.').forEach(attr => {
node[attr] = node[attr] || {}
node = node[attr]
})
})
// explore the tree
function traverse (node, treeNode, outPath, cb, virtualPath) {
cb(outPath)
if (Object.keys(treeNode).length === 0) { // a leaf
return //cb(outPath)
}
Object.keys(treeNode).forEach(attr => {
if (attr === '0') {
if (!node || node.length == 0) {
node = [{}] // fake the path for arrays
}
node.forEach((n, i) => {
outPath.push(i)
traverse(node[i], treeNode[attr], outPath, cb)
outPath.pop()
})
} else {
if (!node) { // fake the path for properties
node = {}
}
outPath.push(attr)
traverse(node[attr], treeNode[attr], outPath, cb)
outPath.pop()
}
})
}
const out = []
traverse(doc, root, [], p => out.push(p.join('.')))
return out.slice(1)
}
console.log(f3(schemaAttributes, myDocument))
You could use try out this piece of code:
const schemaAttributes = [
'id',
'friends',
'friends.0',
'friends.0.name',
'friends.0.addresses',
'friends.0.addresses.0',
'friends.0.addresses.0.country',
'friends.0.addresses.0.zip'
];
const myDocument = {
"id": 1,
"friends": [
{
"name": "Bob",
"addresses": [
{"country": "world"}
]
},
{
"name": "Tim",
"addresses": [
{"country": "moon"},
{"zip": 12345}
]
}
]
};
main(); // Should print `friends.1.addresses.1.country` as one of the elements in the array but does not.
function extractKeysRecursively(obj) {
const attributes = Object.keys(obj)
return attributes
flatMap(key =>
typeof obj[key] === 'object'
? [isNaN(key) && key, ...extractKeysRecursively(obj[key], ( ( parentKey && parentKey + '.' ) || '' ) + key )]
: ( ( parentKey && parentKey + '.' ) || '' ) + key
)
.filter(key => !!key)
}
function main() {
console.log(schemaAttributes.concat(extractKeysRecursively(myDocument)))
}
Lemme know you need some extra help (:

convert a flat json file to tree structure in javascript

I am basically trying to convert a flat json file to tree view. Here the parent child relationship required for tree view is mentained by links key using source and target.
Here is the sample raw input:
{
"nodes" : [
{
name: "bz_db",
index: 0
},
{
name: "mysql",
index: 1
},
{
name: "postgres",
index: 2
},
{
name: "it-infra",
index: 3
},
{
name: "user-count",
index: 4
}
],
links: [
{
source: 0, target: 1
},
{
source: 0, target: 3
},
{
source: 1, target: 3
},
{
source: 3, target: 4
}
]
}
As you can see the link field maintains this relation ship, and finally I want my data in this format:
{
name: "bz_db",
children: [
{
name: "mysql",
children: [
{
name: "it-infra",
children: [
{
name: "user_count",
children: []
}
]
}
]
},
{
name: "it-infra",
children: [{
name: "user_count",
children: []
}
]
}
]
}
I tried to solve this, but it worked for 1 level (to show immediate child of a selected root element.
var findObjectByKeyValue = function(arrayOfObject, key, value){
return _.find(arrayOfObject, function(o){ return o[key] == value})
}
var rootObject = findObjectByKeyValue(sample_raw_input.nodes, 'name', 'bz_db');
var treeObject = {
name: rootObject.name,
index: rootObject.index,
root: true,
children: []
};
angular.forEach(dependencyData.links, function(eachLink){
if(treeObject.index == eachLink.source){
var rawChildObject = findObjectByKeyValue(dependencyData.nodes, 'index', eachLink.target);
var childObject = {};
childObject.index = rawChildObject.index;
childObject.name = rawChildObject.name;
childObject.children = [];
treeObject.children.push(childObject);
}
});
But the above code returns me only first level of depndencies, but i want hierarchical relationship.
I know i can use recursion here. But I am not so comfortable with it.
Josh's answer uses a sequence of map->filter->map->find calls, each of which iterate thru a collection of data. This loop of loop of loop of loops results in a stunning amount of computational complexity as the number of nodes in your collection increases.
You can dramatically simplify the creation of the tree by using a single reduce pass on each nodes and links. Map can also perform look-ups in logarithmic time, compared to Array's find which requires linear time (slower). When you consider this operation is called for each element of the input, it's clear to see a significant difference in time.
const makeTree = (nodes = [], links = []) =>
links.reduce
( (t, l) =>
t.set ( l.source
, MutableNode.push ( t.get (l.source)
, t.get (l.target)
)
)
, nodes.reduce
( (t, n) => t.set (n.index, MutableNode (n.name))
, new Map
)
)
.get (0)
Lastly, we provide the MutableNode interface we relied upon
const MutableNode = (name, children = []) =>
({ name, children })
MutableNode.push = (node, child) =>
(node.children.push (child), node)
Below is a full program demonstration. JSON.stringify is used only to display the result
const MutableNode = (name, children = []) =>
({ name, children })
MutableNode.push = (node, child) =>
(node.children.push (child), node)
const makeTree = (nodes = [], links = []) =>
links.reduce
( (t, l) =>
t.set ( l.source
, MutableNode.push ( t.get (l.source)
, t.get (l.target)
)
)
, nodes.reduce
( (t, n) => t.set (n.index, MutableNode (n.name))
, new Map
)
)
.get (0)
const data =
{ nodes:
[ { name: "bz_db", index: 0 }
, { name: "mysql", index: 1 }
, { name: "postgres", index: 2 }
, { name: "it-infra", index: 3 }
, { name: "user-count", index: 4 }
]
, links:
[ { source: 0, target: 1 }
, { source: 0, target: 3 }
, { source: 1, target: 3 }
, { source: 3, target: 4 }
]
}
const tree =
makeTree (data.nodes, data.links)
console.log (JSON.stringify (tree, null, 2))
You can rely on tracking an object reference and do this without any recursion. Using Object.assign, map the list of nodes to its children:
// Assuming that input is in `input`
const nodes = input.nodes.reduce((a, node) => {
a[node.index] = { ...node, index: undefined };
return a;
}, []);
// organize the links by their source
const links = input.links.reduce((a, link) => {
return a.set((a.get(link.source) || []).concat(nodes[link.target]);
}, new Map());
// Apply side effect of updating node children
nodes.forEach(node => Object.assign(node, {
children: links.get(node.index),
}));
So I'm taking the list of nodes, and assigning to each (to mutate the node itself -- keep in mind this is a side-effect) a new array. Those children are all the links that link this node, and we Array#map them to convert their target ID into the actual node we want.
just share sample, a little different from yours.
But it give you a hint with recursive function.
jsFiddle flat array json transform to recursive tree json
function getNestedChildren(arr, parent) {
var out = []
for(var i in arr) {
if(arr[i].parent == parent) {
var children = getNestedChildren(arr, arr[i].id)
if(children.length) {
arr[i].children = children
}
out.push(arr[i])
}
}
return out
}
var flat = [
{id: 1, title: 'hello', parent: 0},
{id: 2, title: 'hello', parent: 0},
{id: 3, title: 'hello', parent: 1},
{id: 4, title: 'hello', parent: 3},
{id: 5, title: 'hello', parent: 4},
{id: 6, title: 'hello', parent: 4},
{id: 7, title: 'hello', parent: 3},
{id: 8, title: 'hello', parent: 2}
]
var nested = getNestedChildren(flat, 0)
console.log(nested)

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