tree from array of dot-separated strings - javascript

I have an array of dot delimited strings which looks like the following
data = [
'Europe.UK.London.TrafalgarSq',
'Europe.UK.London.HydePark',
'Europe.UK.London.OxfordStreet',
'Europe.UK.London.City.Bank',
'Europe.France.Paris',
'Europe.France.Bordeaux'},
]
and I want to build the following tree of of nested objects. In case it matters, this is for a leaflet map where the Tree Layers Control is going to be used
var tree = {
label: 'Places',
selectAllCheckbox: 'Un/select all',
children: [
{
label: 'Europe',
selectAllCheckbox: true,
children: [
{
label: 'Europe.UK',
selectAllCheckbox: true,
children: [
{
label: 'Europe.UK.London',
selectAllCheckbox: true,
children: [
{label: 'Europe.UK.London.TrafalgarSq'},
{label: 'Europe.UK.London.HydePark'},
{label: 'Europe.UK.London.OxfordStreet'},
{
label: 'Europe.UK.London.City',
selectAllCheckbox: true,
children: [
{label: 'Europe.UK.London.City.Bank'},
]
},
]
},
{
label: 'Europe.France',
selectAllCheckbox: true,
children: [
{label: 'Europe.France.Paris'},
{label: 'Europe.France.Bordeaux'},
]
},
]
}
]
}
]
};
How do I do this tree please?

You could use a mapper object which has partial paths (or label) as key and a reference to the object in the tree as it's value. split the path at . and reduce the array with tree as the initialValue. If the path doesn't exist yet, add it to mapper and tree. Return the nested object in each iteration.
const data = ["Europe.UK.London.TrafalgarSq","Europe.UK.London.HydePark","Europe.UK.London.OxfordStreet","Europe.UK.London.City.Bank","Europe.France.Paris","Europe.France.Bordeaux"],
mapper = {},
tree = {
label: 'Places',
selectAllCheckbox: 'Un/select all',
children: []
}
for (const str of data) {
let splits = str.split('.'),
label = '';
splits.reduce((parent, place) => {
if (label)
label += `.${place}`
else
label = place
if (!mapper[label]) {
const o = { label };
mapper[label] = o;
parent.selectAllCheckbox = true
parent.children = parent.children || [];
parent.children.push(o)
}
return mapper[label];
}, tree)
}
console.log(tree)

You could an iterative approach with a reduceing of the nested objects.
var data = ['Europe.UK.London.TrafalgarSq', 'Europe.UK.London.HydePark', 'Europe.UK.London.OxfordStreet', 'Europe.UK.London.City.Bank', 'Europe.France.Paris', 'Europe.France.Bordeaux'],
children = data.reduce((r, s) => {
s.split('.').reduce((q, _, i, a) => {
q.selectAllCheckbox = true;
var label = a.slice(0, i + 1).join('.'),
temp = (q.children = q.children || []).find(o => o.label === label);
if (!temp) q.children.push(temp = { label });
return temp;
}, r);
return r;
}, { children: [] }).children,
tree = { label: 'Places', selectAllCheckbox: 'Un/select all', children };
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Related

Find nested object by Id using for loop in Javascript

I'm trying to find a specific Object in a nested Object by id and wrote this function, which works like a charm:
const findNestedObjById = (tree, myFunction, id) => {
if(tree.attributes.node_id === id){
myFunction(tree)
} else{
if(tree.children){
tree.children.forEach(child => {
findNestedObjById(child, myFunction, id)
});
}
}
};
const doThat = (tree) => {
console.log("Got it: " + tree.name)
}
findNestedObjById(myObj, doThat, "0.1.2.1");
But i want to be able to get the "path" of the object (e.g. myObj.children[0].children[2]) (The children property of my object is an array)
So I wanted to rewrite the function using a fori loop instead of a foreach, so that I could later add the index of the array (saved in i of the fori loop at the time) to a path string.
So I wanted to start with this function:
const findWithFori = (tree, myFunction, id) => {
if(tree.attributes.node_id === id){
myFunction(tree)
} else{
if(tree.children){
for (let i = 0; i < tree.length; i++) {
const child = tree.children[i];
findNestedObjById(child, myFunction, id)
}
}
}
};
But it doenst work, it's able to locate the object by id, if the inital myObj already has the right id, but it doesn't find nested objects, like the first function does and I don't understand why.
If it helps answerign the question, myObj looks like this btw.:
const myObj = {
name: "Mein zweiter Baum",
attributes: {
node_id: "0"
},
children: [
{
name: "Lorem",
attributes: {
node_id: "0.1",
done: true
},
children: [
{
name: "Ipsum",
attributes: {
node_id: "0.1.1",
done: true
},
children: [
{
name: "Dolor",
attributes: {
node_id: "0.1.1.1",
done: false
}
}
]
},
{
name: "Sit",
attributes: {
node_id: "0.1.2",
done: false
},
children: [
{
name: "Anet",
attributes: {
node_id: "0.1.2.1"
}
}
]
}
]
}
]
};
You could return the indices.
If an item is found return an empty array, or undefined. Inside of some get the result of children and if not undefined add the actual index in front of the array.
const
findNestedObjById = (tree, id, callback) => {
if (tree.attributes.node_id === id) {
callback(tree);
return [];
}
if (tree.children) {
let path;
tree.children.some((child, index) => {
path = findNestedObjById(child, id, callback);
if (path) {
path.unshift(index);
return true;
}
});
return path;
}
},
doThat = tree => {
console.log("Got it: " + tree.name);
},
data = { name: "Mein zweiter Baum", attributes: { node_id: "0" }, children: [{ name: "Lorem", attributes: { node_id: "0.1", done: true }, children: [{ name: "Ipsum", attributes: { node_id: "0.1.1", done: true }, children: [{ name: "Dolor", attributes: { node_id: "0.1.1.1", done: false } }] }, { name: "Sit", attributes: { node_id: "0.1.2", done: false }, children: [{ name: "Anet", attributes: { node_id: "0.1.2.1" } }] }] }] }
console.log(findNestedObjById(data, "0.1.2.1", doThat)); // [0, 1, 0]
.as-console-wrapper { max-height: 100% !important; top: 0; }
I would do this by building atop some reusable functions. We can write a function that visits a node and then recursively visits all its children's nodes. To use this for a find, however, we want to be able to stop once its found, so a generator function would make sense here. We can extend a basic version of this 1 to allow each stop to include not only the values, but also their paths.
Then we can layer on a generic find-path-by-predicate function, testing each node it generates until one matches the predicate.
Finally we can easily write a function using this to search by node_id. It might look like this:
function * visit (value, path = []) {
yield {value, path}
for (let i = 0; i < (value .children || []) .length; i ++) {
yield * visit (value .children [i], path .concat (i))
}
}
const findDeepPath = (fn) => (obj) => {
for (let o of visit (obj)) {
if (fn (o .value)) {return o .path}
}
}
const findPathByNodeId = (id) =>
findDeepPath (({attributes: {node_id}}) => node_id === id)
const myObj = {name: "Mein zweiter Baum", attributes: {node_id: "0"}, children: [{name: "Lorem", attributes: {node_id: "0.1", done: true}, children: [{name: "Ipsum", attributes: {node_id: "0.1.1", done: true}, children: [{name: "Dolor", attributes: {node_id: "0.1.1.1", done: false}}]}, {name: "Sit", attributes: {node_id: "0.1.2", done: false}, children: [{name: "Anet", attributes: {node_id: "0.1.2.1"}}]}]}]}
console .log (findPathByNodeId ('0.1.2.1') (myObj)) //=> [0, 1, 0]
If we want to return the node and the path, it's simply a matter of replacing
if (fn (o .value)) {return o .path}
with
if (fn (o .value)) {return o}
and we would get back something like:
{
value: {attributes: {node_id: "0.1.2.1"}, name: "Anet"},
path: [0, 1, 0],
}
1 A basic version for nodes without their paths might look like this:
function * visit (obj) {
yield obj
for (let child of (obj .children || [])) {
yield * visit (child)
}
}
and we might write a generic search for values matching a predicate with
const findDeep = (fn) => (obj) => {
for (let o of visit (obj)) {
if (fn (o)) {return o}
}
}
Layering in the path handling adds some complexity, but not a great deal.

Convert string array to multidimensional array

I have a javascript string array which I want to convert to a multidimensional array:
const maps = [
"local://aaa/bbb/ccc",
"local://aaa/bbb/ddd",
"local://aaa/bbb/eee",
"as://fff/ggg/hhh",
];
I want to convert it to this:
const maps = [
{label: "aaa", children: [
{label: "bbb", children: [
{label: "ccc", children: []},
{label: "ddd", children: []},
{label: "eee", children: []}
]}
]},
{label: "fff", children: [
{label: "ggg", children: [
{label: "hhh", children: []}
]}
]}
];
I've tried to do it like this, but it turns out that its not working correctly and I think this is also not the appropiate way to handle this:
interface DialogItem {
label: string,
children: DialogItem[]
};
const dialogs: string[] = [
"local://aaa/bbb/ccc",
"local://aaa/bbb/ddd",
"local://aaa/bbb/eee",
"as://fff/ggg/hhh",
];
const mapFolder = (dialogs: string[]) => {
const maps: DialogItem[] = [];
for (const dialog of dialogs) {
const dialogStr: string = dialog.replace(/(\w+):\/\//gi, "");
const dialogArr: string[] = dialogStr.split("/");
const parent = maps.find(mapped => mapped.label === dialogArr[0]);
if (parent === undefined) {
maps.push({label: dialogArr[0], children: []});
} else {
dialogArr.shift();
const child = parent.children.find(mapped => mapped.label === dialogArr[0]);
if (child === undefined) {
parent.children.push({label: dialogArr[0], children: []});
} else {
child.children.push({label: dialogArr[1], children: []});
}
}
}
};
mapFolder(dialogs);
You could split the string to get an array of paths. Use a mapper object to keep track of each nested object and it's path. reduce the chunks and return the nested object in each iteration
const maps = [
"local://aaa/bbb/ccc",
"local://aaa/bbb/ddd",
"local://aaa/bbb/eee",
"as://fff/ggg/hhh"
],
mapper = {},
tree = { children: [] } // root object
for (const str of maps) {
let chunks = str.split('//')[1].split("/"),
path = '';
chunks.reduce((parent, label) => {
if (path)
path += `.${label}`
else
path = label
if (!mapper[path]) {
const o = { label, children: [] };
mapper[path] = o;
parent.children.push(o)
}
return mapper[path];
}, tree)
}
console.log(tree.children)
The expected output can be obtained by using nested maps and recursively appending the data to the sub-array. Also, have handled couple of more test cases.
var dialogs = [
"local://aaa/bbb/ccc",
"local://aaa/bbb/ddd",
"local://aaa/bbb/eee",
"local://aaa/iii/jjj",
"",
"as://",
"as://fff/ggg/hhh",
];
var maps = [];
dialogs.map((dialog) => {
const dialogStr = dialog.substr(dialog.indexOf("//") + 2, dialog.length);
const dialogArr = dialogStr.split("/");
var localObj = maps;
dialogArr.map((elem) => {
if (elem.trim() != "") {
var data = localObj.find(ele => ele.label == elem);
if (data) {
localObj = data['children'];
} else {
localObj.push({
label: elem,
children: []
});
localObj = localObj.find(ele => ele.label == elem)['children'];
}
}
});
});
console.log(maps);

JS recursive function with nested children array

i have tree array of nested objects. Depending on the type of element I want to give it the necessary icon.
const treeData = [
{
id: 1,
type: "FOLDER",
children: [
{
id: 2,
type: "FILE"
},
{
id: 2,
type: "FOLDER",
children: []
},
]
}
]
Unlimited number of nesting possible in folders. Output should be like that.
const treeData = [
{
id: 1,
type: "FOLDER",
icon: "folder-icon"
children: [
{
id: 2,
type: "FILE",
icon: "file-icon"
},
{
id: 2,
type: "FOLDER",
children: []
icon: "file-icon"
},
]
}
]
As i understand i should use recursive map function with CHILDREN check. But i can't reach the proper result.
You could use map method and create a recursive function that will take type and convert it lowercase to create icon name and add it to the new object.
const data = [{"id":1,"type":"FOLDER","children":[{"id":2,"type":"FILE"},{"id":2,"type":"FOLDER","children":[]}]}]
function addIcons(data) {
return data.map(({ type, children = [], ...rest }) => {
const o = { ...rest, type }
if(type) o.icon = `${type.toLowerCase()}-icon`;
if (children.length) o.children = addIcons(children)
return o
})
}
console.log(addIcons(data))
You can create a function like this:
const iconCalculator = object => {
if (object.children) object.children = object.children.map(child=>iconCalculator(child))
return {...object, icon: 'whatever'}
}
And then map your tree like this treeData.map(child=>iconCalculator(child))
You could map by using a new object with recursive children.
const
addIcon = o => ({
...o,
icon: `${o.type.toLowerCase()}-icon`,
...(Array.isArray(o.children)
? { children: o.children.map(addIcon) }
: {}
)
}),
treeData = [{ id: 1, type: "FOLDER", children: [{ id: 2, type: "FILE" }, { id: 2, type: "FOLDER", children: [] }] }],
result = treeData.map(addIcon);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can use forEach method something like this
const treeData = [{id: 1,type: "FOLDER",children: [{id: 2,type: "FILE"},{id: 2,type: "FOLDER",children: []},]}]
function addIcon(data) {
if (Array.isArray(data)) {
data.forEach(value => {
value.icon = value.type.toLowerCase() + '-icon'
if (Array.isArray(value.children)) {
addIcon(value.children)
}
})
}
}
addIcon(treeData)
console.log(treeData)
P.S:- This mutates original array if you don't want you can make a copy every time and return the newly created copy

Tree Structure From Underscore Delimited Strings

Good day,
I need to convert strings as such:
Process1_Cat1_Cat2_Value1
Process1_Cat1_Cat2_Value2
Process2_Cat1_Cat2_Value1
into a nested array as such:
var d = [{
text: 'Process1',
children: [{
text: 'Cat1',
children: [{
text: 'Cat2',
children: [{
text: 'Value1'
},
{
text: 'Value2'
}]
}]
}]
},
{
text: 'Process2',
children: [{
text: 'Cat1',
children: [{
text: 'Cat2',
children: [{
text: 'Value1'
}]
}]
}]
},
];
The reason why I need to do this is to make use of a treeview to display my data:
https://www.npmjs.com/package/bootstrap-tree-view
I have looked at the following solution but was not able to get it working due to lowdash library throwing errors on the findWhere function:
Uncaught TypeError: _.findWhere is not a function
http://brandonclapp.com/arranging-an-array-of-flat-paths-into-a-json-tree-like-structure/
See below for the code:
function arrangeIntoTree(paths, cb) {
var tree = [];
// This example uses the underscore.js library.
_.each(paths, function(path) {
var pathParts = path.split('_');
pathParts.shift(); // Remove first blank element from the parts array.
var currentLevel = tree; // initialize currentLevel to root
_.each(pathParts, function(part) {
// check to see if the path already exists.
var existingPath = _.findWhere(currentLevel, {
name: part
});
if (existingPath) {
// The path to this item was already in the tree, so don't add it again.
// Set the current level to this path's children
currentLevel = existingPath.children;
} else {
var newPart = {
name: part,
children: [],
}
currentLevel.push(newPart);
currentLevel = newPart.children;
}
});
});
cb(tree);
}
arrangeIntoTree(paths, function(tree) {
console.log('tree: ', tree);
});
Any help will be appreciated!
You could use an iterative by looking for the text at the actual level. If not found create a new object. Return the children array for the next level until the most nested array. Then add the leaf object.
var data = ['Process1_Cat1_Cat2_Value1', 'Process1_Cat1_Cat2_Value2', 'Process2_Cat1_Cat2_Value1'],
result = data.reduce((r, s) => {
var keys = s.split('_'),
text = keys.pop();
keys
.reduce((q, text) => {
var temp = q.find(o => o.text === text);
if (!temp) {
q.push(temp = { text, children: [] });
}
return temp.children;
}, r)
.push({ text });
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

javascript array of string to deep merged object

I am trying to convert an array of strings (with many more items):
fullRoutes = ['POST /api/v1/user/login','POST /api/v1/user/logout']
Into a deep nested object like this (to use in the following module react-checkbox-tree):
const nodes = [{
value: 'api',
label: 'api',
children: [
{ value: 'v1',
label: 'v1',
children: [
{ value: 'user',
label: 'user',
children: [
{ value: login, label: login},
{ value: logout, label: logout}
]
}
]
}
]
I managed to get to:
fullRoutes.forEach(function(route){
let path = route.split(" ")[1].split("/").filter(function(e){ return e === 0 || e })
let object = {}
path.reduce(function(o, s) {
return o['children'] = {label: s, value: s, children: []}
}, object)
routes.push(object)
})
Which returns the object with the 'children', but I am struggling to merge them correctly
I believe this will work:
fullRoutes = [
'POST /api/v1/user/login',
'POST /api/v1/user/logout',
'POST /api/v2/user/login'
];
routes = [];
fullRoutes.forEach(route => {
let path = route.split(' ')[1].split('/').filter(e => e);
let rs = routes;
for (let i = 0, n = path.length; i < n; i++) {
let seg = path[i];
let segp = path.slice(0, i + 1).join('/');
let node = rs.find(r => r.label == seg);
if (!node)
rs.push(node = {
label: seg,
value: segp,
children: []
});
rs = node.children;
}
});
console.log(routes);
One way is to reduce everything to an object including the children and use the path name as key within the children
Then recursively loop through all children and use Object#values() to convert them from objects to arrays
const fullRoutes = ['POST /api/v1/user/login', 'POST /api/v1/user/logout'];
const tmp = fullRoutes.reduce(function(tmp, route){
let path = route.split(" ")[1].split("/");
path.reduce(function(o, s, i) {
o[s] = o[s] || {label: s, value: s, children: {}};
return o[s].children;
}, tmp);
return tmp;
},{});
const nodes = Object.values(tmp);
nodes.forEach(childrenToArray);
console.log(nodes)
//recursive helper
function childrenToArray(obj) {
obj.children = Object.values(obj.children);
obj.children.forEach(childrenToArray)
}
.as-console-wrapper {max-height: 100%!important;}

Categories