Create a tree from a list of strings - javascript - javascript

I have an array of objects and I want to create a view tree. But I have a problem with creating. My name Test1/Test2 and Test1/Test2/Test3 (id 147) was missed in final tree.
My snippet:
let arr = [{id: 145, name: "Test1/Test2", public: false},
{id: 146, name: "Test1/Test2/Test3", public: false},
{id: 147, name: "Test1/Test2/Test3", public: false},
{id: 148, name: "Test1/Test2/Test4", public: false}];
let result = [];
let level = { result };
arr.forEach((path) => {
path.name.split("/").reduce((r, name, i, a) => {
if (!r[name]) {
r[name] = { result: [], id: path.id };
r.result.push({ name, children: r[name].result });
}
return r[name];
}, level);
});
console.log(result)
Expected result:
[
{
name: "Test1",
children: [
{
name: "Test2",
children: [],
id: 145
},
{
name: "Test2",
children: [
{
name: "Test3",
children: [],
id: 146
},
{
name: "Test3",
children: [],
id: 147
},
{
name: "Test4",
children: [],
id: 148
},
],
},
],
},
];

If I understand well, the number of leaves in your tree should equal the number entries in the input array. So a leaf would never get any children. This is what made you give "Test1" two children, even though all paths have "Test2" as the next part: one child for a leaf, and another functioning as internal node.
By consequence, leaves don't really need a children property, as that children array would always remain empty.
It is clear that the last element of a path needs to be processed a bit differently. That part should always result in the creation of a new node in the tree. The other parts can reuse a (non-leaf) node, if one is available.
This leads to the following change in your code:
let arr = [
{id: 145, name: "Test1/Test2", public: false},
{id: 146, name: "Test1/Test2/Test3", public: false},
{id: 147, name: "Test1/Test2/Test3", public: false},
{id: 148, name: "Test1/Test2/Test4", public: false}
];
let result = [];
let level = { result };
arr.forEach(({name, id}) => { // destructure
let parts = name.split("/");
name = parts.pop(); // don't pass the last part through reducer
parts.reduce((r, name, i) => {
if (!r[name]) {
r[name] = { result: [] };
r.result.push({ name, children: r[name].result });
}
return r[name];
}, level).result.push({ name, id }); // add last part here
});
console.log(result);

Related

Getting parent names from a nested array in javascript

I have the following structure:
{
id: 15
name: "Power",
childCategories: [],
parent: {
id: 10,
name: "Cables",
parent: null,
stockItems: []
}
}
Ideally, what I would like to end up with is the following String structure:
Cables ⟶ Power
But being new to Javascript, I'm not sure how I would achieve this. I'm assuming it would be through recursion.
Looks like you want to "reverse" the way your data is structured ("children -> parents" to "parents -> children").
Your assumption about recursive function is right: in this example I tried to add more data, according to the description given.
const arr = [
{
childCategories: [],
id: 15,
name: "Power",
parent: {
id: 10,
name: "Cables",
parent: null,
stockItems: [],
}
},
{
childCategories: [],
id: 9,
name: "Gas",
parent: {
id: 20,
name: "Pipes",
parent: {
id: 33,
name: "Metal",
parent: null,
stockItems: [],
},
stockItems: [],
}
}
]
const res = []
for (const o of arr) {
recursiveFunct(o)
}
function recursiveFunct(o) {
if (o.parent) {
let parent = o.parent
delete o.parent
parent.children = o
recursiveFunct(parent)
} else {
res.push(o)
}
}
console.log(res)
For each object passed into the recursive function push its name into into an array (result).
If the object has a parent property that isn't null pass the parent object back into the function along with result.
If the parent is null reverse the array and join it up with a "⟶".
const obj = {
id: 153,
name: "Gold plated",
childCategories: [],
parent: {
id: 15,
name: "Power",
childCategories: [],
parent: {
id: 10,
name: "Cables",
parent: null,
stockItems: []
}
}
};
// Pass in the object, and initialise `result`
function getString(obj, result = []) {
// Push each name into the array
result.push(obj.name);
// If the parent is `null` return the joined array
// otherwise pass the parent object back into the
// function along with the result array
if (!obj.parent) {
return result.reverse().join(' ⟶ ');
} else {
return getString(obj.parent, result);
}
}
console.log(getString(obj));

Counting the highest level of nested Arrays inside Object

I have a problem with a task, I have to count the highest level in a structure which looks like this:
let node = {
age: 23,
name: "christian",
children: [{
age: 25,
name: "michael",
children: [{
age: 33,
name: "Johann",
children: [{
age: 45,
name: "Christiaaann",
}]
}]
}, {
age: 90,
name: "Monika",
children: [{
age: 10,
name: "WHATEVER",
}]
}]
};
The level of the first subtree would be 3 as it contains 3 nested children arrays.
The right level would be 2 as it contains 2 nested children arrays.
I tried to solve it recursively, and of course I know that I have to count the level of each subtree and then check it with a condition if it is greater than the previous maximum.
My problem is that I don't know where to count it up in the recursive call.
This is my solution, so far
let node = {
age: 23,
name: "christian",
children: [{
age: 25,
name: "michael",
children: [{
age: 33,
name: "Johann",
children: [{
age: 45,
name: "Christiaaann",
}]
}]
}, {
age: 90,
name: "Monika",
children: [{
age: 10,
name: "WHATEVER",
}]
}]
};
let level_count;
let max_count = 0;
function level_counter(obj, func) {
level_count = 0;
func(obj);
console.log("INSIDEFUNCTION", level_count);
if (obj.children) {
level_count++;
obj.children.forEach(function(child) {
console.log(child);
level_counter(child, func);
});
if (level_count > max_count) {
max_count = level_count;
}
}
}
function tree_get_levels(root) {
level_counter(root, function(obj) { });
console.log("DOWNONE", level_count);
return 0;
}
let result = tree_get_levels(node);
console.log(result);
I tried to solve it recursively, and of course I know that I have to count the level of each subtree and then check it with a condition if it is greater than the previous maximum.
My problem is that I don't know where to count it up in the recursive call.
You need to use the return value of the recursive call, adding one for the level making the call to it. Separately, always avoid global variables when you're trying to write a recursive solution; every level of recursion sees the same values for those variables, and assigning to them won't work correctly. Stick to variables defined within the recursive function, and (again) report values back up the stack by returning them.
See inline notes in the code:
let node = {
age: 23,
name: "christian",
children: [{
age: 25,
name: "michael",
children: [{
age: 33,
name: "Johann",
children: [{
age: 45,
name: "Christiaaann",
}]
}]
}, {
age: 90,
name: "Monika",
children: [{
age: 10,
name: "WHATEVER",
}]
}]
};
function countLevels(obj, func) {
// No levels so far
let levelCount = 0;
func(obj); // Not sure what this function is for
// If there are any children...
if (obj.children) {
// There are, so we've already gone one level down
for (const child of obj.children) {
// Get levels starting at this child, then add one because we've
// already gone one level down
const levelsFromChild = countLevels(child, func) + 1;
// Remember it if it's higher than the maximum we've seen
if (levelCount < levelsFromChild) {
levelCount = levelsFromChild;
}
// Or that could be replaced with:
// levelCount = Math.max(levelCount, countLevels(child, func) + 1);
}
}
// I added this so you could see the levels count starting from each object
console.log(`${levelCount} level(s) from ${JSON.stringify(obj)}`);
// Return the levels found from this object
return levelCount;
}
// No need for a separate wrapper function, just call the counter directly
let result = countLevels(node, function func(obj) { });
console.log(`Deepest: ${result}`);
.as-console-wrapper {
max-height: 100% !important;
}
You can try this:
function highestDepth(obj) {
let depth = 0;
if (Array.isArray(obj.children) && obj.children.length) {
depth += 1;
let subDepth = 0;
for (const child of obj.children) {
subDepth = Math.max(subDepth, highestDepth(child));
}
depth += subDepth;
}
return depth;
}
console.log(highestDepth(node));
A simple recursive version first checks whether the node has children. If it doesn't, we simply return 0; if it does, we recur over each of its children, and take the maximum of those (or 0 if the children array is empty) and add 1. It looks like this:
const maxDepth = (node) => 'children' in node
? 1 + Math .max (0, ... node .children .map (maxDepth))
: 0
const node = {age: 23, name: "christian", children: [{age: 25, name: "michael", children: [{age: 33, name: "Johann", children: [{age: 45, name: "Christiaaann"}]}]}, {age: 90, name: "Monika", children: [{age: 10, name: "WHATEVER"}]}]}
console .log (maxDepth (node))
The 0 passed as the first parameter to Math .max is necessary because Math .max () (with no parameters) returns -Infinity. It's an interesting exercise to try to figure out why that is.
Here is an iterative solution using object-scan. This solution will work even when you are dealing with very deeply nested object hierarchies.
.as-console-wrapper {max-height: 100% !important; top: 0}
<script type="module">
import objectScan from 'https://cdn.jsdelivr.net/npm/object-scan#18.4.0/lib/index.min.js';
const node = { age: 23, name: 'christian', children: [{ age: 25, name: 'michael', children: [{ age: 33, name: 'Johann', children: [{ age: 45, name: 'Christiaaann' }] }] }, { age: 90, name: 'Monika', children: [{ age: 10, name: 'WHATEVER' }] }] };
const fn = objectScan(['**{children[*]}'], {
rtn: 'count', // for performance
beforeFn: (state) => {
state.context = { depth: 0 };
},
filterFn: ({ context, depth }) => {
context.depth = Math.max(context.depth, depth);
},
afterFn: ({ context }) => context.depth / 2
});
console.log(fn(node));
// => 3
</script>
Disclaimer: I'm the author of object-scan

Toggling checkboxes using recursion | Recursion problem

I am trying to implement a checkbox tree that has the following data structure for items
const checkboxes = [{
field: 'ARTICLE',
id: 41,
name: 'Article',
parentId: null,
checked: false,
children: [
{
field: 'article.colorCode',
id: 42,
name: 'Color',
parentId: 41,
checked: false,
children: [
{
children: [],
field: 'red',
id: 43,
name: 'red',
parentId: 42,
checked: false
},
],
},
],
}]
Whenever I click on Article when it's unchecked, it should set checked property as true for Article and nested children and vice-versa(i.e. if a child is checked and it is the only child of its parent and that parent is also the only child of super parent further, all three would get checked)
Can someone please help me with a recursive function so that no matter what the depth of children is, all children get toggled? I just can't undrestand recursion.
I would break this down into three pieces. First, we need to be able to walk our nodes and alter the appropriate ones. Second, we need a way to, starting with some node in the hierarchy, invert the checked property on that node and pass this new value down through all its descendents. Third, we need to put this together with something that checks the nodes' ids to find where to start this process.
Here is a version which breaks it down this way:
const alterNodes = (pred, trans) => (nodes) => nodes .map (
node => pred (node) ? trans (node) : {...node, children: alterNodes (pred, trans) (node .children)}
)
const checkHierarchy = ({checked, children, ...rest}, value = !checked) => ({
... rest,
checked: value,
children: children .map (child => checkHierarchy (child, value))
})
const toggleHierarchy = (targetId, checkboxes) =>
alterNodes (({id}) => id == targetId, checkHierarchy) (checkboxes)
const checkboxes = [{field: 'ARTICLE', id: 41, name: 'Article', parentId: null, checked: false, children: [{field: 'article.colorCode', id: 42, name: 'Color', parentId: 41, checked: false, children: [{children: [], field: 'red', id: 43, name: 'red', parentId: 42, checked: false}]}]}]
console .log (toggleHierarchy (41, checkboxes))
.as-console-wrapper {max-height: 100% !important; top: 0}
Since you mentioned react, I assume you prefer immutable way to do that, since that tree might be stored in state.
The function you want to use is: toggleNodeInsideTree function, and you should pass it id of the node you want to toggle, and tree data.
let data = [
{
field: 'ARTICLE',
id: 41,
name: 'Article',
parentId: null,
checked: false,
children: [
{
field: 'red',
id: 43,
name: 'red',
parentId: 41,
checked: false,
},
{
field: 'article.colorCode',
id: 42,
name: 'Color',
parentId: 41,
checked: false,
children: [
{
children: [],
field: 'test',
id: 44,
name: 'red',
parentId: 42,
checked: false,
},
],
},
],
},
{
children: [],
field: 'red',
id: 45,
name: 'red',
parentId: 41,
checked: false,
},
];
let setCheckedValueForNodeAndChildren = (item, value) => {
if (item.children) {
return {
...item,
checked: value,
children: item.children.map((x) =>
setCheckedValueForNodeAndChildren(x, value)
),
};
} else {
return { ...item, checked: value };
}
};
let toggleNodeInsideTree = (id, tree) => {
return tree.map((x) => {
if (x.id === id) {
return setCheckedValueForNodeAndChildren(x, !x.checked);
}
if (x.children) {
return { ...x, children: toggleNodeInsideTree(id, x.children) };
}
return x;
});
};
console.log(toggleNodeInsideTree(42, data));

How to improve time complexity of this snippet?

I am trying to improve the time complexity and quality of the code snippet below.
I am iterating through one array to check if the element this array exists in the object, should this be true it should return the name matching the element id in the object.
how can I do this without having a nested loop?
Can someone tell me what I can do to make this algo better, please?
Thank you all in advance.
let genres = [28, 12, 878];
data = {
genres: [
{
id: 28,
name: 'Action',
},
{
id: 12,
name: 'Adventure',
},
{
id: 16,
name: 'Animation',
},
{
id: 35,
name: 'Comedy',
},
{
id: 80,
name: 'Crime',
},
{
id: 99,
name: 'Documentary',
},
{
id: 18,
name: 'Drama',
},
{
id: 10751,
name: 'Family',
},
{
id: 14,
name: 'Fantasy',
},
{
id: 36,
name: 'History',
},
{
id: 27,
name: 'Horror',
},
{
id: 10402,
name: 'Music',
},
{
id: 9648,
name: 'Mystery',
},
{
id: 10749,
name: 'Romance',
},
{
id: 878,
name: 'Science Fiction',
},
{
id: 10770,
name: 'TV Movie',
},
{
id: 53,
name: 'Thriller',
},
{
id: 10752,
name: 'War',
},
{
id: 37,
name: 'Western',
},
],
};
const getGenreName = () => {
let result = [];
for (let genre of data.genres) {
//console.log("genre", genre.name)
for (let id of genres) {
//console.log('id',genres[i])
if (id === genre.id) result.push(genre.name);
}
}
console.log(result);
};
getGenreName();
You can use reduce and includes as others have already shown. This will make the code a bit cleaner, but not change the overall runtime complexity. To improve runtime complexity you may need to use a different data structure.
For instance instead of
let genres = [1,2,3,4];
as a simple array, you could use a Set, which has a better lookup performance.
let genres = new Set([1,2,3,4]);
Then you can use this as follows
let result = data.genres
.filter(g => genres.has(g.id))
.map(g => g.name);
and won't need any explict for loops
The simplest improvement would probably be converting genres to a Set https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
and use the has method to check if each id in the data is a member of the set of chosen genres.
You can also convert the data to a map with the ids as the keys in order to look up by id quickly instead of looping, but that is only faster if the data is reused many times.
JavaScript #reduce in the example outlined below would have O(n) time complexity. This only loops through the array once. We could use filter, and map but it would result in us having to loop through the array twice.
const getGenreName = () => {
const genreSet = new Set(genres);
return data.genres.reduce((accumulator, { id, name }) => {
if (genreSet.has(id)) accumulator.push(name);
return accumulator;
}, []);
};
console.log(getGenreName()); // [ 'Action', 'Adventure', 'Science Fiction' ]
We are initializing the reducer to start with the array [], or an empty array, and then checking to see if the genre property of the object is included in the genres array, if it isn't, return the accumulator, if it is, append it to the end of the accumulator and return it.
You wanted this in one loop, so here it is:
let result = [];
data.genres.forEach(function (e) {
if (genres.includes(e.id)) result.push(e.name);
});
console.log(result);
In case you were wondering about forEach, here's a very good reference: https://www.w3schools.com/jsref/jsref_foreach.asp
The current time complexity is O(MN) where M is the length of data.genres and N is the length of genres.
Time complexity in JavaScript depends on which engine you use, but in most cases you can use a Map to reduce this time complexity to O(max{N,M}):
const getGenreName = () => {
const dataGenresMap = new Map( // O(M)
data.genres.map(({id,...params}) => [id,params]) // O(M)
)
let result = []
for (let id of genres) { // O(N)
if (dataGenresMap.has(id)) result.push(dataGenresMap.get(id).name) // O(1)
}
console.log(result)
}
If you might be doing this more than once then I'd recommend using a Map. By creating a hash map, retrieving genre names per id is much more performant.
let genres = [28, 12, 878];
data = {
genres: [
{
id: 28,
name: 'Action',
},
{
id: 12,
name: 'Adventure',
},
{
id: 16,
name: 'Animation',
},
{
id: 35,
name: 'Comedy',
},
{
id: 80,
name: 'Crime',
},
{
id: 99,
name: 'Documentary',
},
{
id: 18,
name: 'Drama',
},
{
id: 10751,
name: 'Family',
},
{
id: 14,
name: 'Fantasy',
},
{
id: 36,
name: 'History',
},
{
id: 27,
name: 'Horror',
},
{
id: 10402,
name: 'Music',
},
{
id: 9648,
name: 'Mystery',
},
{
id: 10749,
name: 'Romance',
},
{
id: 878,
name: 'Science Fiction',
},
{
id: 10770,
name: 'TV Movie',
},
{
id: 53,
name: 'Thriller',
},
{
id: 10752,
name: 'War',
},
{
id: 37,
name: 'Western',
},
],
};
const genreById = new Map ();
data.genres.forEach(({id, name}) => genreById.set(id, name));
const pushMapValueIfTruthy = map => array => key => {
const val = map.get(key);
if (val) {
array.push(val);
}
};
/** function that takes an array, then id, and pushes corresponding name (if exists) into the array. */
const pushGenreNaneIfExists = pushMapValueIfTruthy(genreById);
const getGenreNames = (ids) => {
result = [];
ids.forEach(pushGenreNaneIfExists(result));
return result;
};
console.log(getGenreNames(genres));

Nested recursive object loop

I have arrays of objects that can also have arrays of their own. My main goal is to find an object with a given id in the whole tree and get readmap to that element by displaying the names of the objects names where it occurs.
For example I have data object like this:
{
id: '0',
name: "Boys"
children: [
{
name: "Soldiers",
children: [
{
name: "Bravo"
children: [
{name: "Tom"},
{name: "Andrew"}
]
}
]
},
{
name: "Runners",
children: [
{
name: "Team B"
children: [
{name: "Mark"},
{name: "David"}
]
}
]
}
]
}
I am currently finding an item by a function
function findByName (name, array) {
for (const node of array) {
if (node.name === name) return node;
if (node.children) {
const child = findByName(name, node.children);
if (child) return child;
}
}
}
But to achive my goal I need also roadmap to that value. For example.
When I want to find "Tom". Besides results of findByName I would like to get {name: "Tom", road: ["Boys", "Soldiers", "Bravo"]
You would need to pass down another property which handles the path. Start by defining path as an empty array. And since you only care about the name, you can push the name into this array everytime you find a node that has children.
Then you just keep passing the updated array to your recursive function. See my working example below:
(I updated your function to return an object which contains both the result and path)
function findByName(name, array, path = []) {
for (const node of array) {
if (node.name === name) return {result: node, path};
if (node.children) {
path.push(node.name) // We update the path with the current node name that has children
const child = findByName(name, node.children, path );
if (child) return { result: child, path};
}
}
}
Demo:
https://jsitor.com/VnktoLq49
You could add the path for every leve in the calling function without handing over the path.
const
findByName = (array, name) => {
for (const node of array) {
if (node.name === name) return { ...node, path: [] };
if (node.children) {
const child = findByName(node.children, name);
if (child) return { ...child, path: [node.name, ...child.path] };
}
}
},
data = [{ id: '0', name: "Boys", children: [{ name: "Soldiers", children: [{ name: "Bravo", children: [{ name: "Tom" }, { name: "Andrew" }] }] }, { name: "Runners", children: [{ name: "Team B", children: [{ name: "Mark" }, { name: "David" }] }] }] }];
console.log(findByName(data, 'Tom'));
.as-console-wrapper { max-height: 100% !important; top: 0; }
I like generators for this kind or problem because it allows you to select one, many, or all results. Additionally generators give control to the caller, allowing you to stop searching whenever you are satisfied with the result. This can be accomplished with a single function -
function* select(a = [], query = Boolean, path = [])
{ for (const t of a)
{ if (query(t)) yield { ...t, path }
yield *select(t.children, query, [...path, t.name])
}
}
const data =
[{ id: '0', name: "Boys", children: [{ name: "Soldiers", children: [{ name: "Bravo", children: [{ name: "Tom" }, { name: "Andrew" }] }] }, { name: "Runners", children: [{ name: "Team B", children: [{ name: "Mark" }, { name: "David" }] }] }] }]
// select "Tom" OR "Mark"
for (const r of select(data, v => v.name == 'Tom' || v.name == "Mark"))
console.log("found:", r)
found: {
"name": "Tom",
"path": [
"Boys",
"Soldiers",
"Bravo"
]
}
found: {
"name": "Mark",
"path": [
"Boys",
"Runners",
"Team B"
]
}
If you want only the first result, we can use return or break, and searching stops immediately, potentially saving many wasted computations -
function first (it)
{ for (const x of it)
return x // <- return and stop searching
}
first(select(data, v => v.name == "Andrew"))
{
"name": "Andrew",
"path": [
"Boys",
"Soldiers",
"Bravo"
]
}
If you want all of the results, we can use Array.from. Because select is flexible, it allows us to do all sorts of useful queries -
Array.from(select(data, v => !v.children), r => r.name)
[
"Tom",
"Andrew",
"Mark",
"David"
]

Categories