I need some help in writing recursive javascript function to achieve something but I am really struggling to achieve that. Can someone please help me or guide me on this.
I have nested tree something like this, Group1 has child Group2 and Group2 has child Group3. Group3 has no child.
[
{
name: "Group1",
children: [{
name: "Group2",
children: [{
name: "Group3"
children: []
}],
}]
},
{
name: "Group5",
children: [],
},
{
name: "Group6",
children: [],
},
{
name: "Group7",
children: [{
name: "Group 10",
children: [
{
name: 'Group2'
children: [{
name: "Group3"
children: []
}],
}
]
}],
}
]
If I search Group2, function should return all parents and child elements separately in an array. How can I achieve this.
Example:
Input: "Group2"
Output:
[
{
name: "Group1",
children:[
{
name:"Group2",
children :[
{
name:"Group3",
children:[]
}
]
}
]
},
{
name:"Group2",
children: [
{
name:"Group3",
children:[]
}
]
},
{
name:"Group3",
children:[]
},
{
name:"Group7",
children :[
{
name:"Group10",
children:[
{
name:"Group2",
children: [
{
name:"Group3",
children :[]
}
]
}
]
}
],
},
{
name:"Group10",
children:[
{
name:"Group2",
children:[
{
name:"Group3",
children":[]
}
]
}
]
}
]
If I search 'Group2', it should give me complete node separately in a list in which this term exist. For example: 'Group 2' is child of 'Group 1' so complete node tree should be in list [Group1, Group2, Group3] and then 'Group2' is also child of 'Group10', which is also child of 'Group7', so combining complete family for this [Group7, Group10, Group2, Group3]. So complete output should be like all distinct nodes [Group1, Group2, Group3, Group7, Group10 ]. Is this making sense??
Any help will be appreciated.
I was writing code something like this, this works but don't know how to do it better.
const groups = this.props.groups;
const filter = (group: Group) => group.name.toLowerCase().includes(searchTerm);
if (this.state.searchTerm !== '') {
groups = groups.filter(filter);
groups = this.groupHierarchy(groups);
}
private groupHierarchy = (filteredGroups: Group[]) => {
const groups: Group[] = filteredGroups;
groups.map((filteredGroup: Group) =>
this.addAllChildren(filteredGroup, groups));
groups.map((filteredGroup: Group) =>
this.addAllParents(filteredGroup, groups));
return groups;
}
private addAllChildren = (filteredGroup: Group, childGroups: Group[]) => {
const groups = childGroups;
if (filteredGroup.children) {
filteredGroup.children.map((child: Group) => {
if (!groups.find(a => a.id === child.id)) {
groups.push(child);
}
this.addAllChildren(child, groups);
});
}
return groups;
}
private addAllParents = (filteredGroup: Group, parentGroups: Group[]) => {
const groups = parentGroups;
if (filteredGroup.parent && filteredGroup.parent.id) {
const parentGroup = this.props.groups.find(group => group.id === filteredGroup.parent!.id)!;
if (!groups.find(a => a.id === parentGroup.id)) {
groups.push(parentGroup);
}
this.addAllParents(parentGroup, groups);
}
return groups;
}
This version makes generic functions for checking whether a node or one of its descendants matches a generic predicate and, using that, for collecting all the nodes matching a predicate along with all their ancestors and children.
Using the latter, we write a simple function which accepts a target name, and returns a function which will find all the nodes with a name property that matches the target, along with all their ancestors and children:
const hasMatch = (pred) => ({children = [], ...rest}) =>
pred (rest) || children .some (hasMatch (pred))
const collectFamily = (pred) => (xs) =>
xs .flatMap (
x => pred(x)
? [x, ...(x.children || [])]
: hasMatch (pred) (x)
? [x, ...collectFamily (pred) (x.children || [])]
: []
)
const collectFamilyByName = (target) =>
collectFamily((({name}) => name == target))
const log = (fn) => (inputs) => // JSON.stringify avoids StackOverflow console artifacts
inputs .forEach (o => console .log (JSON .stringify (fn (o), null, 2)))
const inputs = [
[{name: "Group 1", children: [{name: "Group 2", children: [{name: "Group 3", children: []}]}]}, {name: "Group 5", children: []}],
[{name: "Group 1", children: [{name: "Group 2", children: [{name: "Group 3", children: []}]}]}, {name: "Group 5", children: [{name: 'Group 2', children: []}]}],
]
log (collectFamilyByName ('Group 2')) (inputs)
.as-console-wrapper {max-height: 100% !important; top: 0}
I have two example cases. The first is the one from the question. The second, following a comment on another answer, adds to the 'Group 5' object a child of 'Group 2', and it is collected as I believe you wish.
The code is fairly simple, but I see two potential disadvantages. First, it recurs separately over the tree for testing and collecting. I'm sure there is a relatively clean way to combine them, but I don't see it right away. Second, this will fail to collect descendants of children of nodes that also have the right name. It stops at their children. Again, I imagine there's a quick fix for that, but I'm not seeing it at the moment.
Update - code explanation
A comment implied that this code could use explanation. Here's an attempt
We look first at the simplest function, the main one, collectFamilyByName:
const collectFamilyByName = (target) =>
collectFamily((({name}) => name == target))
This function takes a target string and calls collectFamily passing a function that uses that target. We don't know yet what collectFamily will do; we just know that it takes a predicate function (one returning true or false) and returns to us a new function.
We could have written this in a few different ways. Here's one alternative:
const testMatch = function (target) {
return function (element) {
return element.name == target
}
}
const collectFamilyByName = function (target) {
return collectFamily (testMatch (target))
}
which by simple substitution would be equivalent to
const collectFamilyByName = function (target) {
return collectFamily (function (element) {
return element.name == target
})
}
By using more modern arrow functions for the inner function, this would become
const collectFamilyByName = function (target) {
return collectFamily (element => element.name == target)
}
and then for the outer one:
const collectFamilyByName = (target) =>
collectFamily (element => element.name == target)
And finally, we can use parameter destructuring to remove the element parameter, like this:
const collectFamilyByName = (target) =>
collectFamily (({name}) => name == target)
Now, in order to understand collectFamily, we have to understand hasMatch. Let's expand this again to ES5 style:
const hasMatch = function (pred) {
return function (element) {
return pred (element) ||
element .children .some (function (child) {
return hasMatch (pred) (child)
})
}
}
This is standard ES5 code, almost, but not quite, equivalent to the version above. Here we accept a predicate and return a function which accepts an element and returns a boolean. It will be true if the predicate returns true for the element, or, recurring on the element's children, if hasMatch passed the same predicate and then passed each child, returns true on any of them, using Array.prototype.some.
This is simplified into the above again using arrow functions and parameter destructuring. The one difference is that the ES5 function applies the predicate to the entire object and the ES6 one applies it to a copy of the object that doesn't include children. If this is not desired, we could skip the parameter destructuring here, but I think it usually makes sense to do it this way.
Finally, the main function is collectFamily, which looks like this:
const collectFamily = (pred) => (xs) =>
xs .flatMap (
x => pred(x)
? [x, ...(x.children || [])]
: hasMatch (pred) (x)
? [x, ...collectFamily (pred) (x.children || [])]
: []
)
I won't go through the ES6 -> ES5 exercise here. It works just the same as in the other two. Instead, let's look at flatMap and the spread syntax.
flatMap is an operation on arrays that works much like map, except that it expects the function to return an array of objects on every call, which it then combines into a single array.
[10, 20, 30] .flatMap ((n) => [n, n + 1, n + 2])
//=> [10, 11, 12, 20, 21, 22, 30, 31, 32]
the spread syntax uses the token ... to spread the contents of an array
(or an object, not relevant here) into another array. So if xs held [1, 2, 3] then [5, 10, ...xs, 20] would yield [5, 10, 1, 2, 3, 20].
Knowing these, we can understand collectFamily. It accepts a predicate (which we already know we saw will be one that matches on objects whose name property has the same value as our target value) and returns a function which takes an array of objects. It calls flatMap on this array, passing a function, so we know that that function must return an array of values, given one element in the original array.
If I were to rewrite this, I might lay it out a little differently to make the body of that function slightly more clear, perhaps like this:
const collectFamily = (pred) => (xs) =>
xs .flatMap (
(x) =>
pred (x)
? [x, ...(x .children || [])]
: hasMatch (pred) (x)
? [x, ...collectFamily (pred) (x .children || [])]
: []
)
And the function, which is passed an array value with parameter name x, would have this body:
pred (x)
? [x, ...(x.children || [])]
: hasMatch (pred) (x)
? [x, ...collectFamily (pred) (x.children || [])]
: []
We return one of three different values.
In the first case, if the predicate matches our current value (again, remember in this case, that's if the name property of x matches the target, such as 'Group 2'), then we return an array including x and all of its children. We use the || [] so that if x .children were not defined, we would still have an iterable object to spread into our array. It doesn't matter for the sample data as supplied, but it's useful in many cases.
In the second case, if we have a match somewhere nested more deeply, as reported by hasMatch, we return an array including this node, and all the results found by recursively calling collectFamily with the same predicate against the list of children (again, defaulted to an empty array if they don't exist.)
And in the third case, if neither of those is true, we simply return an empty array.
So that's how this works. There's no magic here, but if you're new to the language, some of the more modern features may seem a bit obscure. I promise, though, that with a little practice, they'll become second-nature. They make for much simpler code, and, once you're used to the syntax, they seem to me much easier to read as well.
You could take a recursive approach with a short circuit on find.
To get all parents, the function contains another parameter for visited parents.
const
getFromTree = (tree, name, parents = []) => {
let result;
tree.some(node => result = node.name === name
? [...parents, node, ...node.children]
: getFromTree(node.children, name, [...parents, node])
);
return result;
},
tree = [{ name: "Group 1", children: [{ name: "Group 2", children: [{ name: "Group 3", children: [] }] }] }, { name: "Group 5", children: [] }];
console.log(getFromTree(tree, "Group 1"));
console.log(getFromTree(tree, "Group 2"));
console.log(getFromTree(tree, "Group 3"));
console.log(getFromTree(tree, "Group 5"));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Related
I have a sorted array that has a list of strings arrays:
0:['BS', 'MS','KHB', 'CCBPO']
1:['BS', 'MS','KHB', 'HBPO']
2:['BS', 'MS','KHB', 'PBPO']
3:['BS', 'PO','BC', 'BC']
4:['H', 'I','SS', 'ESS']
5:['H', 'I','SS', 'E']
6:['H', 'D','PCD', 'D']
7:['H', 'D','P', 'HP']
ECT
As you can see always the first parent is located in first spot. Like in this case it's 'BS' and 'H' and then the rest.
So for the first row we will have something like this:
{
"name": "BS",
"children": [
{
"name": "MS",
"children": [
{
"name": "KB",
"children": [
{
"name": "KHB",
"children": [
{
"name": "CCBPO",
"isTrue": false
},
"isTrue": false
},
"isTrue": false
},
"isTrue": false
},
"isTrue": false
}
For the Second row, we will need to check if the parent is the same and add the child at the proper place which for the second row it will be in the last place and so.
After this, I want to be able to convert it into a similar structure like it was before But with a difference. For example for the first 4 rows, I will have.
0:['BS', 'MS','KHB', 'CCBPO']
1:['', '','', 'HBPO']
2:['', '','', 'PBPO']
3:['', 'PO','BC', 'BC']
Which will help me visuale the tree into a table.
It's looks like work for a recursive function. A recustive function it's a function that called itself (under a determinate circunstances)
we are going to create a function that return an array with name and children. The children are the elements of the array with the first element array is the same that the property name
createTreeAtempOne(array: any[]) {
const tree = array.reduce((a: any, b: any) => {
const el = a.find((x) => x.name == b[0]);
if (el) el.children.push(b.slice(1));
else
a.push({
name: b[0],
children: [b.slice(1)],
}
);
return a;
}, []);
}
See that if you feed this function with a simple array
createTreeAtempOne(['AA'])
return some like
{
"name": "AA",
"children": [
[]
]
},
Well, we don't want this. If has no children, we want that return an object without "children" property, so change our function in the way
createTreeAtempTwo(array: any[]) {
const tree = array.reduce((a: any, b: any) => {
const el = a.find((x) => x.name == b[0]);
if (el){
if (el.children)
el.children.push(b.length > 1 ? b.slice(1) : b[0]);
else
el.children=[b.slice(1)]
} else
a.push(
b.length > 1
? {
name: b[0],
children: [b.slice(1)],
}
: {name:b[0]}
);
return a;
}, []);
return tree;
}
This makes that if we feed this function with
createTreeAtempOne(['AA'])
return some like
{
"name": "AA"
},
Good!! after checked with different values [['AA'],['AA','BB']], etc.. we are going to give to the function the "recursive". After get the values, we can loop over the array. If one element of the array has "children" property, call again to the function using as argument the "children"
createTree(array: any[]) {
const tree = array.reduce((a: any, b: any) => {
const el = a.find((x) => x.name == b[0]);
if (el){
if (el.children)
el.children.push(b.length > 1 ? b.slice(1) : b[0]);
else
el.children=[b.slice(1)]
}
else
a.push(
b.length > 1
? {
name: b[0],
children: [b.slice(1)],
}
: {name:b[0]}
);
return a;
}, []);
tree.forEach((t) => {
if (t.children) t.children = this.createTree(t.children);
});
return tree;
}
Then we need checked againts different values to see if work
In the stackblitz I use a mat-tree to show the data, but we can simply use a
<pre>
{{tree|json}}
</pre>
I have object oriented data in the form:
var alist = [
'foo',
'foo.lol1',
'foo.lol2',
'bar.lol1',
'bar.barbar.kk',
...
]
which I would like to transform into a tree structure, to be able to serve them with a tree component (https://github.com/vinz3872/vuejs-tree in particular). The require form is the following:
var ok = [
{
text: "foo",
state: { expanded: false },
nodes: [
{
id: 1,
path: "foo.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
{
id: 2,
path: "foo.lol2",
text: "lol2",
checkable: true,
state: { checked: false },
},
]
},
{
text: "bar",
state: { expanded: false },
nodes: [
{
id: 3,
path: "bar.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
]
},
{
text: "bar",
state: { expanded: false },
nodes: [
{
id: 3,
path: "bar.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
{
text: "barbar",
state: { expanded: false },
nodes: [
{
id: 4,
path: "bar.barbar.kk",
text: "kk",
checkable: true,
state: { checked: false },
},
]
},
]
}
]
I am aware that I should use recursion and I have tried all relevan posts in stackoverflow, i.e. How to build a JSON tree structure using object dot notation.
My main problem is that I have to somehow preserve the information of the full path to the leaves of the tree. As a newbie in js I lost myself in counters and callback for days without any luck.
I would appreciate your help.
Thank you in advance
Basically you could use forEach then split each string into array and then use reduce on that. Then you build nested object where the keys are current paths and also ad to result array.
var alist = [
'foo',
'foo.lol1',
'foo.lol2',
'bar.lol1',
'bar.barbar.kk',
]
const result = []
const levels = {
result
}
let prev = ''
let id = 1
alist.forEach(str => {
str.split('.').reduce((r, text, i, arr) => {
const path = prev += (prev.length ? '.' : '') + text
if (!r[path]) {
r[path] = {result: []}
const obj = {
id: id++,
text,
}
if (i === 0) {
obj.state = {expanded: false}
} else {
obj.state = {checked: false}
obj.checkable = true
obj.path = path
}
obj.nodes = r[path].result
r.result.push(obj)
}
if (i === arr.length - 1) {
prev = ''
}
return r[path]
}, levels)
})
console.log(result)
I found that it was easiest to do this transformation in two steps. The first converts your input into this format:
{
foo: {
lol1: {},
lol2: {}
},
bar: {
barbar: {
kk: {}
},
lol1: {}
},
}
The second uses just this format to create your desired structure. This has two advantages. First, I have tools lying around that make it easy to create this structure from your input. Second, this structure embeds enough information to create your output, with only one branching construct: whether the value at a path is an empty object or has properties. This makes the generation code relatively simple:
const setPath = ([p, ...ps]) => (v) => (o) =>
p == undefined ? v : Object .assign (
Array .isArray (o) || Number .isInteger (p) ? [] : {},
{...o, [p]: setPath (ps) (v) ((o || {}) [p])}
)
const reformat = (o, path = [], nextId = ((id) => () => String (++ id)) (0)) =>
Object .entries (o) .map (([k, v]) => Object .entries (v) .length > 0
? {text: k, state: {exapanded: false}, nodes: reformat (v, [...path, k], nextId)}
: {id: nextId (), path: [...path, k] .join('.'), text: k, checkable: false, state: {checked: false}}
)
const transform = (pathTokens) =>
reformat (pathTokens
.map (s => s .split ('.'))
.reduce ((a, path) => setPath (path) ({}) (a), {})
)
const alist = ['foo', 'foo.lol1', 'foo.lol2', 'bar.lol1', 'bar.barbar.kk']
console .log (transform (alist))
.as-console-wrapper {max-height: 100% !important; top: 0}
We start with setPath, which takes a path, in a format such as ['bar', 'barbar', 'kk'], the value to set at that path, and an object to shallow clone with this new property along that path. Thus setPath (['foo', 'bar', 'baz']) (42) ({foo: {qux: 7}, corge: 6}) yields {foo: {qux: 7, bar: {baz: 42}}, corge: 6}. (There's a little more in this reusable function to also handle array indices instead of string object paths, but we can't reach that from this input format.)
Then we have reformat, which does the format conversion. It simply builds a different input object based upon whether the input value is an empty object.
Finally, transform maps a splitting function over your input array to get the path structure needed for setPath, folds the results into an initially empty object by setting every path value to an empty object, yielding our intermediate format, which we then pas to reformat.
There is one thing I really don't like here, and that is the nextId function, which is a stateful function. We could just have easily used a generator function, but whatever we do here, we're using state to build this output and that bothers me. If someone has a cleaner suggestion for this, I'd love to hear it.
I want to filter out below data using Ramda. The desired result is to show the properties where usage === 'Defining'.
const data =
[{
"attributes":
[
{"usage": "Descriptive"},
{"usage": "Defining"}
]
}]
So far this is what i have done and it's not filtering out data and returning the whole object.
R.filter(
R.compose(
R.any(R.propEq('usage', 'Defining')),
R.prop('attributes')
)
)(data)
Below is the desired result that i want to acheive:
[{
"attributes":
[
{"usage": "Defining"}
]
}]
If I understand correctly what you want to do, then where is very useful when you want to filter based on properties. But you want to combine this with map. While Ramda does not supply a filterMap, it's quite easy to write our own. We create a function that accepts a filtering function and a mapping function and returns a function which takes an array and maps only those results which pass the filter. Breaking the problem down that way, we could write something like:
const filterMap = (f, m) => (xs) =>
chain ((x) => f (x) ? [m (x)] : [], xs)
const definingProps = filterMap (
where ({attributes: any (propEq ('usage', 'Defining'))}),
over (lensProp('attributes'), filter (propEq ('usage', 'Defining')))
)
const data = [
{id: 1, attributes: [{usage: "Descriptive"}, {usage: "Defining"}]},
{id: 2, attributes: [{usage: "Descriptive"}, {usage: "Something Else"}]},
{id: 3, attributes: [{usage: "Defining"}, {usage: "Filtering"}]}
]
console .log (definingProps (data))
.as-console-wrapper {min-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
<script> const {curry, chain, where, any, propEq, over, lensProp, filter} = R </script>
Obviously, there is a reasonable argument to be made for also extracting propEq ('usage', 'Defining') into a stand-alone function; that is left as an exercise for the reader.
You are trying to do both a map and a filter here, so it's worth having separate functions for each, then composing them together to get what you want:
const data =
[{
"attributes":
[
{"usage": "Descriptive"},
{"usage": "Defining"}
]
},
{
"attributes":
[
{"usage": "Descriptive"},
{"usage": "Definingx"}
]
}]
const removeObjectsWithoutDefining = filter(
compose(any(equals('Defining')), map(prop('usage')), prop('attributes'))
);
const adjustProp = curry((f, k, o) => ({
...o,
[k]: f(o[k]),
}));
const mapAttributesToRemoveNonDefining = map(
adjustProp(
filter(propEq('usage', 'Defining')),
'attributes',
),
)
const f = compose(mapAttributesToRemoveNonDefining, removeObjectsWithoutDefining);
f(data);
Ramda repl link.
I have been looking all night on SO with lots of similar issues but none that directly solves my problem at the moment. So please have a look below.
I have an object of the form:
let data = [{
"id": 777,
"name": "Level 1_section_1",
"children": [{
"id": 778,
"name": "Level 2a",
"children": [
]
},
{
"id": 783,
"name": "Level 2b",
"children": [
]
}
]
},
{
"id": 786,
"name": "Level 1_section_2",
"children": [{
"id": 781,
"name": "Level 2c",
"children": [
]
}]
}
]
Basically, children contains an array of the same structure nodes.
If I wish to get a reference to the node that contains, say, id:783, I would intuitively use recursion but I'm at a loss as to how I would ensure that it covers the entire tree recursively until it finds and returns the exact node that I want so that I could append more children to the found node.
Admittedly, despite coming from a CS background, my knowledge of recursion is rather rusty.
This is what I've tried in my jsfiddle:
https://jsfiddle.net/hanktrizz/surmf7dq/4/
Note that the data tree could be arbitrarily deep (though I don't expect it to go past 8 or 9 levels of depth) but just thought I'd point it out.
Here's one possibility, using a for loop in a recursive function:
let data=[{id:777,name:"Level 1_section_1",children:[{id:778,name:"Level 2a",children:[]},{id:783,name:"Level 2b",children:[]}]},{id:786,name:"Level 1_section_2",children:[{id:781,name:"Level 2c",children:[]}]}];
const findNode = (arr, idToFind) => {
for (const item of arr) {
if (item.id === idToFind) {
return item;
}
const possibleResult = findNode(item.children, idToFind);
if (possibleResult) {
return possibleResult;
}
}
};
console.log(findNode(data, 778));
Here's a higher-order findNode that is not limited to searching by id alone. Instead, it accepts a user-defined lambda to search nodes using any condition -
findNode (n => n.id === 778, data)
// { id: 778, name: "Level 2a" }
findNode (n => n.name === "Level 2c", data)
// { id: 781, name: "Level 2c" }
findNode (n => n.id === 999, data)
// undefined
Verify the results in your own browser below -
const data =
[{id:777,name:"Level 1_section_1",children:[{id:778,name:"Level 2a",children:[]},{id:783,name:"Level 2b",children:[]}]},{id:786,name:"Level 1_section_2",children:[{id:781,name:"Level 2c",children:[]}]}];
const None =
Symbol ()
// findNode : (node -> boolean, node array) -> node?
const findNode = (f, [ node = None, ...nodes ]) =>
node === None
? undefined
: find1 (f, node) || findNode (f, nodes)
// find1 : (node -> boolean, node) -> node?
const find1 = (f, node = {}) =>
f (node) === true
? node
: findNode (f, node.children)
console.log (findNode (n => n.id === 778, data))
// { id: 778, name: "Level 2a" }
console.log (findNode (n => n.name === "Level 2c", data))
// { id: 781, name: "Level 2c" }
console.log (findNode (n => n.id === 999, data))
// undefined
Above, destructing assignment permits an elegant expression but also creates unnecessary intermediate values. The following revision is a significant improvement -
// findNode : (node -> boolean, node array, int) -> node?
const findNode = (f, nodes = [], i = 0) =>
i >= nodes.length
? undefined
: find1 (f, nodes[i]) || findNode (f, nodes, i + 1)
// find1 : (node -> boolean, node) -> node?
const find1 = (f, node = {}) =>
f (node) === true
? node
: findNode (f, node.children)
Both version offer short-circuit evaluation and will stop iteration immediately after the first result is found
For fun, here's one that attempts to return all instances.
var data=[{id:777,name:"Level 1_section_1",children:[{id:778,name:"Level 2a",children:[]},{id:786,name:"Level 2b",children:[]}]},{id:786,name:"Level 1_section_2",children:[{id:781,name:"Level 2c",children:[]}]}]
var f = (o, pred, acc=[]) =>
pred(o) ? [o] : Object.values(o).reduce((a, b) =>
b && typeof b == 'object' ? a.concat(f(b, pred, acc)) : a, acc)
console.log(JSON.stringify(f(data, o => o.id == 781)))
console.log(JSON.stringify(f(data, o => o.id == 786)))
Here is an iterative solution using object-scan
Main advantage is that you get access to other data in filterFn and can easily do further processing. Obviously there is a trade-off in introducing a dependency
// const objectScan = require('object-scan');
const myData = [{ id: 777, name: 'Level 1_section_1', children: [{ id: 778, name: 'Level 2a', children: [] }, { id: 783, name: 'Level 2b', children: [] }] }, { id: 786, name: 'Level 1_section_2', children: [{ id: 781, name: 'Level 2c', children: [] }] }];
const treeSearch = (data, id) => objectScan(['**(^children$)'], {
useArraySelector: false,
abort: true,
rtn: 'value',
filterFn: ({ value }) => value.id === id
})(data);
console.log(treeSearch(myData, 778));
// => { id: 778, name: 'Level 2a', children: [] }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.7.1"></script>
Disclaimer: I'm the author of object-scan
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
)
);
}