Create nested object from multiple string paths - javascript

I'm looking for the best way to convert multiple string paths to a nested object with javascript. I'm using lodash if that could help in any way.
I got the following paths:
/root/library/Folder 1
/root/library/Folder 2
/root/library/Folder 1/Document.docx
/root/library/Folder 1/Document 2.docx
/root/library/Folder 2/Document 3.docx
/root/library/Document 4.docx
and I would like to create the following array of object:
var objectArray =
[
{
"name": "root", "children": [
{
"name": "library", "children": [
{
"name": "Folder 1", "children": [
{ "name": "Document.docx", "children": [] },
{ "name": "Document 2.docx", "children": [] }
]
},
{
"name": "Folder 2", "children": [
{ "name": "Document 3.docx", "children": [] }
]
},
{
"name": "Document 4.docx", "children": []
}
]
}
]
}
];

I suggest implementing a tree insertion function whose arguments are an array of children and a path. It traverses the children according to the given path and inserts new children as necessary, avoiding duplicates:
// Insert path into directory tree structure:
function insert(children = [], [head, ...tail]) {
let child = children.find(child => child.name === head);
if (!child) children.push(child = {name: head, children: []});
if (tail.length > 0) insert(child.children, tail);
return children;
}
// Example:
let paths = [
'/root/library/Folder 1',
'/root/library/Folder 2',
'/root/library/Folder 1/Document.docx',
'/root/library/Folder 1/Document 2.docx',
'/root/library/Folder 2/Document 3.docx',
'/root/library/Document 4.docx'
];
let objectArray = paths
.map(path => path.split('/').slice(1))
.reduce((children, path) => insert(children, path), []);
console.log(objectArray);

Iterate over each string and resolve it to an object:
var glob={name:undefined,children:[]};
["/root/library/Folder 1","/root/library/Folder 2","/root/library/Folder 1/Document.docx","/root/library/Folder 1/Document 2.docx","/root/library/Folder 2/Document 3.docx","/root/library/Document 4.docx"]
.forEach(function(path){
path.split("/").slice(1).reduce(function(dir,sub){
var children;
if(children=dir.children.find(el=>el.name===sub)){
return children;
}
children={name:sub,children:[]};
dir.children.push(children);
return children;
},glob);
});
console.log(glob);
http://jsbin.com/yusopiguci/edit?console
Improved version:
var glob={name:undefined,children:[]};
var symbol="/" /* or Symbol("lookup") in modern browsers */ ;
var lookup={[symbol]:glob};
["/root/library/Folder 1","/root/library/Folder 2","/root/library/Folder 1/Document.docx","/root/library/Folder 1/Document 2.docx","/root/library/Folder 2/Document 3.docx","/root/library/Document 4.docx"]
.forEach(function(path){
path.split("/").slice(1).reduce(function(dir,sub){
if(!dir[sub]){
let subObj={name:sub,children:[]};
dir[symbol].children.push(subObj);
return dir[sub]={[symbol]:subObj};
}
return dir[sub];
},lookup);
});
console.log(glob);
It creates the same result but it is may much faster ( up to O(n) vs. O(n+n!))
http://jsbin.com/xumazinesa/edit?console

Related

Dynamically create multidimensional array from split input

I have an array of ojects which all have a path and a name property.
Like
[
{
"id": "1",
"path": "1",
"name": "root"
},
{
"id": "857",
"path": "1/857",
"name": "Animals"
},
{
"id": "1194",
"path": "1/857/1194",
"name": "Dinasours"
},
...and so on
]
Here are some path examples
1/1279/1282
1/1279/1281
1/1279/1280
1/857
1/857/1194
1/857/1194/1277
1/857/1194/1277/1278
I want to turn this into a multidimensional array like:
const data = {
id: "1",
name: "Root",
children: [
{
id: "1279",
name: "Toys",
},
{
id: "857",
name: "Animals",
children: [
{
id: "1194",
name: "Dinasours",
children: [
{
id: "1277",
name: "T-Rex",
children: [
{
id: "1278",
name: "Superbig T-Rex",
},
],
},
],
},
],
},
],
};
As you can understand the amount of data is much larger.
Is there a neat way to transform this data?
I wonder if this would be sufficient for your needs?
I'll refer to the objects as nodes (just because I'm a graph theory person, and that's how I roll).
Build an index mapping each id to the object itself using a Map. (Purely for efficiency. You could technically find each node from scratch by id each time you need it.)
Split the path to obtain the second last path fragment which should be the id of the direct parent of the node. (Assuming there's only one and that there is guaranteed to be a node corresponding to that id?)
Add the child to the parent's list of children. We'll be careful not to add it multiple times.
This will result in nodes that have no children literally having no children property (as opposed to having a children property that is just []). I also did not remove/delete the path property from the objects.
As a note of caution, if there are path fragments that do not have corresponding objects, this will not work.
const nodes = [
{ id: '1', path: '1', name: 'root' },
{ id: '857', path: '1/857', name: 'Animals' },
{ id: '1194', path: '1/857/1194', name: 'Dinasours' }
//...and so on
];
const index = new Map();
for (let node of nodes) {
index.set(node.id, node)
}
for (let node of nodes) {
const fragments = node.path.split('/');
const parentId = fragments[fragments.length - 2];
const parent = index.get(parentId);
if (parent !== undefined) {
parent.children = parent.children || [];
if (!parent.children.includes(node)) {
parent.children.push(node);
}
}
}
// TODO: Decide which node is the root.
// Here's one way to get the first (possibly only) root.
const root = index.get(nodes[0].path.split('/')[0]);
console.dir(root, { depth: null });
Assuming that the root is always the same I came up with this code, it took me some time but it was fun to think about it.
var data = {};
list.forEach(item => {
var path = item.path.split("/");
let parent = data;
path.forEach((id) => {
if (!parent.id) {
parent.id = id;
parent.children = [];
if (id != item.id) {
let next = {}
parent.children.push(next);
parent = next;
}
} else if (parent.id != id) {
let next = parent.children.find(child => child.id == id);
if (!next) {
next = { id: id, children: [] }
parent.children.push(next);
}
parent = next;
}
});
parent.id = item.id;
parent.name = item.name
});
output:
{
"id": "1",
"children": [
{
"id": "857",
"children": [
{
"id": "1194",
"children": [
{
"id": "1277",
"children": [
{ "id": "1278", "children": [], "name": "Superbig T-Rex" }
],
"name": "T-Rex"
}
],
"name": "Dinasours"
}
],
"name": "Animals"
},
{ "id": "1279", "children": [], "name": "Toys" }
],
"name": "Root"
}
I think that having more roots here may need some fixing. Although I think the problem would be different if we were talking about multiple roots since your data variable is an object
Also, if you think in a recursive way it can be more understandable, but no comments on performance.

Find branches in nested Object in Javascript

I have tried to find a solution for my problem in the last two hours, which included trying myself, scanning lodash docs as well as SO for any suitable answer, but did not come up with anything remotely working or practical. I would be very grateful for help.
I have an object that can be any depth.
e.g.
{
"name": "alpha",
"children": [
{
"name": "beta",
"children": [
{
"name": "gamma",
"children": [
{
"name": "delta",
"children": []
}
]
}
]
},
{
"name": "epsilon",
"children": [
{
"name": "zeta",
"children": [
{
"name": "eta",
"children": []
}
]
}
]
}
]
}
I am looking for a function which will return the whole branch of this object where there is a matching name (if possible without lodash but if really needed its ok).
Given an input of 'gamma' I expect it to return
{
"name": "alpha",
"children": [
{
"name": "beta",
"children": [
{
"name": "gamma",
"children": [
{
"name": "delta",
"children": []
}
]
}
]
}
]
}
Given an input 't' I expect it to return the whole object, since it is included in the names of children in both branches.
You can separate this problem into two parts, first let's test if the name is present in the tree:
function hasStr(item, str) {
return item.name.includes(str) || item.children.some(x => hasStr(x, str));
}
hasStr(item, 'gamma'); // true
You also asked to have a function that returns the passed object and return only a filtered version of the children:
function possibleAnswer(item, str) {
return {
name: item.name,
children: item.children.filter(x => hasStr(x, str)),
};
}
possibleAnswer(item, 'gamma'); // will return desired object
This problem can be solved with recursion as the depth is not know.
getNames = (nameChild, match) => {
if (nameChild.length < 1) return false;
if (nameChild.name.includes(match)) return true;
k = nameChild.children
.filter((child) =>
getNames(child, match))
return (k.length > 0) ? k : false;
}
suppose the object is assign to nameObj variable then
nameObj.children = getNames(nameObj, 'zeta');
this will do the work!

Reverse Traverse a hierarchy

I have a hierarchy of objects that contain the parent ID on them. I am adding the parentId to the child object as I parse the json object like this.
public static fromJson(json: any): Ancestry | Ancestry[] {
if (Array.isArray(json)) {
return json.map(Ancestry.fromJson) as Ancestry[];
}
const result = new Ancestry();
const { parents } = json;
parents.forEach(parent => {
parent.parentId = json.id;
});
json.parents = Parent.fromJson(parents);
Object.assign(result, json);
return result;
}
Any thoughts on how to pull out the ancestors if I have a grandchild.id?
The data is on mockaroo curl (Ancestries.json)
As an example, with the following json and a grandchild.id = 5, I would create and array with the follow IDs
['5', '0723', '133', '1']
[{
"id": "1",
"name": "Deer, spotted",
"parents": [
{
"id": "133",
"name": "Jaime Coldrick",
"children": [
{
"id": "0723",
"name": "Ardys Kurten",
"grandchildren": [
{
"id": "384",
"name": "Madelle Bauman"
},
{
"id": "0576",
"name": "Pincas Maas"
},
{
"id": "5",
"name": "Corrie Beacock"
}
]
},
There is perhaps very many ways to solve this, but in my opinion the easiest way is to simply do a search in the data structure and store the IDs in inverse order of when you find them. This way the output is what you are after.
You could also just reverse the ordering of a different approach.
I would like to note that the json-structure is a bit weird. I would have expected it to simply have nested children arrays, and not have them renamed parent, children, and grandchildren.
let data = [{
"id": "1",
"name": "Deer, spotted",
"parents": [
{
"id": "133",
"name": "Jaime Coldrick",
"children": [
{
"id": "0723",
"name": "Ardys Kurten",
"grandchildren": [
{
"id": "384",
"name": "Madelle Bauman"
},
{
"id": "0576",
"name": "Pincas Maas"
},
{
"id": "5",
"name": "Corrie Beacock"
}
]
}]
}]
}]
const expectedResults = ['5', '0723', '133', '1']
function traverseInverseResults(inputId, childArray) {
if(!childArray){ return }
for (const parent of childArray) {
if(parent.id === inputId){
return [parent.id]
} else {
let res = traverseInverseResults(inputId, parent.parents || parent.children || parent.grandchildren) // This part is a bit hacky, simply to accommodate the strange JSON structure.
if(res) {
res.push(parent.id)
return res
}
}
}
return
}
let result = traverseInverseResults('5', data)
console.log('results', result)
console.log('Got expected results?', expectedResults.length === result.length && expectedResults.every(function(value, index) { return value === result[index]}))

Merging an array of objects without overwriting

I am currently with some JSON, which has to be structured in a tree-like hierarchy. The depth of the hierarchy varies a lot, and is therefor unknown.
As it is right now, I have achieved to get an array of objects. Example is below.
[
{
"name": "level1",
"collapsed": true,
"children": [
{
"name": "Level 1 item here",
"id": 360082134191
}
]
},
{
"name": "level1",
"collapsed": true,
"children": [
{
"name": "level2",
"collapsed": true,
"children": [
{
"name": "Level 2 item here",
"id": 360082134751
}
]
}
]
},
{
"name": "level1",
"collapsed": true,
"children": [
{
"name": "Another level 1 item",
"id": 360082262772
}
]
}
]
What I want to achieve is these objects to be merged, without overwriting or replacing anything. Listed below is an example of how I want the data formatted:
[
{
"name": "level1",
"collapsed": true,
"children": [
{
"name": "level2",
"collapsed": true,
"children": [
{
"name": "Level 2 item here",
"id": 360082134751
}
]
},
{
"name": "Level 1 item here",
"id": 360082134191
},
{
"name": "Another level 1 item",
"id": 360082262772
}
]
}
]
How would I achieve this with JavaScript? No libraries is preferred, ES6 can be used though.
Edit:
It is important that the output is an array, since items without children can appear at the root.
I am assuming you need a little help on working with the data. There could be multiple ways to achieve this, here is how would I do.
// data => supplied data
const result = data.reduce ((acc, item) => {
// if acc array already contains an object with same name,
// as current element [item], merfe the children
let existingItem;
// Using a for loop here to create a reference to the
// existing item, so it'd update this item when childrens
// will be merged.
for (let index = 0; index < acc.length; index ++) {
if (acc[index].name === item.name) {
existingItem = acc[index];
break;
}
}
// if existingItem exists, merge children of
// existing item and current item.
// else push it into the accumulator
if (existingItem) {
existingItem.children = existingItem.children.concat(item.children);
} else {
acc.push (item);
}
return acc;
}, []);
I'm assuming you want to group based on the name property in the level 1 object. You could do a simple reduce and Object.values like this:
const input = [{"name":"level1","collapsed":true,"children":[{"name":"Level 1 item here","id":360082134191}]},{"name":"level1","collapsed":true,"children":[{"name":"level2","collapsed":true,"children":[{"name":"Level 2 item here","id":360082134751}]}]},{"name":"level1","collapsed":true,"children":[{"name":"Another level 1 item","id":360082262772}]}]
const merged = input.reduce((r,{name, collapsed, children}) =>{
r[name] = r[name] || {name, collapsed, children:[]};
r[name]["children"].push(...children)
return r;
}, {})
const final = Object.values(merged);
console.log(final)
You could do the whole thing in one line:
const input = [{"name":"level1","collapsed":true,"children":[{"name":"Level 1 item here","id":360082134191}]},{"name":"level1","collapsed":true,"children":[{"name":"level2","collapsed":true,"children":[{"name":"Level 2 item here","id":360082134751}]}]},{"name":"level1","collapsed":true,"children":[{"name":"Another level 1 item","id":360082262772}]}]
const output = Object.values(input.reduce((r,{name,collapsed,children}) => (
(r[name] = r[name] || {name,collapsed,children: []})["children"].push(...children), r), {}))
console.log(output)

Rename json keys iterative

I got a very simple json but in each block I got something like this.
var json = {
"name": "blabla"
"Children": [{
"name": "something"
"Children": [{ ..... }]
}
And so on. I don't know how many children there are inside each children recursively.
var keys = Object.keys(json);
for (var j = 0; j < keys.length; j++) {
var key = keys[j];
var value = json[key];
delete json[key];
key = key.replace("Children", "children");
json[key] = value;
}
And now I want to replace all "Children" keys with lowercase "children". The following code only works for the first depth. How can I do this recursively?
It looks the input structure is pretty well-defined, so you could simply create a recursive function like this:
function transform(node) {
return {
name: node.name,
children: node.Children.map(transform)
};
}
var json = {
"name": "a",
"Children": [{
"name": "b",
"Children": [{
"name": "c",
"Children": []
}, {
"name": "d",
"Children": []
}]
}, {
"name": "e",
"Children": []
}]
};
console.log(transform(json));
A possible solution:
var s = JSON.stringify(json);
var t = s.replace(/"Children"/g, '"children"');
var newJson = JSON.parse(t);
Pros: This solution is very simple, being just three lines.
Cons: There is a potential unwanted side-effect, consider:
var json = {
"name": "blabla",
"Children": [{
"name": "something",
"Children": [{ ..... }]
}],
"favouriteWords": ["Children","Pets","Cakes"]
}
The solution replaces all instances of "Children", so the entry in the favouriteWords array would also be replaced, despite not being a property name. If there is no chance of the word appearing anywhere else other than as the property name, then this is not an issue, but worth raising just in case.
Here is a function that can do it recursivly:
function convertKey(obj) {
for (objKey in obj)
{
if (Array.isArray(obj[objKey])) {
convertKey[objKey].forEach(x => {
convertKey(x);
});
}
if (objKey === "Children") {
obj.children = obj.Children;
delete obj.Children;
}
}
}
And here is a more generic way for doing this:
function convertKey(obj, oldKey, newKey) {
for (objKey in obj)
{
if (Array.isArray(obj[objKey])) {
obj[objKey].forEach(objInArr => {
convertKey(objInArr);
});
}
if (objKey === oldKey) {
obj[newKey] = obj[oldKey];
delete obj[oldKey];
}
}
}
convertKey(json, "Children", "children");
Both the accepted answer, and #Tamas answer have slight issues.
With #Bardy's answer like he points out, there is the issue if any of your values's had the word Children it would cause problems.
With #Tamas, one issue is that any other properties apart from name & children get dropped. Also it assumes a Children property. And what if the children property is already children and not Children.
Using a slightly modified version of #Tamas, this should avoid the pitfalls.
function transform(node) {
if (node.Children) node.children = node.Children;
if (node.children) node.children = node.children.map(transform);
delete node.Children;
return node;
}
var json = {
"name": "a",
"Children": [{
"age": 13,
"name": "b",
"Children": [{
"name": "Mr Bob Chilren",
"Children": []
}, {
"name": "d",
"age": 33, //other props keep
"children": [{
"name": "already lowecased",
"age": 44,
"Children": [{
"name": "now back to upercased",
"age": 99
}]
}] //what if were alrady lowercased?
}]
}, {
"name": "e",
//"Children": [] //what if we have no children
}]
};
console.log(transform(json));

Categories

Resources