lodash to flatten JSON and show original path - javascript

I have a JSON object with nested children that I would like to flatten and modify using lodash. Ideally the revised JSON will have a value for the original level the nested children were at and show their original path.
Here is sample JSON:
var data = [
{id: 0, name: 'Australia', children: [
{id: 10, name: 'Melbourne', children: []},
{id: 11, name: 'Sydney', children: [
{id: 100, name: 'Surry Hills', children: []},
{id: 102, name: 'Darlinghurst', children: []}
]},
{id: 13, name: 'Kambalda', children: []}
]},
{id: 1, name: 'Spain', children: [
{id: 20, name: 'Barcelona', children: []},
{id: 21, name: 'Madrid', children: []}
]},
{id: 3, name: 'UK', children: [
{id: 30, name: 'London', children: [
{id: 302, name: 'Knightsbridge', children: []},
{id: 309, name: 'West End', children: []}
]},
{id: 33, name: 'Leeds', children: []},
{id: 35, name: 'Manchester', children: []}
]}
];
And the transformed JSON I would like to generate is:
[
{id: 0, name: 'Australia', level: 0, pathname: 'Australia'},
{id: 10, name: 'Melbourne', level: 1, pathname: 'Australia > Melbourne'},
{id: 11, name: 'Sydney', level: 1, pathname: 'Australia > Sydney'},
{id: 100, name: 'Surry Hills', level: 2, pathname: 'Australia > Sydney > Surry Hills'},
{id: 102, name: 'Darlinghurst', level: 2, pathname: 'Australia > Sydney > Darlinghurst'},
{id: 13, name: 'Kambalda', level: 1, pathname: 'Australia > Kambalda'},
{id: 1, name: 'Spain', level: 0, pathname: 'Spain'},
{id: 20, name: 'Barcelona', level: 1, pathname: 'Spain > Barcelona'},
{id: 21, name: 'Madrid', level: 1, pathname: 'Spain > Madrid'},
{id: 3, name: 'UK', level: 0, pathname: 'UK'},
{id: 30, name: 'London', level: 1, pathname: 'UK > London'},
{id: 302, name: 'Knightsbridge', level: 2, pathname: 'UK > London > Knightsbridge'},
{id: 309, name: 'West End', level: 2, pathname: 'UK > London > West End'},
{id: 33, name: 'Leeds', level: 1, pathname: 'UK > Leeds'},
{id: 35, name: 'Manchester', level: 1, pathname: 'UK > Manchester'}
]
I have been playing with _.chain, _.flatten and _.pluck and been unable to get anything close.

You can use a simple recursive helper function that would produce an array of arrays, and then use _.flattenDeep to flatten it. This does what you want:
function flattenMyTree(tree) {
function recurse(nodes, path) {
return _.map(nodes, function(node) {
var newPath = _.union(path, [node.name]);
return [
_.assign({pathname: newPath.join(' > '), level: path.length}, _.omit(node, 'children')),
recurse(node.children, newPath)
];
});
}
return _.flattenDeep(recurse(tree, []));
}

This one gives you the expected answer but it's not in ideal functional style;
The idea is to start flattening the array from outside. On each iteration of the while() loop the array is flattened by one level. Items of that level are appended to the result and children of those items are appended to the children array. In next iteration we flatten those children. When there are no more items in the children array it means that we finished processing the last level. Finally we sort items by pathname which effectively groups parents and their children together.
var current = data;
var level = 0;
var result = [];
var children = [];
while(current.length > 0) {
result = result.concat(_.map(current, function(item) {
if (!_.isArray(item.path)) {
item.path = [];
}
item.path.push(item.name);
children = children.concat(_.map(item.children, function(child) {
child.path = item.path.slice();
return child;
}));
item.level = level;
item.pathname = item.path.join(" > ");
delete item.path;
delete item.children;
return item;
}));
current = children;
children = [];
level++;
}
result.sort(function(a, b) {
return a.pathname.localeCompare(b.pathname);
});
console.log(result);

You can recursively reduce the data collection within a new collection array. Simply tap on each reduce callback for each item with the intention of recursively calling their descendants. The solution below makes use of lazy evaluation to add and remove specific fields.
DEMO
var data = [/*...*/];
function getPathHelper(collection, newCollection, path, level) {
return _.reduce(collection, function(newCollection, item) {
path += (path? ' > ': '') + item.name;
newCollection.push(
_(item)
.tap(_.partial(getPathHelper, item.children || [], newCollection, path, level + 1))
.omit('children')
.set('pathname', path)
.set('level', level)
.value()
);
return newCollection;
}, newCollection);
}
function getPath(collection) {
return _.sortBy(getPathHelper(collection, [], '', 0), 'pathname');
}
console.log(getPath(data));

This is based on #mik01aj's answer, but flattening in each step, and without the default empty array for the path parameter:
var data = [
{id: 0, name: 'Australia', children: [
{id: 10, name: 'Melbourne', children: []},
{id: 11, name: 'Sydney', children: [
{id: 100, name: 'Surry Hills', children: []},
{id: 102, name: 'Darlinghurst', children: []}
]},
{id: 13, name: 'Kambalda', children: []}
]},
{id: 1, name: 'Spain', children: [
{id: 20, name: 'Barcelona', children: []},
{id: 21, name: 'Madrid', children: []}
]},
{id: 3, name: 'UK', children: [
{id: 30, name: 'London', children: [
{id: 302, name: 'Knightsbridge', children: []},
{id: 309, name: 'West End', children: []}
]},
{id: 33, name: 'Leeds', children: []},
{id: 35, name: 'Manchester', children: []}
]}
];
function flattenMyTree(tree) {
function recurse(nodes, path) {
return _.flatMap(nodes, function(node) {
var newPath = _.union(path, [node.name]);
return _.concat([
_.assign({
pathname: newPath.join(' > '),
level: newPath.length - 1
},
_.omit(node, 'children'))
],
recurse(node.children, newPath)
);
});
}
return recurse(tree);
}
console.log(flattenMyTree(data));
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.11/lodash.min.js"></script>

Related

Filter unknown deep nested array based on input value

First I must say sorry if this question is already answered, but I have not found the answer I am looking for :(
I have an array of objects with unknown nesting depth (it can be 20-30 or even more) and I want to filter it's 'name' property based on input field value.
public nestedArray = [
{id: 1, name: 'Example_1', children: []},
{id: 2, name: 'Test', children: []},
{id: 3, name: 'Test Name', children: [
{id: 10, name: 'Child name', children: [
{id: 20, name: 'Example_14', children: []},
{id: 30, name: 'Last Child', children: []}
]
}
]
}
];
The result I want to receive is an array of objects with only one level deep with 'name' field which includes input value.
For example my input value is 'am', so the result would be:
resultsArray = [
{id: 1, name: 'Example_1'},
{id: 3, name: 'Test Name'},
{id: 10, name: 'Child name'},
{id: 20, name: 'Example_14'}
];
There is no problem to do it on the first level like that:
public filter(array: any[], input_value: string): void {
array = array.filter(el => {
return el.name.toLowerCase().includes(input_value.toLowerCase()));
}
}
Thanks in advance!
You could map the array and their children and take a flat result of objects where the string is matching the name property.
const
find = value => ({ children, ...o }) => [
...(o.name.includes(value) ? [o] : []),
...children.flatMap(find(value))
],
data = [{ id: 1, name: 'Example_1', children: [] }, { id: 2, name: 'Test', children: [] }, { id: 3, name: 'Test Name', children: [{ id: 10, name: 'Child name', children: [{ id: 20, name: 'Example_14', children: [] }, { id: 30, name: 'Last Child', children: [] }] }] }],
result = data.flatMap(find('am'));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Another solution with a single result array and a classic approach.
const
find = (array, value) => {
const
iter = array => {
for (const { children, ...o } of array) {
if (o.name.includes(value)) result.push(o);
iter(children);
}
},
result = [];
iter(array);
return result;
},
data = [{ id: 1, name: 'Example_1', children: [] }, { id: 2, name: 'Test', children: [] }, { id: 3, name: 'Test Name', children: [{ id: 10, name: 'Child name', children: [{ id: 20, name: 'Example_14', children: [] }, { id: 30, name: 'Last Child', children: [] }] }] }],
result = find(data, 'am');
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

How to convert a nested array with parent child relationship to a plain array?

I have an array with nested objects having parent-child relationship like so:
[
{id: 1, title: 'hello', parent: 0, children: [
{id: 3, title: 'hello', parent: 1, children: [
{id: 4, title: 'hello', parent: 3, children: [
{id: 5, title: 'hello', parent: 4, children: []},
{id: 6, title: 'hello', parent: 4, children: []}
]},
{id: 7, title: 'hello', parent: 3, children: []}
]}
]},
{id: 2, title: 'hello', parent: 0, children: [
{id: 8, title: 'hello', parent: 2, children: []}
]}
]
I need to convert it into a plain array retaining the parent child relationship like so and in the order of parent and all its children returned first before proceeding on to the next parent.
[
{id: 1, title: 'hello', parent: 0},
{id: 3, title: 'hello', parent: 1},
{id: 4, title: 'hello', parent: 3},
{id: 5, title: 'hello', parent: 4},
{id: 6, title: 'hello', parent: 4},
{id: 7, title: 'hello', parent: 3},
{id: 2, title: 'hello', parent: 0},
{id: 8, title: 'hello', parent: 2}
]
I was able to convert the other way round with a recursive function.
But I need to do the opposite in an efficient way. There is multilevel nesting as shown in the sample nested array.
EDIT: Updated the nested array to have an empty children array for leaf nodes.
And also, an answer in ES5 would help.
I just use a simple recursive function to make an array object into a plain array
var arr = [ {id: 1, title: 'hello', parent: 0, children: [ {id: 3, title: 'hello', parent: 1, children: [ {id: 4, title: 'hello', parent: 3, children: [ {id: 5, title: 'hello', parent: 4, children: []}, {id: 6, title: 'hello', parent: 4, children: []} ]}, {id: 7, title: 'hello', parent: 3, children: []} ]} ]}, {id: 2, title: 'hello', parent: 0, children: [ {id: 8, title: 'hello', parent: 2, children: []} ]} ];
var result = [];
var convertArrToObj = (arr) => {
arr.forEach(e => {
if (e.children) {
result.push({
id: e.id,
title: e.title,
parent: e.parent
});
convertArrToObj(e.children);
} else result.push(e);
});
};
convertArrToObj(arr);
console.log(result);
In ES5 you can also use some functional programming approach, and flatten an array with [].concat.apply:
function flatten(arr) {
return [].concat.apply([], arr.map(function (obj) {
return [].concat.apply([
{ id: obj.id, title: obj.title, parent: obj.parent }
], flatten(obj.children));
}));
}
let arr = [{id: 1, title: 'hello', parent: 0, children: [{id: 3, title: 'hello', parent: 1, children: [{id: 4, title: 'hello', parent: 3, children: [{id: 5, title: 'hello', parent: 4, children: []},{id: 6, title: 'hello', parent: 4, children: []}]},{id: 7, title: 'hello', parent: 3, children: []}]}]},{id: 2, title: 'hello', parent: 0, children: [{id: 8, title: 'hello', parent: 2, children: []}]}];
console.log(flatten(arr));
In ES6 the same algorithm reduces to the following:
const flatten = arr => arr.flatMap(({children, ...o}) => [o, ...flatten(children)]);
let arr = [{id: 1, title: 'hello', parent: 0, children: [{id: 3, title: 'hello', parent: 1, children: [{id: 4, title: 'hello', parent: 3, children: [{id: 5, title: 'hello', parent: 4, children: []},{id: 6, title: 'hello', parent: 4, children: []}]},{id: 7, title: 'hello', parent: 3, children: []}]}]},{id: 2, title: 'hello', parent: 0, children: [{id: 8, title: 'hello', parent: 2, children: []}]}];
console.log(flatten(arr));
Using ES5 would require a lot more lines of code and like you said is not very efficient.
Here's my ES5 version, you should be able to notice the difference in performance
const data = [{id:1,title:'hello',parent:0,children:[{id:3,title:'hello',parent:1,children:[{id:4,title:'hello',parent:3,children:[{id:5,title:'hello',parent:4,children:[]},{id:6,title:'hello',parent:4,children:[]}]},{id:7,title:'hello',parent:3,children:[]}]}]},{id:2,title:'hello',parent:0,children:[{id:8,title:'hello',parent:2,children:[]}]}];
// Recursively
function reduceArrayDimension(array) {
var level = [];
array.forEach(function(item) {
level.push({
id: item.id,
title: item.title,
parent: item.parent
});
item.children.forEach(function(child) {
reduceArrayDimension([child]).forEach(function(childItem) {
level.push(childItem);
});
});
});
return level;
}
console.log(reduceArrayDimension(data));
And ES6
const data=[{id:1,title:'hello',parent:0,children:[{id:3,title:'hello',parent:1,children:[{id:4,title:'hello',parent:3,children:[{id:5,title:'hello',parent:4,children:[]},{id:6,title:'hello',parent:4,children:[]}]},{id:7,title:'hello',parent:3,children:[]}]}]},{id:2,title:'hello',parent:0,children:[{id:8,title:'hello',parent:2,children:[]}]}];
// Recursively
function reduceArrayDimension(array) {
const level = [];
array.forEach(item => {
level.push({id: item.id, title: item.title, parent: item.parent});
if (item.children) level.push(...reduceArrayDimension(item.children));
});
return level;
}
console.log(reduceArrayDimension(data));
if data is large can consider use tail optimization and async/await
const arr = [
{id: 1, title: 'hello', parent: 0, children: [
{id: 3, title: 'hello', parent: 1, children: [
{id: 4, title: 'hello', parent: 3, children: [
{id: 5, title: 'hello', parent: 4},
{id: 6, title: 'hello', parent: 4}
]},
{id: 7, title: 'hello', parent: 3}
]}
]},
{id: 2, title: 'hello', parent: 0, children: [
{id: 8, title: 'hello', parent: 2}
]}
];
const convertArr = (arr) => {
return arr.reduce((init, cur) => {
const plain = init.concat(cur);
const children = cur.children;
return plain.concat(children && children.length ? convertArr(children) : [])
}, [])
}
const generateArr = (arr) => {
return convertArr(arr).map(v => ({
id: v.id,
parent: v.parent,
title: v.title
}))
}
console.log('result:', generateArr(arr))
If the data is not very large, this could be a pragmatic method
const data = [{ id: 1, title: 'hello', parent: 0, children: [{ id: 3, title: 'hello', parent: 1, children: [{ id: 4, title: 'hello', parent: 3, children: [{ id: 5, title: 'hello', parent: 4 }, { id: 6, title: 'hello', parent: 4 , children:[]} ] }, { id: 7, title: 'hello', parent: 3 } ] }] }, { id: 2, title: 'hello', parent: 0, children: [{ id: 8, title: 'hello', parent: 2 }] } ];
const str = JSON.stringify(data)
.replaceAll('"children":[',"},")
.replaceAll("]}","")
.replaceAll(",,",",") // handle empty children
.replaceAll(",}","}");
console.log(JSON.parse(str).sort((a,b) => a.id-b.id))
You can use a generator function:
var arr = [ {id: 1, title: 'hello', parent: 0, children: [ {id: 3, title: 'hello', parent: 1, children: [ {id: 4, title: 'hello', parent: 3, children: [ {id: 5, title: 'hello', parent: 4, children: []}, {id: 6, title: 'hello', parent: 4, children: []} ]}, {id: 7, title: 'hello', parent: 3, children: []} ]} ]}, {id: 2, title: 'hello', parent: 0, children: [ {id: 8, title: 'hello', parent: 2, children: []} ]} ];
function* flatten(d){
for (var i of d){
yield {id:i.id, title:i.title, parent:i.parent}
yield* flatten(i.children)
}
}
console.log([...flatten(arr)])

Sort aray of objects by another array of objects reference

I need some help to sort this data out, i have an array of products and i need to sort and display by settings configuration. The output must have the same order as settings array (index) and if display is true. Thanks in advance. This is what i tryed:
var products = [
{id: 0, name: 'Chocolate', category: 'Sweet'},
{id: 1, name: 'Almendras', category: 'Fruit'},
{id: 2, name: 'Nueces', category: 'Fruit'},
{id: 3, name: 'Mermelada', category: 'Jam'},
{id: 4, name: 'Alfajor', category: 'Sweet'},
{id: 5, name: 'Queso', category: 'UwU'},
{id: 6, name: 'Arandanos', category: 'Fruit'},
{id: 7, name: 'Maracuya', category: 'Fruit'}
];
let settings = [
{
name: 'Fruit',
display: true
},
{
name: 'Jam',
display: false
},
{
name: 'Sweet',
display: true
},
{
name: 'UwU',
display: true
}
]
let group = products.reduce((r, a) => {
r[a.category] = [...r[a.category] || [], a];
return r;
}, {});
let arrangedProducts = Object.keys(group);
console.log(group);
console.log(arrangedProducts);
This is my expected output:
/*
expected result = [
[
{id: 1, name: 'Almendras', category: 'Fruit'},
{id: 2, name: 'Nueces', category: 'Fruit'},
{id: 6, name: 'Arandanos', category: 'Fruit'},
{id: 7, name: 'Maracuya', category: 'Fruit'}
],
[
{id: 0, name: 'Chocolate', category: 'Sweet'},
{id: 4, name: 'Alfajor', category: 'Sweet'}
],
[
{id: 5, name: 'Queso', category: 'UwU'}
]
]
*/
Solution
Making of groups
Apply settings and retrieve the result
const products = [
{ id: 0, name: "Chocolate", category: "Sweet" },
{ id: 1, name: "Almendras", category: "Fruit" },
{ id: 2, name: "Nueces", category: "Fruit" },
{ id: 3, name: "Mermelada", category: "Jam" },
{ id: 4, name: "Alfajor", category: "Sweet" },
{ id: 5, name: "Queso", category: "UwU" },
{ id: 6, name: "Arandanos", category: "Fruit" },
{ id: 7, name: "Maracuya", category: "Fruit" },
];
const productsGroup = products.reduce((r, a) => {
r[a.category] = [...(r[a.category] || []), a];
return r;
}, {});
function applySettings(settings) {
return settings.filter((s) => s.display).map((s) => productsGroup[s.name]);
}
console.log(
applySettings([
{
name: "Fruit",
display: true,
},
{
name: "Jam",
display: false,
},
])
);
console.log(
applySettings([
{
name: "Fruit",
display: true,
},
{
name: "Sweet",
display: true,
},
{
name: "UwU",
display: true,
},
])
);
You can filter your settings list based on the display property and then use Array.map to return a list of objects in products that match the category:
const products = [
{id: 0, name: 'Chocolate', category: 'Sweet'},
{id: 1, name: 'Almendras', category: 'Fruit'},
{id: 2, name: 'Nueces', category: 'Fruit'},
{id: 3, name: 'Mermelada', category: 'Jam'},
{id: 4, name: 'Alfajor', category: 'Sweet'},
{id: 5, name: 'Queso', category: 'UwU'},
{id: 6, name: 'Arandanos', category: 'Fruit'},
{id: 7, name: 'Maracuya', category: 'Fruit'}
];
const settings = [
{ name: 'Fruit', display: true },
{ name: 'Jam', display: false },
{ name: 'Sweet', display: true },
{ name: 'UwU', display: true }
];
const result = settings
.filter(c => c.display)
.map(c => products.filter(o => o.category == c.name));
console.log(result);
Note that this code does filter the products array for each settings value that has display:true, so may be slow for large arrays. However filter is pretty low overhead and testing with OP's sample data shows this to run 3x the speed of the reduce version; and with a larger products array (99 entries) to run 10x faster.
This should be pretty quick, because it continues on to the next iteration without executing the inner loop when display is false:
var products = [
{id: 0, name: 'Chocolate', category: 'Sweet'},
{id: 1, name: 'Almendras', category: 'Fruit'},
{id: 2, name: 'Nueces', category: 'Fruit'},
{id: 3, name: 'Mermelada', category: 'Jam'},
{id: 4, name: 'Alfajor', category: 'Sweet'},
{id: 5, name: 'Queso', category: 'UwU'},
{id: 6, name: 'Arandanos', category: 'Fruit'},
{id: 7, name: 'Maracuya', category: 'Fruit'}
];
let settings = [
{
name: 'Fruit',
display: true
},
{
name: 'Jam',
display: false
},
{
name: 'Sweet',
display: true
},
{
name: 'UwU',
display: true
}
];
function sortProducts(){
const r = [];
let i = -1;
for(let s of settings){
if(!s.display){
continue;
}
i++;
for(let o of products){
if(s.name === o.category){
if(r[i]){
r[i].push(o);
}
else{
r.push([o]);
}
}
}
}
return r;
}
console.log(sortProducts());

Count the number of items in each category within a JavaScript object

I have an array containing several hundred objects, each of which has a category. I wish to return an object that lists out the categories with a count of the number of items for each category.
const arr = [
{id: 1, name: 'ford', category: 'vehicle'},
{id: 2, name: 'pig', category: 'animal'},
{id: 3, name: 'dog', category: 'animal'},
{id: 4, name: 'chev', category: 'vehicle'},
{id: 5, name: 'cat', category: 'animal'},
{id: 6, name: 'jeep', category: 'vehicle'},
{id: 7, name: 'honda', category: 'vehicle'}
]
How would I loop through the object and create a new object that contains just the two categories and how many of each per category?
Desired output:
{vehicle: 4, animal: 3}
Code:
const arr = [
{id: 1, name: 'ford', category: 'vehicle'},
{id: 2, name: 'pig', category: 'animal'},
{id: 3, name: 'dog', category: 'animal'},
{id: 4, name: 'chev', category: 'vehicle'},
{id: 5, name: 'cat', category: 'animal'},
{id: 6, name: 'jeep', category: 'vehicle'},
{id: 7, name: 'honda', category: 'vehicle'}
]
const final = {};
arr.forEach((v) => {
const tst = v.category;
console.log(tst);
if (tst in final){
console.log('found one');
}
});
//console.log(final);
You can use reduce
const arr = [
{id: 1, name: 'ford', category: 'vehicle'},
{id: 2, name: 'pig', category: 'animal'},
{id: 3, name: 'dog', category: 'animal'},
{id: 4, name: 'chev', category: 'vehicle'},
{id: 5, name: 'cat', category: 'animal'},
{id: 6, name: 'jeep', category: 'vehicle'},
{id: 7, name: 'honda', category: 'vehicle'}
]
const categories = arr.reduce((acc, cur) => {
acc[cur.category] = (acc[cur.category] || 0) + 1
return acc;
}, {})
console.log(categories)
edit:
Now, after a year a would wrt this like that
const arr = [
{id: 1, name: 'ford', category: 'vehicle'},
{id: 2, name: 'pig', category: 'animal'},
{id: 3, name: 'dog', category: 'animal'},
{id: 4, name: 'chev', category: 'vehicle'},
{id: 5, name: 'cat', category: 'animal'},
{id: 6, name: 'jeep', category: 'vehicle'},
{id: 7, name: 'honda', category: 'vehicle'}
]
const categories = arr.reduce((acc, cur) => Object.assign(acc, {
[cur.category]: (acc[cur.category] || 0) + 1,
}), {})
console.log(categories)
It looks like the category will always exist, so you don't need to check whether it exists, but what it contains; take what it contains and increment that property on the final object:
const arr = [
{id: 1, name: 'ford', category: 'vehicle'},
{id: 2, name: 'pig', category: 'animal'},
{id: 3, name: 'dog', category: 'animal'},
{id: 4, name: 'chev', category: 'vehicle'},
{id: 5, name: 'cat', category: 'animal'},
{id: 6, name: 'jeep', category: 'vehicle'},
{id: 7, name: 'honda', category: 'vehicle'}
]
const final = {};
for (const { category } of arr) {
final[category] = (final[category] || 0) + 1;
};
console.log(final);
You have the right idea regarding looping over the array and checking if the category was already encountered. What you're missing is initializing a counter when you find a new category and incrementing it the next time that category is encountered:
const arr = [
{id: 1, name: 'ford', category: 'vehicle'},
{id: 2, name: 'pig', category: 'animal'},
{id: 3, name: 'dog', category: 'animal'},
{id: 4, name: 'chev', category: 'vehicle'},
{id: 5, name: 'cat', category: 'animal'},
{id: 6, name: 'jeep', category: 'vehicle'},
{id: 7, name: 'honda', category: 'vehicle'}
]
const final = {};
arr.forEach((v) => {
const cat = v.category;
if (cat in final) {
final[cat]++;
} else {
final[cat] = 1;
}
});
console.log(final);
const arr = [
{ id: 1, name: 'ford', category: 'vehicle' },
{ id: 2, name: 'pig', category: 'animal' },
{ id: 3, name: 'dog', category: 'animal' },
{ id: 4, name: 'chev', category: 'vehicle' },
{ id: 5, name: 'cat', category: 'animal' },
{ id: 6, name: 'jeep', category: 'vehicle' },
{ id: 7, name: 'honda', category: 'vehicle' },
]
// this will hold the results
const result = {}
for (const item of arr) {
// we have not encountered such category before
if (result[item.category] === undefined) {
// setting this category to 1
result[item.category] = 1
// we encountered such category before
} else {
// addint +1 to it
result[item.category] += 1
}
}
console.log(result)

Sort array of nested objects into separate array of arrays

I have an array:
[
{ id: 1,
name: "parent1",
children: [
{ id: 10,
name: "first_child_of_id_1",
children: [
{ id: 100, name: "child_of_id_10", children: []},
{ id: 141, name: "child_of_id_10", children: []},
{ id: 155, name: "child_of_id_10", children: []}
]
},
{ id: 42,
name: "second_child_of_id_1",
children: [
{ id: 122, name: "child_of_id_42", children: []},
{ id: 133, name: "child_of_id_42", children: []},
{ id: 177, name: "child_of_id_42", children: []}
]
}
]
},
{ id: 7,
name: "parent7",
children: [
{ id: 74,
name: "first_child_of_id_7",
children: [
{ id: 700, name: "child_of_id_74", children: []},
{ id: 732, name: "child_of_id_74", children: []},
{ id: 755, name: "child_of_id_74", children: []}
]
},
{ id: 80,
name: "second_child_of_id_7",
children: [
{ id: 22, name: "child_of_id_80", children: []},
{ id: 33, name: "child_of_id_80", children: []},
{ id: 77, name: "child_of_id_80", children: []}
]
}
]
}
]
What I need is an array of arrays like this:
[
[ "id", "name", "parent_id", "parent_name" ],
[ 1, "parent1", null, "" ],
[ 10, "first_child_of_id_1", 1, "parent1"],
[ 42, "second_child_of_id_1", 1, "parent1"],
[100, "child_of_id_10", 10, "first_child_of_id_1"]
]
and so on for all nested objects for me to convert them into CSV rows. I've checked many answers and found a similar problem here: How to convert array of nested objects to CSV?
But it produces too long rows for many nested objects and I am not experienced enough with JavaScript to modify map function.
const categories = [
{ id: 1,
name: "parent1",
children: [
{ id: 10,
name: "first_child_of_id_1",
children: [
{ id: 100, name: "child_of_id_10", children: []},
{ id: 141, name: "child_of_id_10", children: []},
{ id: 155, name: "child_of_id_10", children: []}
]
},
{ id: 42,
name: "second_child_of_id_1",
children: [
{ id: 122, name: "child_of_id_42", children: []},
{ id: 133, name: "child_of_id_42", children: []},
{ id: 177, name: "child_of_id_42", children: []}
]
}
]
},
{ id: 7,
name: "parent7",
children: [
{ id: 74,
name: "first_child_of_id_7",
children: [
{ id: 700, name: "child_of_id_74", children: []},
{ id: 732, name: "child_of_id_74", children: []},
{ id: 755, name: "child_of_id_74", children: []}
]
},
{ id: 80,
name: "second_child_of_id_7",
children: [
{ id: 22, name: "child_of_id_80", children: []},
{ id: 33, name: "child_of_id_80", children: []},
{ id: 77, name: "child_of_id_80", children: []}
]
}
]
}
]
function pivot(arr) {
var mp = new Map();
function setValue(a, path, val) {
if (Object(val) !== val) { // primitive value
var pathStr = path.join('.');
var i = (mp.has(pathStr) ? mp : mp.set(pathStr, mp.size)).get(pathStr);
a[i] = val;
} else {
for (var key in val) {
setValue(a, key == '0' ? path : path.concat(key), val[key]);
}
}
return a;
}
var result = arr.map(obj => setValue([], [], obj));
return [[...mp.keys()], ...result];
}
function toCsv(arr) {
return arr.map(row =>
row.map(val => isNaN(val) ? JSON.stringify(val) : +val).join(',')
).join('\n');
}
<button onclick="console.log(toCsv(pivot(categories)))">Output</button>
Simple DFS or BFS algorithm should do the job here.
The difference is the order of created "rows". If you want to have all children of given node listed immediately after their parent, then you need to use BFS.
Example with DFS and BFS:
const input = [{
id: 1,
name: "parent1",
children: [{
id: 10,
name: "first_child_of_id_1",
children: [{
id: 100,
name: "child_of_id_10",
children: []
},
{
id: 141,
name: "child_of_id_10",
children: []
},
{
id: 155,
name: "child_of_id_10",
children: []
}
]
},
{
id: 42,
name: "second_child_of_id_1",
children: [{
id: 122,
name: "child_of_id_42",
children: []
},
{
id: 133,
name: "child_of_id_42",
children: []
},
{
id: 177,
name: "child_of_id_42",
children: []
}
]
}
]
},
{
id: 7,
name: "parent7",
children: [{
id: 74,
name: "first_child_of_id_7",
children: [{
id: 700,
name: "child_of_id_74",
children: []
},
{
id: 732,
name: "child_of_id_74",
children: []
},
{
id: 755,
name: "child_of_id_74",
children: []
}
]
},
{
id: 80,
name: "second_child_of_id_1",
children: [{
id: 22,
name: "child_of_id_80",
children: []
},
{
id: 33,
name: "child_of_id_80",
children: []
},
{
id: 77,
name: "child_of_id_80",
children: []
}
]
}
]
}
]
//DFS
function deepWalk(node, parent, output = []) {
if (!node || typeof node !== 'object' || !node.id) return;
output.push([node.id, node.name, parent ? parent.id : null, parent ? parent.name : ""])
if (node.children) {
for (const child of node.children) {
deepWalk(child, node, output);
}
}
return output;
}
//BFS
function broadWalk(root) {
const output = []
const queue = [];
queue.push({
node: root,
parent: null
});
while (queue.length) {
const {
node,
parent
} = queue.shift();
output.push([node.id, node.name, parent ? parent.id : null, parent ? parent.name : ""])
if (node.children) {
for (const child of node.children) {
queue.push({
node: child,
parent: node
});
}
}
}
return output;
}
let rowsDfs = [
["id", "name", "parent_id", "parent_name"]
];
let rowsBfs = [
["id", "name", "parent_id", "parent_name"]
];
for (const node of input) {
rowsDfs = [...rowsDfs, ...deepWalk(node)];
rowsBfs = [...rowsBfs, ...broadWalk(node)];
}
console.log("rows DFS: ", rowsDfs)
console.log("rows BFS: ", rowsBfs)

Categories

Resources