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.
Related
I am trying to generate URLs for pages stored in a MongoDB in node.
Using the following function I want to traverse a javascript object that and display the path to each element.
I am nearly there, but I am stuck - There might even be a better way to do this using using Async (which I must admit, confuses me a bit).
Function: (demo)
function printTree(people, slug) {
for (var p = 0; p < people.length; p++) {
var root = people[p];
slug = slug + root.name + "/";
console.log(slug);
if (root.children.length > 0) {
var childrenCount = root.children.length;
for (var c = 0; c < childrenCount; c++) {
if (root.children[c].children.length > 0) {
printTree(root.children[c].children, slug + root.children[c].name + "/");
}
}
}
}
};
Output:
/michael/
/michael/angela/oscar
/michael/meredith/creed
/michael/meredith/creed/kelly
Expected Output:
/michael/
/michael/angela/
/michael/angela/oscar/
/michael/meredith/
/michael/meredith/creed/
/michael/meredith/kelly/
Object:
[
{
"name": "michael",
...
"children": [
{
"name": "angela",
...
"children": [
{
"name": "oscar",
...
"children": []
}
]
},
{
"name": "meredith",
...
"children": [
{
"name": "creed",
...
"children": []
},
{
"name": "kelly",
...
"children": []
}
]
},
{ ... }
]
}
]
If it helps, the data is stored using nested sets: https://github.com/groupdock/mongoose-nested-set
So there might be a better way to do the above work using nested sets (negating the above object).
Here you go. You don't need a second for loop, since your printTree function is going to loop through everything anyway (demo).
function printTree(people, slug){
slug = slug || '/';
for(var i = 0; i < people.length; i++) {
console.log(slug + people[i].name + '/');
if(people[i].children.length){
printTree(people[i].children, slug + people[i].name + '/')
}
}
}
You could also consider something in ECMA5 like this, in case you have further use of the tree or want to use some a seperator other than /. Nothing wrong with #bioball answer, this just gives you some more flexibility if wanted.
function makeTree(people, slug, sep) {
slug = slug || '/';
sep = sep || slug;
return people.reduce(function (tree, person) {
var slugPerson = slug + person.name + sep;
return tree.concat(slugPerson, makeTree(person.children, slugPerson, sep));
}, []);
}
function printTree(tree) {
tree.forEach(function (path) {
console.log(path);
});
}
printTree(makeTree(data));
On jsFiddle
Not a big fan of reinventing the wheel, so here is a solution using a object-scan. We use it for many data processing tasks and really like it because it makes things easier to maintain. However there is a learning curve. Anyways, here is how you could solve your question
// const objectScan = require('object-scan');
const scanTree = (tree) => objectScan(['**.children'], {
reverse: false,
breakFn: ({ isMatch, parents, context }) => {
if (!isMatch) {
return
}
context.push(
`/${parents
.filter((p) => 'name' in p)
.map(({ name }) => name)
.reverse()
.join('/')}/`
);
}
})(tree, []);
const tree = [{ id: '52fc69975ba8400021da5c7a', name: 'michael', children: [{ id: '52fc69975ba8400021da5c7d', parentId: '52fc69975ba8400021da5c7a', name: 'angela', children: [{ id: '52fc69975ba8400021da5c83', parentId: '52fc69975ba8400021da5c7d', name: 'oscar', children: [] }] }, { id: '52fc69975ba8400021da5c7b', parentId: '52fc69975ba8400021da5c7a', name: 'meredith', children: [{ id: '52fc69975ba8400021da5c7f', parentId: '52fc69975ba8400021da5c7b', name: 'creed', children: [] }, { id: '52fc69975ba8400021da5c7e', parentId: '52fc69975ba8400021da5c7b', name: 'kelly', children: [] }] }, { id: '52fc69975ba8400021da5c7c', parentId: '52fc69975ba8400021da5c7a', name: 'jim', children: [{ id: '52fc69975ba8400021da5c82', parentId: '52fc69975ba8400021da5c7c', name: 'dwight', children: [] }, { id: '52fc69975ba8400021da5c80', parentId: '52fc69975ba8400021da5c7c', name: 'phyllis', children: [] }, { id: '52fc69975ba8400021da5c81', parentId: '52fc69975ba8400021da5c7c', name: 'stanley', children: [] }] }] }];
scanTree(tree).map((e) => console.log(e));
// => /michael/
// => /michael/angela/
// => /michael/angela/oscar/
// => /michael/meredith/
// => /michael/meredith/creed/
// => /michael/meredith/kelly/
// => /michael/jim/
// => /michael/jim/dwight/
// => /michael/jim/phyllis/
// => /michael/jim/stanley/
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
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; }
I have an array of objects as following :
[
{"id":1,"lib":"A","categoryID":10,"categoryTitle":"Cat10","moduleID":"2","moduleTitle":"Module 2"},
{"id":2,"lib":"B","categoryID":10,"categoryTitle":"Cat10","moduleID":"2","moduleTitle":"Module 2"},
...
{"id":110,"lib":"XXX","categoryID":90,"categoryTitle":"Cat90","moduleID":"4","moduleTitle":"Module 4"}
]
I want to group this array by (moduleID,moduleTitle) and then by (categoryID,categoryTitle).
This is what I tried :
function groupBy(data, id, text) {
return data.reduce(function (rv, x) {
var el = rv.find(function(r){
return r && r.id === x[id];
});
if (el) {
el.children.push(x);
} else {
rv.push({ id: x[id], text: x[text], children: [x] });
}
return rv;
}, []);
}
var result = groupBy(response, "moduleID", "moduleTitle");
result.forEach(function(el){
el.children = groupBy(el.children, "categoryID", "categoryTitle");
});
The above code is working as expected, but as you can see, after the first grouping I had to iterate again over the array which was grouped by the moduleId in order to group by the categoryId.
How can I modify this code so I can only call groupBy function once on the array ?
Edit:
Sorry this might be late, but I want this done by using ES5, no Shim and no Polyfill too.
Here's one possible (although may be a bit advanced) approach:
class DefaultMap extends Map {
constructor(factory, iter) {
super(iter || []);
this.factory = factory;
}
get(key) {
if (!this.has(key))
this.set(key, this.factory());
return super.get(key);
}
}
Basically, it's the a Map that invokes a factory function when a value is missing. Now, the funny part:
let grouper = new DefaultMap(() => new DefaultMap(Array));
for (let item of yourArray) {
let firstKey = item.whatever;
let secondKey = item.somethingElse;
grouper.get(firstKey).get(secondKey).push(item);
}
For each firstKey this creates a Map inside grouper, and the values of those maps are arrays grouped by the second key.
A more interesting part of your question is that you're using compound keys, which is quite tricky in JS, since it provides (almost) no immutable data structures. Consider:
items = [
{a: 'one', b: 1},
{a: 'one', b: 1},
{a: 'one', b: 2},
{a: 'two', b: 2},
]
let grouper = new DefaultMap(Array);
for (let x of items) {
let key = [x.a, x.b]; // wrong!
grouper.get(key).push(x);
}
So, we're naively grouping objects by a compound key and expecting to see two objects under ['one', 1] in our grouper (which is one level for the sake of the example). Of course, that won't work, because each key is a freshly created array and all of them are different for Map or any other keyed storage.
One possible solution is to create an immutable structure for each key. An obvious choice would be to use Symbol, e.g.
let tuple = (...args) => Symbol.for(JSON.stringify(args))
and then
for (let x of items) {
let key = tuple(x.a, x.b); // works
grouper.get(key).push(x);
}
You could extend your function by using an array for the grouping id/names.
function groupBy(data, groups) {
return data.reduce(function (rv, x) {
groups.reduce(function (level, key) {
var el;
level.some(function (r) {
if (r && r.id === x[key[0]]) {
el = r;
return true;
}
});
if (!el) {
el = { id: x[key[0]], text: x[key[1]], children: [] };
level.push(el);
}
return el.children;
}, rv).push({ id: x.id, text: x.lib });
return rv;
}, []);
}
var response = [{ id: 1, lib: "A", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Workflow" }, { id: 2, lib: "B", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Module 2" }, { id: 110, lib: "XXX", categoryID: 90, categoryTitle: "Cat90", moduleID: "4", moduleTitle: "Module 4" }],
result = groupBy(response, [["moduleID", "moduleTitle"], ["categoryID", "categoryTitle"]]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Version with path as id.
function groupBy(data, groups) {
return data.reduce(function (rv, x) {
var path = [];
var last = groups.reduce(function (level, key, i) {
path.length = i;
path[i] = key[0].slice(0, -2).toUpperCase() + ':' + x[key[0]];
var id = path.join(';'),
el = level.find(function (r) {
return r && r.id === id;
});
if (!el) {
el = { id: path.join(';'), text: x[key[1]], children: [] };
level.push(el);
}
return el.children;
}, rv);
last.push({ id: path.concat('NODE:' + x.id).join(';') });
return rv;
}, []);
}
var response = [{ id: 1, lib: "A", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Workflow" }, { id: 2, lib: "B", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Module 2" }, { id: 110, lib: "XXX", categoryID: 90, categoryTitle: "Cat90", moduleID: "4", moduleTitle: "Module 4" }];
var result = groupBy(response, [["moduleID", "moduleTitle"], ["categoryID", "categoryTitle"]]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You could do it like this:
const exit = Symbol("exit");
function groupBy(arr, ...props){
const root = {};
for(const el of arr){
const obj = props.map(key => el[key])
.reduce((obj, key) => obj[key] || (obj[key] = {}), root);
(obj[exit] || (obj[exit] = [])).push(el);
}
}
So you can access it like:
const grouped = groupBy(response, "moduleID", "moduleTitle");
console.log( grouped[2]["workflow"][exit] );
You might leave away that exit symbol, but it feels a bit wrong to mix a nested tree with arrays.
There is an array:
let docs = [
{ "_id":"1", parent:"_", "title":"one"},
{ "_id":"2", parent:"1", "title":"two"},
{ "_id":"4", parent:"_", "title":"title"},
{ "_id":"5", parent:"4", "title":"www"},
{"_id":"_", "name":"root" },
];
I need to get out of it that's a tree:
{'_id':'_','name':'root','child':
[
{'_id':'1','parent':'_','title':'one','child':
[
{'_id':'2','parent':'1','title':'two','child':[]}
]
},
{'_id':'4','parent':'_','title':'title','child':
[
{'_id':'6','parent':'4','title':'vvv','child':[]}
]
}
]
}
But my code only works if the parent element is always higher on the list than the children, and I want to make that work universally.
This is code:
let node = {};
for (let doc of docs) {
doc.child = [];
node[doc._id] = doc;
if (typeof doc.parent === "undefined")
tree = doc;
else
node[doc.parent].child.push(doc);
}
console.log('tree->', JSON.stringify(tree));
code on codepen:
http://codepen.io/alex183/pen/OWvrPG?editors=0112
You can create recursive function using reduce method and basically check in each iteration of the parent property of current object is equal to passed parent param in function call.
let docs = [
{ "_id":"1", parent:"_", "title":"one"},
{ "_id":"2", parent:"1", "title":"two"},
{ "_id":"4", parent:"_", "title":"title"},
{ "_id":"5", parent:"4", "title":"www"},
{"_id":"_", "name":"root" }
];
function makeTree(data, parent = undefined) {
return data.reduce((r, e) => {
// check if current e.parent is equal to parent
if (e.parent === parent) {
// make a copy of current e so we keep original as is
const o = { ...e }
// set value as output of recursive call to child prop
o.child = makeTree(data, e._id)
// push to accumulator
r.push(o)
}
return r
}, [])
}
console.log(makeTree(docs))
This is a proposal with Array#reduce and Map. It sorts the array in advance.
var docs = [{ _id: "1", parent: "_", title: "one" }, { _id: "2", parent: "1", title: "two" }, { _id: "4", parent: "_", title: "title" }, { _id: "5", parent: "4", title: "www" }, { _id: "_", name: "root" }],
order = { undefined: -2, _: -1 },
tree = docs
.sort((a, b) => (order[a.parent] || a.parent) - (order[b.parent] || b.parent) || a._id - b._id)
.reduce(
(m, a) => (
m
.get(a.parent)
.push(Object.assign({}, a, { child: m.set(a._id, []).get(a._id) })),
m
),
new Map([[undefined, []]])
)
.get(undefined);
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }
The quick and dirty way is to use a sort function.
docs = docs.sort((a, b) => (a._id - b._id));
I have an app with a tree of nested nodes. all nodes are same type.
{
id: 1,
title: "node_1",
children: [
{
id: 2,
title: "node_2",
children: []
},
{
id: 3,
title: "node_3",
children: []
}
]
}
When user expanded some node (for example node with id === 3) i have to perform request to database and insert response (array children) inside of "children" property of node with id === 3 . So as result app state should be like this:
{
id: 1,
title: "node_1",
children: [
{
id: 2,
title: "node_2",
children: []
},
{
id: 3,
title: "node_3",
children: [
{
id: 4,
title: "node_4",
children: []
},
{
id: 5,
title: "node_5",
children: []
}
]
}
]
}
how can i paste array of children inside node_3 children property?
Given:
const layer1Id = 1;
const layer2Id = 3;
const newArray = [
{
id: 4,
title: "node_4",
children: [],
},
{
id: 5,
title: "node_5",
children: [],
}
];
Then, in the reducer you'll do:
return Object.assign({}, state, { children: state.children.map(child => {
if (child.id !== layer1Id) return child;
return Object.assign({}, child, { children: child.children.map(node => {
if (node.id !== layer2Id) return node;
return Object.assign({}, node, { children: node.children.concat(newArray) });
})});
})});
To make sure you don't mutate the previous state.
If it is dynamically or deeply nested, I'll recommend you to write some recursive function and use that instead.
Edit: here's sample recursive solution (untested). The indices are in order by level (ie: indices[0] refers to first level's id, indices[1] refers to second level's id):
const state = { ... };
const indices = [1, 3, 4, 5, 6];
const newArray = [ ... ];
const recursion = (node, ids, newChildren) => {
let children;
if (ids.length === 0) {
children = newChildren;
} else {
children = node.children.map(child => {
if (child.id !== ids[0]) return child;
return Object.assign({}, child, { children: recursion(child, ids.slice(1), newChildren) });
});
}
return Object.assign({}, node, { children });
};
recursion(state, indecies, newArray);
The suggested approach for relational or normalized data in a Redux store is to organize it in "normalized" fashion, similar to database tables. That will make it easier to manage updates. See http://redux.js.org/docs/FAQ.html#organizing-state-nested-data, How to handle tree-shaped entities in Redux reducers?, and https://github.com/reactjs/redux/pull/1269.
Just iterate through children array and push to correct one .
var id = expandedItemId;
for(var i = 0; i < obj.children.length; i++){
if(obj.id == expandedItemId){
obj.children.push(`data got from server`);
}
}