Lodash - How to create a tree from a flat array - javascript

Every time my application loads, I receive the following json:
[
{
id: 'mALRRY93jASr',
identifier: '100',
text: 'Text A'
},
{
id: '7S3xHZEdNcfV',
identifier: '200',
text: 'Text B'
},
{
id: '2ZA5xSJeukU6',
identifier: '300',
text: 'Text C',
},
{
id: 'bhg3GnLEvw2k',
identifier: '300.100',
text: 'Text C - A'
},
{
id: 'bhg3GnLEvw2k',
identifier: '300.100.100',
text: 'Text C - A - A'
},
{
id: '2AcXNr4HT388',
identifier: '300.200',
text: 'Text C - B'
}
]
The tree levels are identified by the identifier property.
The tree can have thousands of children, so it needs to be recursive.
How can I arrange the json using Lodash to looks like the following json?
[
{
id: 'mALRRY93jASr',
identifier: '100',
text: 'Text A'
},
{
id: '7S3xHZEdNcfV',
identifier: '200',
text: 'Text B'
},
{
id: '2ZA5xSJeukU6',
identifier: '300',
text: 'Text C',
children: [
{
id: 'bhg3GnLEvw2k',
identifier: '300.100',
text: 'Text C - A',
children: [
{
id: 'bhg3GnLEvw2k',
identifier: '300.100.100',
text: 'Text C - A - A'
}
]
},
{
id: '2AcXNr4HT388',
identifier: '300.200',
text: 'Text C - B'
}
]
}
]

You could take an iterative approach by looking for objects in the same path of identifier and build a nested structure.
This approach works for unsorted data as well.
var data = [{ id: 'mALRRY93jASr', identifier: '100', text: 'Text A' }, { id: '7S3xHZEdNcfV', identifier: '200', text: 'Text B' }, { id: '2ZA5xSJeukU6', identifier: '300', text: 'Text C' }, { id: 'bhg3GnLEvw2k', identifier: '300.100', text: 'Text C - A' }, { id: 'bhg3GnLEvw2k', identifier: '300.100.100', text: 'Text C - A - A' }, { id: '2AcXNr4HT388', identifier: '300.200', text: 'Text C - B' }],
tree = [];
data.reduce((r, o) => {
o.identifier
.split('.')
.map((_, i, a) => a.slice(0, i + 1).join('.'))
.reduce((q, identifier, i, { length }) => {
var temp = (q.children = q.children || []).find(p => p.identifier === identifier);
if (!temp) {
q.children.push(temp = { identifier });
}
if (i + 1 === length) {
Object.assign(temp, o);
}
return temp;
}, r);
return r;
}, { children: tree });
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }

I used a previous answer of mine as a base. There are a lot of similarities, but your "path" syntax is a little different and I had to tweak some of the parsing.
const data = [
{
id: 'mALRRY93jASr',
identifier: '100',
text: 'Text A'
},
{
id: '7S3xHZEdNcfV',
identifier: '200',
text: 'Text B'
},
{
id: '2ZA5xSJeukU6',
identifier: '300',
text: 'Text C',
},
{
id: 'bhg3GnLEvw2k',
identifier: '300.100',
text: 'Text C - A'
},
{
id: 'bhg3GnLEvw2k',
identifier: '300.100.100',
text: 'Text C - A - A'
},
{
id: '2AcXNr4HT388',
identifier: '300.200',
text: 'Text C - B'
}
];
const pathPartRegex = /.*?\./g;
const tree = _.reduce(data, (result, value) => {
// We use the . character as a "path part" terminator,
// but it is not there at the end of the string, so we add it
let identifier = value.identifier;
if (!identifier.endsWith(".")) {
identifier = identifier + ".";
}
const pathParts = identifier.match(pathPartRegex);
let node = result;
let path = "";
// Go down through tree until last path part
const notLastPart = pathParts.splice(0, pathParts.length - 1);
for (const pathPart of notLastPart) {
path += pathPart;
const existingNode = node.children
? node.children.find(item => path.startsWith(item.identifier) )
: node.find(item => path.startsWith(item.identifier));
if (existingNode) {
node = existingNode
} else {
// If we need to traverse over a path that doesn't exist, just create it
// See notes
const newNode = {
identifier: path,
children: []
};
// The root element is just an array, and doesn't have a children property
if (node.children) {
node.children.push(newNode);
} else {
node.push(newNode);
}
node = newNode;
}
}
// Add new node
const newNode = {
id: value.id,
text: value.text,
identifier: value.identifier,
children: []
};
// The root element is just an array, and doesn't have a children property
if (node.children) {
node.children.push(newNode);
} else {
node.push(newNode);
}
return result;
}, []);
Tested via RunKit (https://npm.runkit.com/lodash)
Notes:
The same warnings from the original answer also apply here.

You can use Array.reduce() and _.setWith() to create a tree of objects by the path (identity). Then you can use a recursive function with _.transform() to convert the children to an array using _.values():
const createTree = (arr) => {
// reduce to a tree of objects
const oTree = arr.reduce((r, o) => {
const key = o.identifier.replace(/\./g, '.children.');
// creates the path and adds the object value
return _.setWith(r, key, o, Object)
}, {});
// transforms the children to an array recursivly
const transformChildren = (tree) =>
_.transform(tree, (acc, v, k) => {
const value = _.isObject(v) ? transformChildren(v) : v;
acc[k] = _.eq(k, 'children') ? _.values(value) : value;
});
return transformChildren(_.values(oTree));
};
const data = [{"id":"mALRRY93jASr","identifier":"100","text":"Text A"},{"id":"7S3xHZEdNcfV","identifier":"200","text":"Text B"},{"id":"2ZA5xSJeukU6","identifier":"300","text":"Text C"},{"id":"bhg3GnLEvw2k","identifier":"300.100","text":"Text C - A"},{"id":"bhg3GnLEvw2k","identifier":"300.100.100","text":"Text C - A - A"},{"id":"2AcXNr4HT388","identifier":"300.200","text":"Text C - B"}];
const result = createTree(data);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

Related

Walk through a nested array using index in JavaScript

I want walk through a nested array and need to find the target element in the array. An example path [2, 1] should return {text: 'More 2'} and path [2, 2, 1] should return { text: 'Other-2' }. I tried lodash functions but no luck yet.
My Nested array is given below:
var data = [
{ text: 'Item 1', },
{ text: 'Item 2', },
{
text: 'More',
children: [
{ text: 'More 1', children: [] },
{ text: 'More 2'},
{ text: 'Other', children:[ {text: 'Other-1'}, {text: 'Other-2'}, {text: 'Other-3'} ] }
]
}
];
Well, it's not a multi-dimensional array, nor is it a raggedy array-of-arrays. It's an array of objects (that happen contain other arrays of objects that happen to...).
Lodash's _.get() ought to do the trick for you:
const _ = require('lodash');
const data = data = [
{ text: 'Item 1', },
{ text: 'Item 2', },
{
text: 'More',
children: [
{ text: 'More 1', children: [] },
{ text: 'More 2'},
{ text: 'Other', children:[ {text: 'Other-1'}, {text: 'Other-2'}, {text: 'Other-3'} ] }
]
}
];
const widget = _.get(obj, '[2].children[1]');
console.log('widget',widget);
Or... roll your own. It's not that hard to walk the tree:
function select(data, ...path) {
let i = path.shift() ;
let node = data[i] ;
while ( node && (i=path.shift()) !== undefined ) {
node = node?.children?.[i] ;
}
return node ;
}
const widget = select( data, 2, 1 );
console.log(widget);

Compare two array of objects and filter element not present in second array

Suppose I have two array of object as:
const array1 = [
{ name: 'detail1', title: 'detail1' },
{ name: 'detail2 ', title: 'detail2 ' },
{ name: 'detail3', title: 'detail3' },
{ name: 'detail4', title: 'detail4' },
{ name: 'detail5', title: 'detail5' },
{ name: 'detail6', title: 'detail6' },
{ name: 'detail7', title: 'detail7' }
]
const array2 = [
{ name: 'detail1', title: 'detail1' },
{ name: 'detail2 ', title: 'detail2 ' },
{ name: 'detail3', title: 'detail3' },
{ name: 'detail4', title: 'detail4' },
]
I want to compare two arrays i.e. array1 and array2 and get the missing element of aaray2.
For this I tried as:
var absent = array2.filter(e=>!array1.includes(e));
But I am unable to get missing element of array2.
My expected O/P :
[ { name: 'detail5', title: 'detail5' },
{ name: 'detail6', title: 'detail6' },
{ name: 'detail7', title: 'detail7' }]
These are all the elements which are not present in array2.
What exactly am I doing wrong here?
Please let me know if anyone needs any further information.
You could build a normalised object with key and values and filter the objects.
const
array1 = [{ name: 'detail1', title: 'detail1' }, { name: 'detail2 ', title: 'detail2 ' }, { name: 'detail3', title: 'detail3' }, { name: 'detail4', title: 'detail4' }, { name: 'detail5', title: 'detail6' }, { name: 'detail7', title: 'detail7' }, { name: 'detail8', title: 'detail8' }],
array2 = [{ name: 'detail1', title: 'detail1' }, { name: 'detail2 ', title: 'detail2 ' }, { name: 'detail3', title: 'detail3' }, { name: 'detail4', title: 'detail4' }],
sortEntriesByKey = ([a], [b]) => a.localeCompare(b),
filter = array2.reduce((r, o) => {
Object
.entries(o)
.sort(sortEntriesByKey)
.reduce((o, [k, v]) => (o[k] ??= {})[v] ??= {}, r);
return r;
}, {});
absent = array1.filter((o) => {
let f = filter;
return !Object
.entries(o)
.sort(sortEntriesByKey)
.every(([k, v]) => f = f[k]?.[v]);
});
console.log(absent);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Edit: you want objects in A not in B.
It is ideal to loop through A, find if the element exists in B. If yes, then do not include it.
In javacript object references are compared when you do "==" or "===" or other array search methods.
{} == {} will return false. You can check in your dev console.
In your case, you will have to check specific properties.
var absent = array1.filter(e=>{
let findInd = array2.findIndex((a)=>{
return (a.name == e.name && a.title == e.title);});
return (findInd == -1); });
In the inner findIndex, I am finding index based on a condition. In filter method, I am returning true only if that index is -1(not found).
This worked for me:
let cart = [{
"id": "6304a51af5726921c0dadd64",
"qty": 1
},
{
"id": "8704a51af5726921c0dadd64",
"qty": 1
},
{
"id": "4704a51af5726921c0dadd64",
"qty": 1
}
]
let cartList = [{
"id": "6304a51af5726921c0dadd64",
"qty": 1
},
{
"id": "9704a51af5726921c0dadd64",
"qty": 1
}
]
let test = cart.some((element) =>
cartList.some((e) => element.id === e.id)
);
console.log(" if any single object matched:", test);
let test1 = cart.filter((element) =>
cartList.some((e) => element.id === e.id)
);
console.log("display matching objects :", test1);

Complicated array merging (keeping order)

I've got two arrays as follows:
const array1 = [
{
id: 'a1',
text: 'some text'
},
{
id: 'a2',
text: 'some more text',
},
{
id: 'a3',
text: 'some more text',
},
]
and
const array2 = [
{
id: 'a1',
text: 'some text'
},
{
id: 'ab12',
text: 'some text'
},
{
id: 'abc123',
text: 'some further text'
},
{
id: 'a3',
text: 'some more text'
},
{
id: 'bx44',
text: 'some more text'
},
]
I would like to combine these arrays with information has to whether each item has a "matching" 'id' property as the other array, so:
combined = [
{
id: 'a1',
text: 'some text',
info: 'in-both'
},
{
id: 'a2',
text: 'some text',
info: 'only-array1',
},
{
id: 'ab12',
text: 'some text',
info: 'only-array2',
},
{
id: 'abc123',
text: 'some further text',
info: 'only-array2',
},
{
id: 'a3',
text: 'some more text',
info: 'in-both',
},
{
id: 'bx44',
text: 'some more text',
info: 'only-array2',
},
]
I'm trying to keep the "inherent" order, so that items being only present in array1 or array2 end up between matches (items in both arrays) relative to their index. For example, 'a2' (only present in array1) comes after 'a1' but before 'a3'. If, between matches, there are multiple items being present only in array1 or array2, I'm trying to achieve so that the ones belonging to array1 come first (in the example, 'a2' comes before 'ab12' and 'abc123').
The code so far:
array1.reduce((all, curr, a1index, a1array) => {
let correspondingItemInArray2Index = array2.findIndex(a2item => a2item.id === curr.id);
if(correspondingItemInArray2Index === -1) {
curr.info = 'only-in-array1';
}
else if(correspondingItemInArray2Index === a1index) {
// Items are on same level...
curr.info = 'in-both';
}
else {
... // I need to find all items of array2 until the next 'match' of ids?
}
}
, []);
You could get common items and iterate all between.
function merge(...data) {
var common = data.map(a => a.map(({ id }) => id)).reduce((a, b) => a.filter(v => b.includes(v))),
indices = data.map(_ => 0),
result = [];
while (indices.every((l, i) => l < data[i].length)) {
indices = indices.map((j, i) => {
while (j < data[i].length && !common.includes(data[i][j].id)) {
result.push(Object.assign({}, data[i][j++], { info: ['a', 'b'][i] }));
}
return j;
});
if (indices.some((l, i) => l >= data[i].length)) break;
result.push(Object.assign({}, data[0][indices[0]], { info: 'both' }));
indices = indices.map(v => v + 1);
}
indices.forEach((j, i) => {
while (j < data[i].length) {
result.push(Object.assign({}, data[i][j++], { info: ['a', 'b'][i] }));
}
});
return result;
}
var array1 = [{ id: 'a1', text: 'some text' }, { id: 'a2', text: 'some more text' }, { id: 'a3', text: 'some more text' }],
array2 = [{ id: 'a1', text: 'some text' }, { id: 'ab12', text: 'some text' }, { id: 'abc123', text: 'some further text' }, { id: 'a3', text: 'some more text' }, { id: 'bx44', text: 'some more text' }],
result = merge(array1, array2);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Extract all specific properties from a nested array of objects

I have a nested array of objects. I would like to get an object that only provides me the onClick property as the key and the value as null. I am able to navigate to the first level, but I am unable to navigate down further. How do I go forward doing it?
const headers = [{
id: 'title1',
title: 'Title 1',
children: [
{
title: 'Children 1',
child: [
{
title: 'Child 1',
id: 'child1Id',
onClick: 'child1Click',
url: '/child1'
},
{
title: 'Child 2',
id: 'child2Id',
onClick: 'child2Click'
}
]
},
{
title: 'Children 2',
child: [
{
title: 'Child 3',
id: 'child3Id',
onClick: 'child3Click',
},
{
title: 'Child 4',
id: 'child4Id',
onClick: 'child4Click'
}
]
}
]
},
{
id: 'title2',
title: 'Title 2',
privilege: '',
children: [{
title: 'Children 3',
privilege: '',
child: [{
title: 'Child 5',
id: 'child5Id',
onClick: 'child3Click',
url: '/child5',
privilege: ''
},
{
title: 'Child 6',
id: 'child6Id',
onClick: 'child6Click',
url: '/child6',
privilege: ''
}
]
},
{
title: 'Children 4',
privilege: '',
child: [{
title: 'Child 7',
id: 'child7Id',
onClick: 'child7Click',
url: '/child7',
privilege: ''
},
{
title: 'Child 8',
id: 'child8Id',
onClick: 'child8Click',
url: '/child8',
privilege: ''
}
]
}
]
}
];
const routesMap = ({ onClick, children }) => (onClick ? { [onClick]: null } : _.flatMap(children, routesMap));
const routeStates = _.assign({}, ..._.flatMap(headers, routesMap));
console.log(routeStates)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
Expected Output:
{
child1Click: null,
child2Click: null,
child3Click: null,
child4Click: null,
child5Click: null,
child6Click: null,
child7Click: null,
child8Click: null,
}
Please advice. Any help is highly appreciated.
Using vanilla JS you can use 3 nested map loops to get the onClick values, and return a an object with the value as property. Use Array.flat() to convert to a single array, and spread into Object.assign() to get an object.
const fn = headers => Object.assign({}, ...headers
.map(({ children }) =>
children.map(({ child }) => child.map(({ onClick }) => ({ [onClick]: null })))
) // extract onClick values
.flat(Infinity) // flatten to a single array
)
const headers = [{"id":"title1","title":"Title 1","children":[{"title":"Children 1","child":[{"title":"Child 1","id":"child1Id","onClick":"child1Click","url":"/child1"},{"title":"Child 2","id":"child2Id","onClick":"child2Click"}]},{"title":"Children 2","child":[{"title":"Child 3","id":"child3Id","onClick":"child3Click"},{"title":"Child 4","id":"child4Id","onClick":"child4Click"}]}]},{"id":"title2","title":"Title 2","privilege":"","children":[{"title":"Children 3","privilege":"","child":[{"title":"Child 5","id":"child5Id","onClick":"child3Click","url":"/child5","privilege":""},{"title":"Child 6","id":"child6Id","onClick":"child6Click","url":"/child6","privilege":""}]},{"title":"Children 4","privilege":"","child":[{"title":"Child 7","id":"child7Id","onClick":"child7Click","url":"/child7","privilege":""},{"title":"Child 8","id":"child8Id","onClick":"child8Click","url":"/child8","privilege":""}]}]}]
const routeStates = fn(headers)
console.log(routeStates)
With lodash you can use _.flatMapDeep() instead of Array.flat():
const fn = headers => Object.assign({}, ...
_.flatMapDeep(headers, ({ children }) =>
_.map(children, ({ child }) => _.map(child, ({ onClick }) => ({ [onClick]: null })))
) // extract onClick values and flatte
)
const headers = [{"id":"title1","title":"Title 1","children":[{"title":"Children 1","child":[{"title":"Child 1","id":"child1Id","onClick":"child1Click","url":"/child1"},{"title":"Child 2","id":"child2Id","onClick":"child2Click"}]},{"title":"Children 2","child":[{"title":"Child 3","id":"child3Id","onClick":"child3Click"},{"title":"Child 4","id":"child4Id","onClick":"child4Click"}]}]},{"id":"title2","title":"Title 2","privilege":"","children":[{"title":"Children 3","privilege":"","child":[{"title":"Child 5","id":"child5Id","onClick":"child3Click","url":"/child5","privilege":""},{"title":"Child 6","id":"child6Id","onClick":"child6Click","url":"/child6","privilege":""}]},{"title":"Children 4","privilege":"","child":[{"title":"Child 7","id":"child7Id","onClick":"child7Click","url":"/child7","privilege":""},{"title":"Child 8","id":"child8Id","onClick":"child8Click","url":"/child8","privilege":""}]}]}]
const routeStates = fn(headers)
console.log(routeStates)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
try to access it in a loop and to access it try this as it is an object...
for ( let i = 0; i < headers.length; i++ ) {
for (let j = 0; j < headers[i].children.length; j++ ) {
for (let k = 0; k < headers[i].children[j].child.length; j++ ) {
console.log( headers[i].children[j].child[k].onClick);
}
}
}
it will print as follows ...
child1Click,
child2Click,
child3Click,
child4Click,
child5Click,
child6Click,
child7Click,
child8Click
if you want to set the all childClick to null try adding this..
console.log( headers[i].children[j].child[k].onClick: null);
i hope it helps
I borrowed a solution I found here to create this:
var headers = [
//...
];
var newObj = {};
function iterate(obj) {
Object.keys(obj).forEach(key => {
if (key === 'onClick') {
newObj[obj[key]] = 'null';
}
if (typeof obj[key] === 'object') {
iterate(obj[key])
}
})
}
iterate(headers);
console.log(newObj);

Return a value from inside an array that is inside an array of objects

//Structure
const definitions = {
sections: [
{ title: 'Section A', actions: [ { id: 0, name: 'action A' } ] },
{ title: 'Section B', actions: [ { id: 1, name: 'action B' } ] },
]
};
//Code to retrieve the action
const id = 1;
const sectionDef = definitions.sections.find(s => s.actions.find(a => a.id === id));
const actionDef = sectionDef.actions.find(a => a.id === id);
//Print it
console.log(actionDef);
The above solution works, but I think there must be a better way to retrieve an object from inside an array especially since I need to run the same code twice...
You can do it with an Array.forEach and an Array.find
//Structure
const definitions = {
sections: [
{ title: 'Section A', actions: [ { id: 0, name: 'action A' } ] },
{ title: 'Section B', actions: [ { id: 1, name: 'action B' } ] },
]
};
//Code to retrieve the action
const id = 1;
let action;
definitions.sections.forEach(section => {
action = section.actions.find(a => a.id === id);
});
//Print it
console.log(action);
You could use a recursive approach by using all values of the object for searching in an arbitrary data structure.
function find(object, key, value) {
var result;
if (object[key] === value) {
return object;
}
if (object && typeof object === 'object') {
Object.values(object).some(o => result = find(o, key, value));
}
return result;
}
var definitions = { sections: [{ title: 'Section A', actions: [{ id: 0, name: 'action A' }] }, { title: 'Section B', actions: [{ id: 1, name: 'action B' }] }] };
console.log(find(definitions, 'id', 0));
console.log(find(definitions, 'id', 1));
Your code has an error on const sectionDef = definitions.find, as it should be const sectionDef = definitions.sections.find as find works for array type. And the way you are doing is fine to achieve the desired result.
const definitions = {
sections: [
{ title: 'Section A', actions: [ { id: 0, name: 'action A' } ] },
{ title: 'Section B', actions: [ { id: 1, name: 'action B' } ] },
]
}
const id = 1;
const sectionDef = definitions.sections.find(s => s.actions.find(a => a.id === id));
const actionDef = sectionDef.actions.find(a => a.id === id);
console.log(sectionDef);
console.log(actionDef);
const definitions = {
sections: [
{ title: 'Section A', actions: [ { id: 0, name: 'action A' } ] },
{ title: 'Section B', actions: [ { id: 1, name: 'action B' } ] },
]
}
const id = 1;
var result2;
var data = definitions.sections;
var result = data.filter(function(obj) {
var data2 = obj.actions;
result2 = data2.filter(function(obj) {
return obj.id == id;
});
});
console.log(result2);

Categories