Reputation: 317
I found the implementation of the function in js in the Internet, This function recursively filters an array of objects, each object may have property "children" which is array of objects and that objects also may have children and so on. The function works correctly but I didn't understand it a bit.
This is my function:
getFilteredArray (array, key, searchString) {
const res = array.filter(function iter(o) {
if (o[key].toLowerCase().includes(searchString.toLowerCase())) {
return true;
}
if(o.children){
return (o.children = o.children.filter(iter)).length;
}
});
this.setState({
filteredArray: res
});
}
I don't understand in this code:
if(o.children){
return (o.children = o.children.filter(iter)).length;
}
Can we simplify this expression (o.children = o.children.filter(iter)).length;
?
Why we return the length of array not the array itself?
And function "iter" accepts one argument that is object. Why we just write o.children.filter(iter) without any arguments passed to the "iter" here? according to recursion tutorials, there are arguments always passed, if the function requires them. But here we don't pass, this is strangely.
Upvotes: 0
Views: 609
Reputation: 50787
The answer from ggorlen admirably explains how this function works.
But this function and ggorlen's simplification of it do something I believe a filter
should never do: they mutate the initial data structure. If you examine this value before and after the call in ggorlen's example, you will notice that it changes from 2
to 1
:
arr[0].children[0].children.length
And this issue is present in the original code too. As far as I can see, there is no simple way to fix this with an implementation based on Array.prototype.filter
. So another implementation makes some sense. Here's what I have come up with, demonstrated with ggorlen's test case:
const getFilteredArray = (arr, test) => arr .reduce
( ( acc
, {children = undefined, ...rest}
, _ // index, ignored
, __ // array, ignored
, kids = children && getFilteredArray (children, test)
) => test (rest) || (kids && kids .length)
? acc .concat ({
... rest,
...(kids && kids .length ? {children: kids} : {})
})
: acc
, []
)
const arr = [
{foo: "bar", children: [{foo: "baz", children: [{foo: "quux"}, {foo: "quuz"}]}]},
{foo: "corge", children: [{foo: "quux"}]},
{foo: "grault", children: [{foo: "bar"}]}
];
const test= (item) => item.foo == 'quux'
console .log (
getFilteredArray (arr, test)
)
Note that I made it a bit more generic than requested, testing with an arbitrary predicate rather than testing that key
property matches the searchString
value. This makes the code simpler and the breakdown in logic clearer. But if you want that exact API, you can make just a minor change.
const getFilteredArray = (arr, key, searchString) => arr .reduce
( ( acc
, {children = undefined, ...rest}
, _ // index, ignored
, __ // array, ignored
, kids = children && getFilteredArray (children, key, searchString)
) => rest[key] === searchString || (kids && kids .length)
? acc .concat ({
... rest,
...(kids && kids .length ? {children: kids} : {})
})
: acc
, []
)
One thing that might be missing is that the predicate runs against a value that does not include the children. If you wanted to be able include them, it's not much more work. We'd have to pass item
in place of {children = undefined, ...rest}
and destructure them inside the body of the function. That would require changing the body from a single expression to a {
- }
one.
If I wasn't trying to closely match someone else's API, I would also change the signature to
const getFilteredArray = (test) => (arr) => arr .reduce ( /* ... */ )
This version would allow us to partially apply the predicate and get a function that we can run against different inputs.
Upvotes: 0
Reputation: 11930
class A {
static getFilteredArray(array, key, searchString) {
const query = searchString.toLowerCase()
const res = array.filter(function searchText(item) {
const text = item[key].toLowerCase()
if (text.includes(query)) {
return true
}
if (item.children) { // if object has children, do same filtering for children
item.children = item.children.filter(searchText)
return item.children.length
// same as below, but shorter
// item.children = item.children.filter(function (child) {
// return searchText(child)
// })
}
})
return res
}
}
const input = [{
name: 'John',
children: [{
name: 'Alice'
}]
}]
const output1 = A.getFilteredArray(input, 'name', 'Alic')
const output2 = A.getFilteredArray(input, 'name', 'XY')
console.log('Alic => ', output1)
console.log('XY =>', output2)
Upvotes: 1
Reputation: 56885
Here's a re-write that strives for clarity and simplifies the logic a bit to remove distractions:
const recursivelyFilter = (arr, key, searchString) => {
return arr.filter(function iter(obj) {
if (obj[key].includes(searchString)) {
return true;
}
if (obj.children) {
obj.children = obj.children.filter(child => iter(child));
return obj.children.length > 0;
}
return false;
});
};
Array#filter
is the meat of this code. filter
accepts a callback which should return a boolean to determine whether an element will be included in the result array. It doesn't work in-place.
The base cases (terminating conditions for the recursion) are:
key
key matching searchTerm
, return true.searchTerm
and has no children, return false. In the original code, returning undefined
defaults to falsey.The recursive case is:
iter
function. If at least one descendant of the current node passes the filter condition, include the current node in its parent's children
array, otherwise remove it. The code treats the length of the new child array as a boolean to achieve this.return (o.children = o.children.filter(iter)).length;
first performs an assignment to o.children
, necessary because o.children.filter
returns a fresh copy of the array. After the assignment is finished, the expression resolves to the new o.children
and its length
property is returned. The length is then treated as truthy/falsey according to the recursive case rule described above. This amounts to:
obj.children = obj.children.filter(child => iter(child));
return obj.children.length > 0;
If we returned the array itself, everything would be treated as true because an empty array, []
, evaluates to true. [].length
, on the other hand, evaluates to false, which is the desired outcome.
As for o.children.filter(iter)
, Array#filter
accepts a callback as its first parameter, which can be a function variable such as iter
. Another option is creating an anonymous function directly in the argument list; this is usually how it's done. The above version adds an arrow wrapper, but it's an obviously unnecessary extra layer of indirection since the lone parameter is just passed through the wrapper. We could also use the function
keyword here; whatever the case, the goal is the same, which is that we pass a function into filter
to call on each element.
By the way, the function assumes that key
is set on all the nodes of the nested objects in array
and that obj[key].includes
is defined. Clearly, the author had a very specific data structure and purpose in mind and wasn't interested in prematurely generalizing.
Here's test code illustrating its operation. Playing around with it should help your understanding.
const recursivelyFilter = (arr, key, searchString) => {
return arr.filter(function iter(obj) {
if (obj[key].includes(searchString)) {
return true;
}
if (obj.children) {
obj.children = obj.children.filter(child => iter(child));
return obj.children.length > 0;
}
return false;
});
};
const arr = [
{
foo: "bar",
children: [
{
foo: "baz",
children: [
{foo: "quux"},
{foo: "quuz"},
]
}
]
},
{
foo: "corge",
children: [
{foo: "quux"}
]
},
{
foo: "grault",
children: [{foo: "bar"}]
}
];
console.log(recursivelyFilter(arr, "foo", "quux"));
Upvotes: 4
Reputation: 113866
The return is not for getObject
. It is for the .filter()
callback.
The answer is therefore simple: filter
expects its callback to return a true
/false
value depending on weather or not you want to keep or remove the object form the resulting array. Therefore returning the length is enough since 0
is falsy and all other numbers are truthy.
Upvotes: 0
Reputation: 2167
Perhaps some code changes will help you understand what is going on.
function iter(o){
if (o[key].toLowerCase().includes(searchString.toLowerCase())) {
return true;
}
if(o.children){
o.children = o.children.filter(iter);
return o.children.length;
}
}
getObject (array, key, searchString) {
const res = array.filter(iter);
this.setState({
filteredArray: res
});
}
The iter function is executed by array.filter for each element in the array, if it returns true the element is added to the result, otherwise it is excluded.
In this scenario, if the item itself isn't a direct match, but a child item is we want to keep it. The function handles that by filtering the o.children array using the same criteria. The filtered version of the array is re-assigned to o.children.
The length of the filtered array is then returned as the true/false value that the previous array.filter is looking for. If the array is empty, the length is zero, which is false so the item is excluded. Otherwise a non-zero value is returned, which is true, so the item is kept.
Upvotes: 1