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
I am trying to create a nested object recursively. Here is the sample data:
"reactions": [
{
"name": "Ester amidation",
"target": "Data #1",
"sources": ["Data #2", "Data #3"],
},
{
"name": "Buchwald-Hartwig amination with amide",
"target": "Data #4",
"sources": ["Data #5", "Data #1"], // BECAUSE Data #1 is a target AND in sources for Data #4, we should nest it as a child of Data #4
}
]
Given the target, I was trying to have something that will output something like:
{
name: "My Route 1",
children: [
{
name: "Data #4",
children: [
{
name: "Data #5",
children: []
},
{
name: "Data #1",
children: [
{
name: "Data #2",
children: []
},
{
name: "Data #3",
children: []
}
]
},
],
},
],
};
I have tried something like this but I get confused when it comes to handling arrays:
function createNestedTree(objects, target = null) {
const children = objects.filter(o => o.target === target);
if (!children.length) {
return null;
}
return children.map(child => ({
...child,
children: createNestedTree(objects, child.id)
}));
}
Anyone have an idea of how to create an algorithm for this? Much appreciated!
You could first iterate the data to create the target objects (with empty children arrays), and reference them in a Map keyed by name. Then iterate the data to populate those children arrays, and at the same time mark children as not being a root. Return the remaining root(s).
Here is a possible implementation:
function makeTree(reactions) {
const map = new Map(reactions.flatMap(({target, sources}) =>
[target, ...sources].map(name => [name, { name, children: [] }])
));
const roots = new Map(map);
for (const {target, sources} of reactions) {
for (const source of sources) {
map.get(target).children.push(map.get(source));
roots.delete(source);
}
}
return [...roots.values()];
}
const reactions = [{"name": "Ester amidation","target": "Data #1","sources": ["Data #2", "Data #3"],},{"name": "Buchwald-Hartwig amination with amide","target": "Data #4","sources": ["Data #5", "Data #1"]}];
const result = makeTree(reactions);
console.log(result);
how does one go about inserting an item into a nested javascript array of objects (with and without using a library)? running to a problem where once you insert the item after traversing, how would you reassign it back to the original object without manually accessing the object like data.content[0].content[0].content[0] etc..? already tried Iterate through Nested JavaScript Objects but could not get the reassignment to work
const data = {
"content": [
{
"name": "a",
"content": [
{
"name": "b",
"content": [
{
"name": "c",
"content": []
}
]
}
]
}
]
}
inserting {"name": "d", "content": []} into the contents of c
const data = {
"content": [
{
"name": "a",
"content": [
{
"name": "b",
"content": [
{
"name": "c",
"content": [{"name": "d", "content": []}]
}
]
}
]
}
]
}
const data = {
"content": [{
"name": "a",
"content": [{
"name": "b",
"content": [{
"name": "c",
"content": []
}]
}]
}]
}
const insert = createInsert(data)
insert({
"name": "d",
"content": []
}, 'c')
console.log(data)
// create a new function that will be able to insert items to the object
function createInsert(object) {
return function insert(obj, to) {
// create a queue with root data object
const queue = [object]
// while there are elements in the queue
while (queue.length) {
// remove first element from the queue
const current = queue.shift()
// if name of the element is the searched one
if (current.name === to) {
// push the object into the current element and break the loop
current.content.push(obj)
break
}
// add child elements to the queue
for (const item of current.content) {
queue.push(item)
}
}
}
}
It looks like we should assume that the name property uniquely identifies an object in the data structure. With that assumption you could create a mapping object for it, so to map a given name to the corresponding object in the nested structure. Also keep track which is the parent of a given object.
All this meta data can be wrapped in a decorator function, so that the data object gets some capabilities to get, add and remove certain names from it, no matter where it is in the hierarchy:
function mappable(data) {
const map = { "__root__": { content: [] } };
const parent = {};
const dfs = (parentName, obj) => {
parent[obj.name] = parentName;
map[obj.name] = obj;
obj.content?.forEach?.(child => dfs(obj.name, child));
}
Object.defineProperties(data, {
get: { value(name) {
return map[name];
}},
add: { value(parentName, obj) {
this.get(parentName).content.push(obj);
dfs(parentName, obj);
}},
remove: { value(name) {
map[parent[name]].content = map[parent[name]].content.filter(obj =>
obj.name != name
);
delete map[name];
delete parent[name];
}}
});
data.add("__root__", data);
}
// Demo
const data = {"content": [{"name": "a","content": [{"name": "b","content": [{"name": "c","content": []}]}]}]};
mappable(data);
data.add("c", { name: "d", content: [] });
console.log(data);
console.log(data.get("d")); // { name: "d", content: [] }
data.remove("d");
console.log(data.get("d")); // undefined
console.log(data); // original object structure
I have an array of objects that determine which ones should be showed first. An example of this array would be:
[
{
"id": "b94ae1a5-c6b2-4e45-87cd-a4036fdb7870",
"prerequisites_ids": [
"2a4fdd9c-45d0-49d9-a0eb-ba5a0464f2b1"
]
},
{
"id": "ef7d2415-808f-4efc-939e-2692f38a5ee7",
"prerequisites_ids": [
"74e41a2c-e74e-4016-bb2c-f2e84c04fe92"
]
},
{
"id": "74e41a2c-e74e-4016-bb2c-f2e84c04fe92",
"prerequisites_ids": []
},
{
"id": "2a4fdd9c-45d0-49d9-a0eb-ba5a0464f2b1",
"prerequisites_ids": [
"ef7d2415-808f-4efc-939e-2692f38a5ee7"
]
}
]
How could I sort it to get this?
[
{
"id": "74e41a2c-e74e-4016-bb2c-f2e84c04fe92",
"prerequisites_ids": []
},
{
"id": "ef7d2415-808f-4efc-939e-2692f38a5ee7",
"prerequisites_ids": [
"74e41a2c-e74e-4016-bb2c-f2e84c04fe92"
]
},
{
"id": "2a4fdd9c-45d0-49d9-a0eb-ba5a0464f2b1",
"prerequisites_ids": [
"ef7d2415-808f-4efc-939e-2692f38a5ee7"
]
},
{
"id": "b94ae1a5-c6b2-4e45-87cd-a4036fdb7870",
"prerequisites_ids": [
"2a4fdd9c-45d0-49d9-a0eb-ba5a0464f2b1"
]
}
]
I have tried creating a custom function:
export function comparePrerequisites(a, b) {
if (!a.prerequisites_ids) {
return -1
}
if (a.prerequisites_ids.includes(b.id)) {
return 1;
}
}
data.sort(comparePrerequisites)
but does not seem to work. Thanks in advance!
We have here the requirements for a topological sort. This is not a job for the sort method. Instead you can use recursion (a DFS traversal) to drill down to a dependency that is already collected, or to a leaf (no dependencies).
Here is an implementation:
function topologicalSort(tasks) {
const visited = new Set;
const taskMap = new Map(tasks.map(task => [task.id, task]));
function dfs(tasks) {
for (let task of tasks) {
if (!visited.has(task.id)) {
dfs(task.prerequisites_ids.map(id => taskMap.get(id)));
}
visited.add(task);
}
}
dfs(tasks);
return [...visited];
}
// Demo on your example:
let tasks = [{"id": "b94ae1a5-c6b2-4e45-87cd-a4036fdb7870","prerequisites_ids": ["2a4fdd9c-45d0-49d9-a0eb-ba5a0464f2b1"]},{"id": "ef7d2415-808f-4efc-939e-2692f38a5ee7","prerequisites_ids": ["74e41a2c-e74e-4016-bb2c-f2e84c04fe92"]},{"id": "74e41a2c-e74e-4016-bb2c-f2e84c04fe92","prerequisites_ids": []},{"id": "2a4fdd9c-45d0-49d9-a0eb-ba5a0464f2b1","prerequisites_ids": ["ef7d2415-808f-4efc-939e-2692f38a5ee7"]}];
console.log(topologicalSort(tasks));
I found the question How to convert a file path into treeview?, but I'm not sure how to get the desired result in JavaScript:
I'm trying to turn an array of paths into a JSON tree:
var paths = [
"/org/openbmc/UserManager/Group",
"/org/stackExchange/StackOverflow",
"/org/stackExchange/StackOverflow/Meta",
"/org/stackExchange/Programmers",
"/org/stackExchange/Philosophy",
"/org/stackExchange/Religion/Christianity",
"/org/openbmc/records/events",
"/org/stackExchange/Religion/Hinduism",
"/org/openbmc/HostServices",
"/org/openbmc/UserManager/Users",
"/org/openbmc/records/transactions",
"/org/stackExchange/Religion/Islam",
"/org/openbmc/UserManager/Groups",
"/org/openbmc/NetworkManager/Interface"
];
I want to have json structure like below using the folder paths.
var xyz = [{
"path": "photos",
"name": "photos",
"children": [
{
"path": "photos/summer",
"name": "summer",
"children": [
{
"path": "photos/summer/june",
"name": "june",
"children": [
{
"path": "photos/summer/june/windsurf",
"name": "windsurf",
}
]
}
]
},
{
"path": "photos/winter",
"name": "winter",
"children": [
{
"path": "photos/winter/january",
"name": "january",
"children": [
{
"path": "photos/winter/january/ski",
"name": "ski",
},
{
"path": "photos/winter/january/snowboard",
"name": "snowboard",
}
]
}
]
}
]
}];
I have used below function but it's not working
var parsePathArray = function(paths) {
var parsed = [];
for (var i = 0; i < paths.length; i++) {
var position = parsed;
var split = paths[i].split('/');
for (var j = 0; j < split.length; j++) {
if (split[j] !== "") {
if (typeof position[split[j]] === 'undefined')
position[split[j]] = {};
position.children = [position[split[j]]];
position.name = split[j];
position = position[split[j]];
}
}
}
return parsed;
}
Disclaimer: I wrote this answer because it's a fun exercise. I'm still disappointed in you for not trying and not taking the time to explain what it is you don't understand...
I didn't follow your exact format so you'll have to try to understand how it's done instead of being able to copy the code and leave :)
I'll touch upon each step briefly to not risk explaining what you already know.
Step 1:
Go from a list of strings to a list of arrays:
["a/1", "a/2", "b/1"] -> [["a", "1"], ["a", "2"], ["b", "1"]]
We use String.prototype.slice to remove the prepended "/" and String.prototype.split with your folder delimiter to convert to an array: path.split("/")
Step 2
Loop over each folder and add the folder to an object.
[["a", "1"], ["a", "2"], ["b", "1"]] -> { a: { 1: {}, 2: {} }, b: { 1: {} } }
We use a reducer that accesses an object using bracket notation obj[key] instantiating new folder objects and returning the deepest location along the way.
Step 3
Recursively loop over the keys of your object and convert to a specified format:
{ a: { 1: { } } -> { name: "a", path: [], children: [ /* ... */ ] }
We take a list of keys, which are folder names, using Object.keys. Recursively call for each nested object.
Please, update your answer with the specific step you have trouble with which allows others to help as well, and me to describe the step in more detail.
const pathStrings = ["/org/openbmc/UserManager/Group", "/org/stackExchange/StackOverflow", "/org/stackExchange/StackOverflow/Meta", "/org/stackExchange/Programmers", "/org/stackExchange/Philosophy", "/org/stackExchange/Religion/Christianity", "/org/openbmc/records/events", "/org/stackExchange/Religion/Hinduism", "/org/openbmc/HostServices", "/org/openbmc/UserManager/Users", "/org/openbmc/records/transactions", "/org/stackExchange/Religion/Islam", "/org/openbmc/UserManager/Groups", "/org/openbmc/NetworkManager/Interface"];
const paths = pathStrings
.map(str => str.slice(1)) // remove first "/"
.map(str => str.split("/"));
// Mutates map!
const mergePathInToMap = (map, path) => {
path.reduce(
(loc, folder) => (loc[folder] = loc[folder] || {}, loc[folder]),
map
);
return map;
};
// Folder structure as { folderName: folderContents }
const folderMap = paths.reduce(mergePathInToMap, {});
// Go from
// { folderName: folderContents }
// to a desired format like
// { name: folderName, children: [contents] }
const formatStructure = (folder, path) => {
return Object
.keys(folder)
.map(k => ({
name: k,
path: path,
children: formatStructure(folder[k], path.concat(k))
}))
}
console.log(
JSON.stringify(
formatStructure(folderMap, []),
null,
2
)
)
.as-console-wrapper { min-height: 100% }
I wish to filter a nested javascript object by the value of the "step" key:
var data = {
"name": "Root",
"step": 1,
"id": "0.0",
"children": [
{
"name": "first level child 1",
"id": "0.1",
"step":2,
"children": [
{
"name": "second level child 1",
"id": "0.1.1",
"step": 3,
"children": [
{
"name": "third level child 1",
"id": "0.1.1.1",
"step": 4,
"children": []},
{
"name": "third level child 2",
"id": "0.1.1.2",
"step": 5,
"children": []}
]},
]}
]
};
var subdata = data.children.filter(function (d) {
return (d.step <= 2)});
This just returns the unmodified nested object, even if I put value of filter to 1.
does .filter work on nested objects or do I need to roll my own function here, advise and correct code appreciated.
cjm
Recursive filter functions are fairly easy to create. This is an example, which strips a JS object of all items defined ["depth","x","x0","y","y0","parent","size"]:
function filter(data) {
for(var i in data){
if(["depth","x","x0","y","y0","parent","size"].indexOf(i) != -1){
delete data[i];
} else if (i === "children") {
for (var j in data.children) {
data.children[j] = filter(data.children[j])
}
}
}
return data;
}
If you would like to filter by something else, just updated the 2nd line with your filter function of choice.
Here's the function to filter nested arrays:
const filter = arr => condition => {
const res = [];
for (const item of arr) {
if (condition(item)) {
if (!item.children) {
res.push({ ...item });
} else {
const children = filter(item.children)(condition);
res.push({ ...item, children })
}
}
}
return res;
}
The only thing you have to do is to wrap your root object into an array to reach self-similarity. In common, your input array should look like this:
data = [
{ <...>, children: [
{ <...>, children: [...] },
...
] },
...
]
where <...> stands for some properties (in your case those are "name", "step" and "id"), and "children" is an optional service property.
Now you can pass your wrapped object into the filter function alongside a condition callback:
filter(data)(item => item.step <= 2)
and you'll get your structure filtered.
Here are a few more functions to deal with such structures I've just coded for fun:
const map = arr => f => {
const res = [];
for (const item of arr) {
if (!item.children) {
res.push({ ...f({ ...item }) });
} else {
res.push({ ...f({ ...item }), children: map(item.children)(f) });
}
}
return res;
}
const reduce = arr => g => init => {
if (!arr) return undefined;
let res = init;
for (const item of arr) {
if (!item.children) {
res = g(res)({ ...item });
} else {
res = g(res)({ ...item });
res = reduce(item.children)(g)(res);
}
}
return res;
}
Usage examples:
map(data)(item => ({ step: item.step }))
reduce(data)($ => item => $ + item.step)(0)
Likely, the code samples aren't ideal but probably could push someone to the right direction.
Yes, filter works on one array (list), like the children of one node. You have got a tree, if you want to search the whole tree you will need to use a tree traversal algorithm or you first put all nodes into an array which you can filter. I'm sure you can write the code yourself.