Recursively check for condition on tree and stop when found - javascript

I have a basic tree data structure like this (forget about the parent properties that are missing):
data = {
name: 'root',
value: 20,
children: [
{
name: 'child_1',
value: 12,
children: [...]
},
{
name: 'child_2',
value: 8,
children: [...]
},
]
}
And I want to write a function that takes in any item, which can be the root, or any of the children, and do some evaluation on it.
Like the following:
public doCheck(item: TreeItem): boolean {
item.children.forEach( (i: TreeItem) => {
return this.doCheck(i);
});
return (item.children.some( (i: TreeItem) => i.value >= 10));
}
However, right now this seems to be traversing the tree properly, but only returns the evaluation (item.children.some( (i: TreeItem) => i.value >= 10)) as if it was called on the root item alone, for which it will never be true.
Where am I going wrong?

You want to get rid of the forEach and instead recurse inside the some.
I'm going to assume value appears on the entries in children. If so:
function doCheck(item) {
// If `children` is optional, you could add
// `item.children &&` just after `return`
return item.children.some(entry => entry.value >= 10 || doCheck(entry));
}
console.log(doCheck(data)); // true or false
var data = {
name: 'root',
data: [],
value: 5,
children: [
{
name: 'child_1',
data: [],
children: [],
value: 10,
},
{
name: 'child_2',
data: [],
children: [],
value: 20
},
]
};
function doCheck(item) {
// If `children` is optional, you could add
// `item.children &&` just after `return`
return item.children.some(entry => entry.value >= 10 || doCheck(entry));
}
console.log(doCheck(data)); // true, since `child_1` has it
You'll need to add back the type annotations, etc., to turn that back into TypeScript.
If you wanted to find the entry, not just check for it, you'd use find instead of some:
function doCheck(item) {
// If `children` is optional, you could add
// `item.children &&` just after `return`
return item.children.find(entry => entry.value >= 10 || doCheck(entry));
}
console.log(doCheck(data)); // undefined, or the child
var data = {
name: 'root',
data: [],
value: 5,
children: [
{
name: 'child_1',
data: [],
children: [],
value: 10,
},
{
name: 'child_2',
data: [],
children: [],
value: 20
},
]
};
function doCheck(item) {
// If `children` is optional, you could add
// `item.children &&` just after `return`
return item.children.find(entry => entry.value >= 10 || doCheck(entry));
}
console.log(doCheck(data).name);// "child_1"

Not clear what exactly you're trying to do, but this part:
item.children.forEach( (i: TreeItem) => {
return this.doCheck(i);
});
Makes little sense, as you're basically not doing anything here.
You're probably looking for this:
public doCheck(item: TreeItem): boolean {
return
item.children.some(i => i.value >= 10)
&&
item.children.some(i => this.doCheck(i));
}
Maybe || instead of &&.
Which can of course be changed to:
public doCheck(item: TreeItem): boolean {
return item.children.some(i => i.value >= 10 && this.doCheck(i));
}

Related

Javascript doesn't sort list items properly

I have - array of objects - list items, I sort these items by fieldName. Normally it seems it works fine, but on some items it behaves strange and doesn't sort items properly.
Here is the code that I am making sorting:
elements.slice(0).sort((a, b) => {
if (a[fieldName] === '' || a[fieldName] == null) return 1;
if (b[fieldName] === '' || b[fieldName] == null) return -1;
return (
itemSort
? a[fieldName]?.toLowerCase() < b[fieldName]?.toLowerCase()
: a[fieldName]?.toLowerCase() > b[fieldName]?.toLowerCase()
)
? 1
: -1;
})
itemSort is a boolean and I decide to make A-Z or Z-A sorting.
Here is a picture from strange behaviour, I only see the wrong sorting on these items.
Here is an example of elements
[
{
icon: "IssueTracking"
id: "62a0868c2b2b180061ab05d8"
name: "[DEMO ASLC] All Issues"
type: "sheet"
updatedAt: "2022-12-05T15:17:23.072Z"
url: "/admin/documents/edit/62a0868c2b2b180061ab05d8"
},
{
icon: "..."
id: "..."
name: "..."
type: "..."
updatedAt: "..."
url: "..."
},
...
]
.sort() method modifies array itself, so you need to copy the array into a new array if you would like to keep your original array order in place.
const elementArray = [
{ name: "abc" },
{ name: "abb" },
{ name: "cc" },
{ name: "1bb" },
{ name: "4bc" },
{ name: "abb4" },
{ name: "" },
];
const sortItems = (elements, asc = true) => {
const sortedArray = [...elements];
sortedArray.sort((a, b) => {
let sortResult = a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1;
return asc ? sortResult : sortResult * -1
});
return sortedArray;
};
console.log(`descending: ${JSON.stringify(sortItems(elementArray, false))}`);
console.log(`ascending: ${JSON.stringify(sortItems(elementArray))}`);
One of the best way to do this is to use the Lodash sortBy method.
You can install this library with npm or yarn and simply do _.sortBy(elements, 'fieldName')

Transform dot notation to a tree data form

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.

How to fix the count variable value in a recursive method using react and javascript?

I have data like below:
const arr_obj = [
{
id: '1',
children: [],
type: 'TYPE1',
},
{
id: '2',
children: [
{
id: '1',
children: [
{
//some attributes
},
],
type: 'MAIN',
},
{
id: '2',
children: [
{
//some attributes
},
],
type: 'MAIN',
},
{
id: '3',
children: [
{
//some attributes
},
],
type: 'MAIN',
},
],
type: 'TYPE2',
},
{
id: '3',
children: [
{
id: '4',
children: [
{
//some attributes
},
],
type: 'MAIN',
},
{
id: '5',
children: [
{
//some attributes
},
],
type: 'MAIN',
},
{
id: '6',
children: [
{
//some attributes
},
],
type: 'MAIN',
},
],
type: 'TYPE2',
},
];
I have to find out the count of type: 'MAIN'. these 'MAIN' will be within type: 'TYPE2'
So the expected count is 6.
below is the code,
const ParentComponent = () => {
const findCount = (arr_obj) => {
let count = 0;
const expectedCount = 2;
const loop = (children) => {
for (const obj of children) {
const { type, children } = obj;
if (type === 'TYPE2') {
loop(children);
} else if (type === 'MAIN') {
++count;
if (count > expectedCount) return;
}
}
};
loop(children);
return count > expectedCount;
};
const output = findCount(arr_obj);
return (
//some jsx rendering
);
};
The above code works fine, but I want to make a loop (children) function a pure function. I am not sure how to do it.
The problem now is: I define variables outside the loop method.
How can I define everything as arguments to the function? You could move the function outside the component.
I have tried something like below to make it a pure function
const loop = (children, count = 0) => {
if (!children) return;
for (const obj of children) {
const { type, children } = obj;
if (type === 'TYPE2') {
loop(children, count + 1);
} else if (type === 'MAIN') {
++count;
if (count > expectedCount) return;
}
}
console.log('count', count); //this is 0 always when i log
return count;
};
const ParentComponent = () => {
const output = React.useMemo(() => {
return loop(arr_obj);
}, [arr_obj]);
console.log('output', output); // output is always false
return (
//some jsx rendering
)
};
Now the problem with above code is that the count is always 0. I am not sure where the problem is.
Your approach is fine: you update count by passing it as a parameter and by returning its updated value. However, the function returns at three spots and you only return count at the third spot. Furthermore, you call loop recursively, but there, you don't use its return value and you should pass count instead of count + 1 as an argument.
You need to make the following changes:
Inside loop, replace return; with return count; two times.
Inside loop, replace loop(children, count + 1); with count = loop(children, count);
Now, if you would remove if (count > expectedCount) return;, you would get 6 as the result.
I'm not sure what exactly you want but I don't think you'd need to make it complicated with recursion. You can simply achieve the same function like this:
const findCount = (arr) => {
const filteredArray = arr
.filter(el => el.type === 'TYPE2')
// removed all elements whose type is not TYPE2
.map(el => el.children)
// replaced all element with element's children array
.flat()
// flattened the array
.filter(el => el.type === 'MAIN');
// removed all elements whose type is not MAIN
return filteredArray.length;
};

Create a tree from a list of strings containing paths

See edit below
I wanted to try and create a tree from a list of paths and found this code on stackoverflow from another question and it seems to work fine but i would like to remove the empty children arrays instead of having them showing with zero items.
I tried counting r[name].result length and only pushing it if it greater than zero but i just end up with no children on any of the nodes.
let paths = ["About.vue","Categories/Index.vue","Categories/Demo.vue","Categories/Flavors.vue","Categories/Types/Index.vue","Categories/Types/Other.vue"];
let result = [];
let level = {result};
paths.forEach(path => {
path.split('/').reduce((r, name, i, a) => {
if(!r[name]) {
r[name] = {result: []};
r.result.push({name, children: r[name].result})
}
return r[name];
}, level)
})
console.log(result)
EDIT
I didnt want to ask directly for the purpose i am using it for but if it helps i am trying to create an array like this: (this is a copy paste of the config needed from ng-zorro cascader)
const options = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
isLeaf: true
}
]
},
{
value: 'ningbo',
label: 'Ningbo',
isLeaf: true
}
]
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
isLeaf: true
}
]
}
]
}
];
from an array of flat fields like this:
let paths = ["About.vue","Categories/Index.vue","Categories/Demo.vue","Categories/Flavors.vue","Categories/Types/Index.vue","Categories/Types/Other.vue"];
I suggest to use a different approach.
This approach takes an object and not an array for reaching deeper levels and assigns an array only if the nested level is required.
let paths = ["About.vue", "Categories/Index.vue", "Categories/Demo.vue", "Categories/Flavors.vue", "Categories/Types/Index.vue", "Categories/Types/Other.vue"],
result = paths
.reduce((parent, path) => {
path.split('/').reduce((r, name, i, { length }) => {
let temp = (r.children ??= []).find(q => q.name === name);
if (!temp) r.children.push(temp = { name, ...(i + 1 === length && { isLeaf: true }) });
return temp;
}, parent);
return parent;
}, { children: [] })
.children;
console.log(result)
.as-console-wrapper { max-height: 100% !important; top: 0; }

style objects with a specific key

I have the following data structure:
this.state = {
active_menu: 2018,
info: [
{
key: 11,
title: 'A',
opened: false,
content: []
}, {
key: 10,
title: 'B',
opened: false,
content: []
}, {
key: 9,
title: 'C',
opened: false,
content: []
},
{
key: 1,
title: 'D',
opened: false,
content: []
}],
display: true
}
Tell me, please, how is it possible with the value of display:false to remove (perhaps it can assign style display:none) elements with keys 11, 10 and 9? At display:true elements 11, 10 and 9should be visible, and the element with key 1 is hidden.
Honestly I sit for the third day and can not decide. I ask for your help and would be grateful for any help...
Yes, i asking how to change the objects in the array
Since you're not allowed to modify state directly, you create a copy of info and its objects:
this.setState(({display, info}) => ({
info: info.map(entry => {
const {key} = entry;
const newEntry = {...entry};
if (!display && (key == 11 || key == 10 || key == 9)) {
newEntry.display = "none";
}
return newEntry;
})
}));
Or if it's okay to have a display property with the value undefined when it isn't "none":
this.setState(({display, info}) => ({
info: info.map(entry => ({
...entry,
display: !display && (entry.key == 11 || entry.key == 10 || entry.key == 9) ? "none" : undefined
}))
}));
I guess you can use the following code:
this.setState((prevState) => {
return {
...prevState,
display: false,
info: prevState.info.map(i => {
if(i.key ===9 || i.key === 10 || i.key === 11) return {...i, display: 'none' }
else return i
})
}
})
and something similar for the true case.
so what's going on in this code?
First, we use the setState's function argument instead of object one, because we need component's previous state in order to create the new one.
Next, we map over the prevState's info and try to create the new state based on that.
We do this by checking each item's key to see if it is equal to 9, 10 or 11 and if so, returning the modified object, elsewhere, we return the original item without changing it.
But I think your state structure is bad for this use case. so I suggest this one instead:
state = {
active_menu: 2018,
info: {
11: {
title: 'A',
opened: false,
content: []
},
....
}
}
And the following code for updating state:
this.setState((prevState) => {
return {
...prevState,
display: false,
info: {
...prevState.info,
11: {
...prevState.info[11],
display: 'none'
},
...
}
}
})
And the following one for mapping over the info items:
Object.keys(this.state.info).map(infoKey => {
let info = this.state.info[infoKey];
// Do whatever you want with info and return the component you want to render
})

Categories

Resources