JavaScript - update deeply nested array of objects with the target path - javascript

The nesting level is always unknown, and children can be either undefined or an array with at least one item. Each key is always unique. This would be an example:
const arr = [{
key: '001',
children: [{
key: 'abc',
children: [{
key: 'ee',
children: [{
key: 'goc',
}, {
key: 'zzv',
children: [{
key: '241',
}],
}],
}],
}, {
key: '125',
children: undefined,
}],
}, {
key: '003',
children: [{
key: 'ahge',
}, {
key: '21521',
}],
}];
I'd like to write a function that receives a key to find the element and then updates its children field with the given children array and then returns the whole arr.
// Function that returns arr with updated the target element - how can I write this?
const mysteryFn = (arr, key, childrenToUpdate) => {
// Do something..
return arr;
}
const key = 'goc';
const childrenToUpdate = [{
key: '12345',
}, {
key: '25221a',
}];
const newArr = mysteryFn(arr, key, childrenToUpdate);
// expected newArr
const newArr= [{
key: '001',
children: [{
key: 'abc',
children: [{
key: 'ee',
children: [{
key: 'goc',
children: [{
key: '12345',
}, {
key: '25221a',
}],
}, {
key: 'zzv',
children: [{
key: '241',
}],
}],
}],
}, {
key: '125',
children: undefined,
}],
}, {
key: '003',
children: [{
key: 'ahge',
}, {
key: '21521',
}],
}];

This can be achieved with recursion.
const mysteryFn = (arr, key, childrenToUpdate) => {
// if children are undefined
if (!arr) return;
// loop over each entry and its children to find
// entry with passed key
arr.forEach((entry) => {
if (entry.key === key) {
entry.children = childrenToUpdate;
}
// recursive call to traverse children
mysteryFn(entry.children, key, childrenToUpdate);
});
return arr;
};

Related

Nest flat objects based on a parentId and id property

Given a flat level array of objects, what's the most efficient and modern way to nest them based on a parent and id property? The top level objects have no parentId, and there's no limit to nest levels.
[{
id: 'OS:MacOS',
type: 'OS',
value: 'MacOS'
}, {
parentId: 'OS:MacOS',
id: 'Version:Catalina',
type: 'Version',
value: 'Catalina'
}, {
parentId: 'Version:Catalina',
id: 'Browser:Chrome',
type: 'Browser',
value: 'Chrome'
}, {
id: 'OS:Windows',
type: 'OS',
value: 'Windows'
}, {
parentId: 'OS:Windows',
id: 'Version:7',
type: 'Version',
value: '7'
}, {
parentId: 'OS:MacOS',
id: 'Version:Mojave',
type: 'Version',
value: 'Mojave'
}, {
parentId: 'Version:Mojave',
id: 'Browser:Chrome',
type: 'Browser',
value: 'Chrome'
}, {
parentId: 'OS:Windows',
id: 'Version:XP',
type: 'Version',
value: 'XP'
}, {
parentId: 'Version:XP',
id: 'Browser:Chrome',
type: 'Browser',
value: 'Chrome'
}]
Where parentId matches up to a corresponding id field. Ideally transforming them to include a children array field along the lines of:
[{
id: 'OS:MacOS',
type: 'OS',
value: 'MacOS',
children: [
{
parentId: 'OS:MacOS',
id: 'Version:Catalina',
type: 'Version',
value: 'Catalina',
children: [
{
parentId: 'Version:Catalina',
id: 'Browser:Chrome',
type: 'Browser',
value: 'Chrome'
}
]
},
{
parentId: 'OS:MacOS',
id: 'Version:Mojave',
type: 'Version',
value: 'Mojave',
children: [
{
parentId: 'Version:Mojave',
id: 'Browser:Chrome',
type: 'Browser',
value: 'Chrome'
}
]
}
]
}, {
id: 'OS:Windows',
type: 'OS',
value: 'Windows',
children: [
{
parentId: 'OS:Windows',
id: 'Version:7',
type: 'Version',
value: '7'
},
{
parentId: 'OS:Windows',
id: 'Version:XP',
type: 'Version',
value: 'XP',
children: [
{
parentId: 'Version:XP',
id: 'Browser:Chrome',
type: 'Browser',
value: 'Chrome'
}
]
}
]
}]
Thoughts appreciated!
You could use reduce in recursive function that will pass down the current element id and compare it with parent id in nested calls.
const data = [{"id":"OS:MacOS","type":"OS","value":"MacOS"},{"parentId":"OS:MacOS","id":"Version:Catalina","type":"Version","value":"Catalina"},{"parentId":"Version:Catalina","id":"Browser:Chrome","type":"Browser","value":"Chrome"},{"id":"OS:Windows","type":"OS","value":"Windows"},{"parentId":"OS:Windows","id":"Version:7","type":"Version","value":"7"},{"parentId":"OS:MacOS","id":"Version:Mojave","type":"Version","value":"Mojave"},{"parentId":"Version:Mojave","id":"Browser:Chrome","type":"Browser","value":"Chrome"},{"parentId":"OS:Windows","id":"Version:XP","type":"Version","value":"XP"},{"parentId":"Version:XP","id":"Browser:Chrome","type":"Browser","value":"Chrome"}]
function nested(data, pid = undefined) {
return data.reduce((r, e) => {
if (e.parentId == pid) {
const obj = { ...e }
const children = nested(data, e.id);
if (children.length) obj.children = children;
r.push(obj)
}
return r;
}, [])
}
const result = nested(data);
console.log(result)
The reducer approach by Nenad works, but is pretty inefficient as it iterates through the data list n^2 times. Here is an O(n) solution:
function buildTree(data) {
const store = new Map(); // stores data indexed by it's id
const rels = new Map(); // stores array of children associated with id
const roots = []; // stores root nodes
data.forEach(d => {
store.set(d.id, d);
!rels.get(d.id) ? rels.set(d.id, []) : undefined; // noOp.;
if (!d.parentId) {
roots.push(d.id)
return;
}
const parent = rels.get(d.parentId) || [];
parent.push(d.id);
rels.set(d.parentId, parent);
});
function build(id) {
const data = store.get(id);
const children = rels.get(id);
if (children.length === 0) {
return {...data}
}
return {...data, children: children.map(c => build(c)) };
}
return roots.map(r => build(r));
}
const data = [{"id":"OS:MacOS","type":"OS","value":"MacOS"},{"parentId":"OS:MacOS","id":"Version:Catalina","type":"Version","value":"Catalina"},{"parentId":"Version:Catalina","id":"Browser:Chrome","type":"Browser","value":"Chrome"},{"id":"OS:Windows","type":"OS","value":"Windows"},{"parentId":"OS:Windows","id":"Version:7","type":"Version","value":"7"},{"parentId":"OS:MacOS","id":"Version:Mojave","type":"Version","value":"Mojave"},{"parentId":"Version:Mojave","id":"Browser:Chrome","type":"Browser","value":"Chrome"},{"parentId":"OS:Windows","id":"Version:XP","type":"Version","value":"XP"},{"parentId":"Version:XP","id":"Browser:Chrome","type":"Browser","value":"Chrome"}]
console.log(JSON.stringify(buildTree(data), null, 2))
Edit Note:
Earlier answer was class based. Removed that for simplicity. You can further optimize the space storage by changing store to be index based.

Pruning a tree by mapping nodes of tree that fail a test to its children

My problem here builds upon another problem I was trying to solve and received an excellent answer for where I have a tree:
const treeData = [{
title: '0-0',
key: '0-0',
children: [{
title: '0-0-0',
key: '0-0-0',
children: [
{ title: '0-0-0-0', key: '0-0-0-0', children: [] },
{ title: '0-0-0-1', key: '0-0-0-1', children: [] },
{ title: '0-0-0-2', key: '0-0-0-2', children: [] },
],
}, {
title: '0-0-1',
key: '0-0-1',
children: [
{ title: '0-0-1-0', key: '0-0-1-0', children: [] },
{ title: '0-0-1-1', key: '0-0-1-1', children: [] },
{ title: '0-0-1-2', key: '0-0-1-2', children: [] },
],
}, {
title: '0-0-2',
key: '0-0-2',
children: []
}],
}, {
title: '0-1',
key: '0-1',
children: [
{ title: '0-1-0-0', key: '0-1-0-0', children: [] },
{ title: '0-1-0-1', key: '0-1-0-1', children: [] },
{ title: '0-1-0-2', key: '0-1-0-2', children: [] },
],
}, {
title: '0-2',
key: '0-2',
children: []
}];
and an array of leaf nodes:
const leafNodes = ['0-0-1-2', '0-1-0-1', '0-1-0-2']
Before, I just wanted a filtered/pruned copy of the tree that contains all the paths to the leaf nodes, but now I would like to further prune it by removing the parent node that doesn't satisfy a test -- the test being having all of its children included in the list of leaf nodes. The resulting tree would look like this:
const pruned = [{
title: '0-0-1-2',
key: '0-0-1-2',
children: []
},
{
title: '0-1-0-1',
key: '0-1-0-1',
children: []
}, {
title: '0-1-0-2',
key: '0-1-0-2',
children: []
}
]
Here, the node with keys 0-0-1 would be removed because only one of its 3 child nodes (0-0-1-2) is included in the leafNodes list and the child nodes included in the leaf nodes list (in this case, just the one) are bumped up to the level of their now removed parent. This would flow back up to the parent of the removed node, now with key 0-0, since not all of its children are included in the pruned tree.
This same pattern would apply to 0-1.
You could iterate the array and check if the child are complete selected , then get the actual node or just some children, then take the children only.
function getShort(array, keys) {
var result = [],
every = true;
array.forEach(o => {
var children;
if (keys.includes(o.key)) return result.push(o);
if (!o.children || !o.children.length) return every = false;
children = getShort(o.children, keys);
if (children.length && children.length === o.children.length) return result.push(o);
result.push(...children);
every = false;
});
return every
? array
: result;
}
const
treeData = [{ key: '0-0', children: [{ key: '0-0-0', children: [{ key: '0-0-0-0', children: [] }, { key: '0-0-0-1', children: [] }, { key: '0-0-0-2', children: [] }] }, { key: '0-0-1', children: [{ key: '0-0-1-0', children: [] }, { key: '0-0-1-1', children: [] }, { key: '0-0-1-2', children: [] }] }, { key: '0-0-2', children: [] }] }, { key: '0-1', children: [{ key: '0-1-0-0', children: [] }, { key: '0-1-0-1', children: [] }, { key: '0-1-0-2', children: [] }] }, { key: '0-2', children: [] }],
leafNodes = [
'0-0-0-0', '0-0-0-1', '0-0-0-2', // all 0-0-0 all 0-0
'0-0-1-0', '0-0-1-1', '0-0-1-2', // all 0-0-1 all 0-0
'0-0-2', // all 0-0-2 all 0-0
'0-1-0-1', '0-1-0-2'
],
short = getShort(treeData, leafNodes);
console.log(short);
.as-console-wrapper { max-height: 100% !important; top: 0; }

How to find a tree inside a tree in typescript

let say i have a tree in javascript
a1
--b
----c1
a2
--b2
--b3
----c2
and if i wanted to find c2, it should return a2->b3->c2
Lets say my json looked like this?
treeFamily = {
name : "Parent",
children: [{
name : "Child1",
children: [{
name : "Grandchild1",
children: []
},{
name : "Grandchild2",
children: []
},{
name : "Grandchild3",
children: []
}]
}, {
name: "Child2",
children: []
}]
};
You could check if the nested children have the wanted key/value. Then take the name and hand over the result to the outer call.
function findPath(array, target) {
var path;
array.some(({ name, children }) => {
var temp;
if (name === target) {
path = [name];
return true;
}
if (temp = findPath(children, target)) {
path = [name, ...temp];
return true;
}
});
return path;
}
var treeFamily = { name: "Parent", children: [{ name: "Child1", children: [{ name: "Grandchild1", children: [] }, { name: "Grandchild2", children: [] }, { name: "Grandchild3", children: [] }] }, { name: "Child2", children: [] }] };
console.log(findPath([treeFamily], 'Grandchild2'));
console.log(findPath([treeFamily], 'foo'));
You can use for...of to search the children by calling the function recursively. If the target is found, the name is returned, and combined with the previous names. If not, the function will return undefined. Alternatively, you can return an empty array.
const findPath = (targetName, { name, children }) => {
if(name === targetName) return [name];
for(const child of children) {
const result = findPath(targetName, child);
if(result) return [name, ...result];
}
// if child not found implicitly return undefined or return [] to get an empty array
};
const treeFamily = { name: "Parent", children: [{ name: "Child1", children: [{ name: "Grandchild1", children: [] }, { name: "Grandchild2", children: [] }, { name: "Grandchild3", children: [] }] }, { name: "Child2", children: [] }] };
console.log(findPath('Child2', treeFamily));
console.log(findPath('Grandchild3', treeFamily));
console.log(findPath('Grandchild400', treeFamily));

How to construct a tree of objects from an array of objects (unflatten an array) [duplicate]

This question already has answers here:
Build tree array from flat array in javascript
(34 answers)
Closed 4 years ago.
I currently have a flat array of objects which I am trying to convert to a nested array of objects. I would like to reuse this function throughout my application - whatever the final depth of the array - so I believe a recursive function would be more appropriate.
My attemps so far I have been a combination of sort + reduce with no success.
It would be much appreciated if you could help me write a clean function for my app !
Initial Array - Flat list of objects
const data = [
{ index: 0, parentKey: '-LLnMWy69vACjys0QIGH', type: 'options', name: 'OPTION_1', key: '-LLnOxg5hsDYR-PcfjBT' },
{ index: 1, parentKey: '-LLnMWy69vACjys0QIGH', type: 'options', name: 'OPTION_2', key: '-LLnP-O6TyHxIpPk9bCU' },
{ index: 0, parentKey: '-LLnLuLt6cn-vBpMWv-u', type: 'collections', name: 'COLLECTION_1', key: '-LLnMWy69vACjys0QIGH' },
{ index: 1, parentKey: '-LLnLuLt6cn-vBpMWv-u', type: 'collections', name: 'COLLECTION_2', key: '-LLnMYNyJmhSCPB-8lL1' },
{ index: 0, name: 'CONFIGURATOR_1', key: '-LLnLuLt6cn-vBpMWv-u' },
{ index: 1, name: 'CONFIGURATOR_2', key: '-LLnLtLs7PjXSAW0PWCQ' },
];
Desired outcome - Nested arrays of objects
const data = [
{
key: '-LLnLuLt6cn-vBpMWv-u',
name: 'CONFIGURATOR_1',
collections: [
{
key: '-LLnMWy69vACjys0QIGH',
name: 'COLLECTION_1',
options: [
{
key: '-LLnOxg5hsDYR-PcfjBT',
name: 'OPTION_1',
},
{
key: '-LLnP-O6TyHxIpPk9bCU',
name: 'OPTION_2',
},
],
},
{
key: '-LLnMYNyJmhSCPB-8lL1',
name: 'COLLECTION_2',
},
],
},
{ key: '-LLnLtLs7PjXSAW0PWCQ',
name: 'CONFIGURATOR_2',
}]
As usual for this problem, one of the simplest method is to index all object by key with a map M.
then, go through the map M, if the current keyed element has a parent, then add it to the keyed parent in the Map M then map all these elements to an array A. Since all objects in javascript are references, the tree structure would be reconstructed automatically.
The last step would be to filter out all elements in A that have a parent.
const data = [
{ index: 0, parentKey: '-LLnMWy69vACjys0QIGH', type: 'options', name: 'OPTION_1', key: '-LLnOxg5hsDYR-PcfjBT' },
{ index: 1, parentKey: '-LLnMWy69vACjys0QIGH', type: 'options', name: 'OPTION_2', key: '-LLnP-O6TyHxIpPk9bCU' },
{ index: 0, parentKey: '-LLnLuLt6cn-vBpMWv-u', type: 'collections', name: 'COLLECTION_1', key: '-LLnMWy69vACjys0QIGH' },
{ index: 1, parentKey: '-LLnLuLt6cn-vBpMWv-u', type: 'collections', name: 'COLLECTION_2', key: '-LLnMYNyJmhSCPB-8lL1' },
{ index: 0, name: 'CONFIGURATOR_1', key: '-LLnLuLt6cn-vBpMWv-u' },
{ index: 1, name: 'CONFIGURATOR_2', key: '-LLnLtLs7PjXSAW0PWCQ' },
];
const M = data.reduce(function (result, el) {
result[el.key] = el
return result;
}, {});
const A = Object.keys(M).map(key => {
const el = M[key]
if (M[el.parentKey]) {
if (!M[el.parentKey].collections) {
M[el.parentKey].collections = []
}
M[el.parentKey].collections.push(el)
}
return el
}).filter(el => {
return !el.parentKey
})
console.log(JSON.stringify(A, null, '\t'))
I have found my issue using the following function. I am sure there is other ways more efficient ways to do it but it is the cleanest option i have found!
const objectNest = (array, parentKey) => {
const output = [];
array.forEach((object) => {
if (object.parentKey === parentKey) {
const children = objectNest(array, object.key);
if (children.length) { object[children[0].type] = children; }
const { index, parentKey, ...content } = object;
output.push(content);
}
});
return output;
};

flatten an array of objects in javascript

I have a data structure that looks like this
const array = [{
name: 'bar',
children: [{
name: 'foo',
children: [{
name: 'baz123',
}, {
name: 'baz',
}]
}]
}, {
name: 'shallowKey'
}, {
name: 'abc'
}];
And I would like to flatten it to look something like this
[{
name: 'bar'
}, {
name: 'foo',
}, {
name: 'baz123',
}, {
name: 'baz',
}, {
name: 'shallowKey'
}, {
name: 'abc'
}];
I tried lodash like this https://jsfiddle.net/hmzhjji/081q60qg/1/
But it's not doing anything, any other way I can do this?
Thanks
A recursive way would be:
function flatten(array, result = []){
for(const {name, children} of array){
result.push({name});
if(children) flatten(children, result);
}
return result;
}
Or the alternative ES6 version:
const flatten = array => array.reduce((res, {name, children = []}) => res.concat(name).concat(flatten(children)), []);
So you can do flatten(array) to get the desired result.
You can use forEach to iterate over the array and check if the required object is present and call the function recursively
const array = [{
name: 'bar',
children: [{
name: 'foo',
children: [{
name: 'baz123',
}, {
name: 'baz',
}]
}]
}, {
name: 'shallowKey'
}, {
name: 'abc'
}];
var res = [];
function flatten(array){
array.forEach(function(obj){
var name = {name: obj.name}
res.push(name);
if(obj.children){
flatten(obj.children)
}
})
return res;
}
console.log(flatten(array))

Categories