Lodash - Group Children using N (repeated) keys for parent - javascript

I have a select from database that basically joins a master entity and a child entity, like the example below (Cars vs Parts) as snippet
And I'd like to group by all the keys for the Car part, and have an array of the parts, but including all the keys for the car and the parts. For the groupBy examples I could find, generally it uses groupBy, but it only groups one key only. I was able to achieve using a lot of workarounds, but I'm sure it is manageable (and achieve more performance) to do the same using either es6 or lodash.
Could someone help me in this matter? I've tried multiple groupBy and reduce combinations, but was not able to chain those correctly.
var data = [{id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId1',
partName: 'partName1'},
{id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId2',
partName: 'partName2'},
{id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId3',
partName: 'partName3'},
{id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId4',
partName: 'partName4'}
];
var dictionary = {};
data.forEach(function(item, index, array)
{
var masterDocument = null;
if (typeof dictionary[item.id] === 'undefined')
{
masterDocument = {
id: item.id,
name: item.name,
description: item.description,
parts: []
};
dictionary[item.id] = masterDocument;
}
else {
var masterDocument = dictionary[item.id];
}
masterDocument.parts.push({
partId: item.partId,
partName: item.partName
})
})
var asList = [];
Object.keys(dictionary).forEach((item) => {
asList.push(dictionary[item])
});
console.log(asList);
.as-console-wrapper {
min-height: 100%;
top: 0;
}
Here's the snippet with just the result I want to achieve.
[
{
"id": "car1",
"name": "name for car 1",
"description": "description for car1",
"parts": [
{
"partId": "partId1",
"partName": "partName1"
},
{
"partId": "partId2",
"partName": "partName2"
}
]
},
{
"id": "car2",
"name": "name for car 2",
"description": "description for car2",
"parts": [
{
"partId": "partId3",
"partName": "partName3"
},
{
"partId": "partId4",
"partName": "partName4"
}
]
}
]

The code below should solve your problem using Lodash. Basically what you want to do is:
Group the cars by id
Once you have the cars grouped by their IDs, iterate over that top-level array with a map call, and grab the id, name, and description from the first entry (since you know these are all the same for all cars in this group). Save these for later for your return object
Then, while still in this top-level map iteration, also iterate over the individual cars in each carGrouping (a nested map) to get their partId and partName, and put those into a parts array
Finally, get all of your object attributes, put them all into a return object in your top-level map call, and return them all back
Don't forget to call valueOf() at the end of your chain to get the Lodash sequence to fire
let data = [{id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId1',
partName: 'partName1'},
{id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId2',
partName: 'partName2'},
{id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId3',
partName: 'partName3'},
{id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId4',
partName: 'partName4'}
];
const carsInfo = _(data)
.groupBy('id')
.map(carGrouping => {
// all cars in this array have the same id, name, description, so just grab them from the first one
const firstCarInGroup = _.first(carGrouping);
const id = firstCarInGroup.id;
const name = firstCarInGroup.name;
const description = firstCarInGroup.description;
// do a nested map call to iterate over each car in the carGrouping, and grab their partId and partName, and return it in an object
const parts = _.map(carGrouping, car => {
return {
partId: car.partId,
partName: car.partName
}
});
return {
id,
name,
description,
parts
}
})
.valueOf();
console.log(carsInfo);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>

This one uses no dependencies. Just plain ES6+.
const data = [{
id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId1',
partName: 'partName1'
},
{
id: 'car1',
name: 'name for car 1',
description: 'description for car1',
partId: 'partId2',
partName: 'partName2'
},
{
id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId3',
partName: 'partName3'
},
{
id: 'car2',
name: 'name for car 2',
description: 'description for car2',
partId: 'partId4',
partName: 'partName4'
}
];
const nested = data.reduce((acc, part) => {
let index = acc.findIndex(car => car.id === part.id)
const { partId, partName, ...car } = part
if (index === -1) {
acc.push({
...car,
parts: [],
})
index = acc.length - 1
}
acc[index].parts.push({
partId,
partName,
})
return acc
}, [])
console.log(JSON.stringify(nested, null, ' '));

Related

How to check if array of objects that contains another array of object has property

Hi I have an array of objects which contains another array of objects.
I need to find an object in array which contains another object in it's array with certain propery
ID.
Let's say i need to find an object in casses array which contains a user with certain ID.
ID for user is unique.
casses = [
{
caseName: 'case 1',
date: '2021-05-4',
id: '123',
user: [{name: 'Vlad', id: '1'}, {name: 'Misha', id: '2'}]
},
{
caseName: 'case 2',
date: '2021-05-4',
id: '123',
user: [{name: 'Alina', id: '3'}, {name: 'Alex', id: '4'}]
},
{
caseName: 'case 3',
date: '2021-05-4',
id: '123',
user: []
},
]
I could use a nested loops and so on. But i wondering is it possible to do with one line ?
Something like this but one level deeper:
let val = casses(item => item.id === element.id); ​
Assume your case with ID set to "3"
Try below
const ID = "3";
const casses = [
{
caseName: "case 1",
date: "2021-05-4",
id: "123",
user: [
{ name: "Vlad", id: "1" },
{ name: "Misha", id: "2" }
]
},
{
caseName: "case 2",
date: "2021-05-4",
id: "123",
user: [
{ name: "Alina", id: "3" },
{ name: "Alex", id: "4" }
]
},
{
caseName: "case 3",
date: "2021-05-4",
id: "123",
user: []
}
];
casses.find(item => item.user.some(subItem => subItem.id === ID));

Restructure objects based on their inner arrays duplicates

I have this array of tasks:
[
{ id: 54321, Task: 'Task 1', Topics: ["111", "222"]},
{ id: 667566, Task: 'Task 2', Topics: ["222"] },
{ id: 76889, Task: 'Task 3', Topics: ["333"] },
]
and I want to restructure it based on duplicate strings inside Topics,
So the result should be:
[{
name: "111",
id: [54321]
}, {
name: "222",
id: [54321, 667566]
}, {
name: "333",
id: [76889]
}]
basically topic names should become unique and tasks id should group under the topic's name
You can use Array.reduce() to create the desired structure, we create a map of topics, using the topic name as the key, then use Object.values() to get the result array:
const tasks = [
{ id: 54321, Task: 'Task 1', Topics: ["111", "222"]},
{ id: 667566, Task: 'Task 2', Topics: ["222"] },
{ id: 76889, Task: 'Task 3', Topics: ["333"] },
]
const result = Object.values(tasks.reduce((acc, cur) => {
for(let topic of cur.Topics) {
if (!acc[topic]) acc[topic] = { name: topic, id: [] }
acc[topic].id.push(cur.id);
}
return acc;
}, {}))
console.log('Result:', result);

Compare two arrays, get out the duplicates

I built a courses page with 3 courses,
i want to build it in a way that all the courses are displayed with a buy button, the course info is collected from the database via the products collection.
then i want that if the user bought the course, instead of buy, watch will be displayed.
to do that i have 2 arrays:
1) all the products
2) the products that the user bought
now i want to compare them, and delete from the first array all the products that the user already bought.
i tried checking online for methods, but i didn't understand them at all.
Here is the function to get the arrays:
const productRef = await db.collection("products").get();
const products = await productRef.docs.map(doc => {
return {
id: doc.id,
name: doc.data().name,
price: doc.data().price
};
});
console.log(products);
//[ { id: 'course1', name: 'Course 1', price: 25 },
{ id: 'course2', name: 'Course 2', price: 10 },
{ id: 'course3', name: 'Course 3', price: 30 } ]
if (user) {
const claims = res.locals.decodedClaims;
const uid = claims.uid;
const email = claims.email;
const userProductsRef = await db
.collection("users")
.doc(uid)
.collection("courses")
.get();
if (userProductsRef.docs.length > 0) {
const userProducts = userProductsRef.docs.map(doc => {
return { id: doc.id, name: doc.data().name };
});
console.log(userProducts);//[ { id: 'course1', name: 'Course 1' } ]
///COMPARE HERE the two arrays
}
}
now i want to compare products with userProducts by the id.
so expected result at the end should be something like:
products= [
{ id: 'course2', name: 'Course 2', price: 10 },
{ id: 'course3', name: 'Course 3', price: 30 } ];
userProducts= [ { id: 'course1', name: 'Course 1' } ]
Thank You!
You can filter the products array by checking to see if each product id is in the userProducts array:
const filteredProducts = products
.filter(prod => !userProducts.find(userProd => userProd.id === prod.id))
console.log(filteredProducts)
// [
// { id: 'course2', name: 'Course 2', price: 10 },
// { id: 'course3', name: 'Course 3', price: 30 }
// ]
I hope this is what you were looking for.
You could use a combination of .filter() and .find() to achieve what you want:
const products = [
{ id: 'course1', name: 'Course 1', price: 25 },
{ id: 'course2', name: 'Course 2', price: 10 },
{ id: 'course3', name: 'Course 3', price: 30 },
];
const userProducts = [
{ id: 'course1', name: 'Course 1' }
];
const result = products.filter(product => {
return !userProducts.find(o => o.id === product.id);
});
console.log(result);

How to group an array based on a key and create a dynamic object with grouped array as values?

I've got multiple items, some of them with the same title. I want to create an multidimensional array with the title as the first key and a unique number as the second key. So it's possible to categorize the items by title.
Example:
itemArray['title1'][0] = item1
itemArray['title1'][1] = item2
itemArray['title2'][0] = item3
My example is working with this code, but in my opinion it's to complicated and I hope there is an other way with JavaScript.
let itemArray = {}
items.forEach(item => {
let title = item['title']
if (itemArray[title] == null) {
itemArray[title] = {}
}
let index = Object.keys(itemArray[title]).length
itemArray[title][index] = item
},
)
The Input:
[ RowDataPacket {
uid: 1,
title: 'booktitle',
description: 'description' },
RowDataPacket {
uid: 2,
title: 'booktitle',
description: 'description 2' },
RowDataPacket {
uid: 1,
title: 'booktitle 2',
description: 'description' } ]
Expected output (Since it's the result of a sql query the item is a RowDataPacket):
{ 'booktitle':
{ '0':
RowDataPacket {
uid: 1,
title: 'booktitle',
description: 'description' } },
{ '1':
RowDataPacket {
uid: 2,
title: 'booktitle',
description: 'description 2' } },
'booktitle 2':
{ '0':
RowDataPacket {
uid: 1,
title: 'booktitle 2',
description: 'description' } }
}
It's easy with PHP, there it's working like this:
$itemArray = array();
foreach ($items as $key => $item) {
$itemArray[$item['title']][] = $item;
}
Thanks in advance!
You could reduce the array by taking a default array and push the item.
var items = [{ title: 'title1' }, { title: 'title1' }, { title: 'title2' }],
result = items.reduce((r, item) => {
(r[item.title] = r[item.title] || []).push(item);
return r;
}, {});
console.log(result);
You've got the correct idea. The itemArray you created is not a multidimensional array. It's an object with each title as key and an array of items which share the same title as their value. You could probably simplify your forEach like this:
var items = [{
uid: 1,
title: 'booktitle',
description: 'description'
}, {
uid: 2,
title: 'booktitle',
description: 'description 2'
}, {
uid: 1,
title: 'booktitle 2',
description: 'description'
}]
let itemArray = {}
items.forEach(item => {
itemArray[item.title] = itemArray[item.title] || [];
itemArray[item.title].push(item)
})
console.log(itemArray)
Checck if itemArray already has the title as a key. If yes, use it. Else, point it to an empty array []. Then just push the current item to that property.
With reduce, you can even simplify it to:
var items=[{uid:1,title:'booktitle',description:'description'},{uid:2,title:'booktitle',description:'description 2'},{uid:1,title:'booktitle 2',description:'description'}]
let itemArray = items.reduce((acc,i) =>
((acc[i.title] = acc[i.title] || []).push(i), acc)
,{})
console.log(itemArray)
Expected output
{ 'booktitle':
{ '0':
RowDataPacket {
uid: 1,
title: 'booktitle',
description: 'description' } },
{ '1':
RowDataPacket {
uid: 2,
title: 'booktitle',
description: 'description 2' } },
'booktitle 2':
{ '0':
RowDataPacket {
uid: 1,
title: 'booktitle 2',
description: 'description' } }
}
That's a bad practice. Don't use enumerated properties on Objects. You see how cumbersome they are. Unlike PHP, JS has no associative Arrays; so using Objects for that is the right equivalent. But for indexed Arrays you should use Arrays in JS, not Objects.
var data = [{
uid: 1,
title: 'booktitle',
description: 'description'
},
{
uid: 2,
title: 'booktitle',
description: 'description 2'
},
{
uid: 1,
title: 'booktitle 2',
description: 'description'
}
];
const booksByTitle = {};
for (const item of data) {
const {
title
} = item;
if (!(title in booksByTitle)) booksByTitle[title] = [];
booksByTitle[title].push(item);
}
console.log(booksByTitle);
.as-console-wrapper{top:0;max-height:100%!important}
but there are also plenty of frameworks in JS that offer some groupBy method, something like this quick implementation:
var data = [{
uid: 1,
title: 'booktitle',
description: 'description'
},
{
uid: 2,
title: 'booktitle',
description: 'description 2'
},
{
uid: 1,
title: 'booktitle 2',
description: 'description'
}
];
function groupBy(iterable, keyOrFn) {
const fn = typeof keyOrFn === "function" ?
keyOrFn :
(item) => item == null ? undefined : item[keyOrFn];
var result = Object.create(null);
for (const item of iterable) {
const key = fn(item);
if (key in result) {
result[key].push(item);
} else {
result[key] = [item];
}
}
return result;
}
const booksByTitle = groupBy(data, "title");
console.log(booksByTitle);
.as-console-wrapper{top:0;max-height:100%!important}

Merge & Group Two Javascript array of objects and Group

I have two arrays of objects. One array contains list of items, another array contains list of categories. I want to create a new array based on categoryIds. I tried using lodash. But, couldn't get the correct solution.
I can do this using looping. But, I am looking for more clean approach.
var items = [
{
id: '001',
name: 'item1',
description: 'description of item1',
categoryId: 'cat1'
},
{
id: '002',
name: 'item2',
description: 'description of item2',
categoryId: 'cat2'
},
{
id: '003',
name: 'item3',
description: 'description of item3',
categoryId: 'cat1'
},
{
id: '004',
name: 'item4',
description: 'description of item4'
}
];
var categories = [
{
id: 'cat1',
name: 'Category1'
},
{
id: 'cat2',
name: 'Category2'
}
];
Expected output
[
{
categoryId: 'cat1',
name: 'Category1',
items: [
{
id: '001',
name: 'item1',
description: 'description of item1',
categoryId: 'cat1'
},
{
id: '003',
name: 'item3',
description: 'description of item3',
categoryId: 'cat1'
}
]
},
{
categoryId: 'cat2',
name: 'Category2',
items: [
{
id: '002',
name: 'item2',
description: 'description of item2',
categoryId: 'cat2'
}
]
},
{
categoryId: '',
name: '',
items: [
{
id: '004',
name: 'item4',
description: 'description of item4'
}
]
}
]
https://jsfiddle.net/sfpd3ppn/
Thanks for the help
The following does the trick:
var items = [{ id: '001', name: 'item1', description: 'description of item1', categoryId: 'cat1' }, { id: '002', name: 'item2', description: 'description of item2', categoryId: 'cat2' }, { id: '003', name: 'item3', description: 'description of item3', categoryId: 'cat1' }, { id: '004', name: 'item4', description: 'description of item4' } ];
var categories = [ { id: 'cat1', name: 'Category1' }, { id: 'cat2', name: 'Category2' } ];
var output = categories.concat([{id:'',name:''}]).map(function(v) {
return {
categoryId: v.id,
name: v.name,
items: items.filter(function(o) {
return o.categoryId === v.id || !o.categoryId && !v.id;
})
};
});
console.log(output);
I start by using .concat() to create a new categories array that holds the original categories items plus an "empty" category. Then I .map() that array to return category objects with your desired output structure, each of which has an items array that is produced by .filter()ing the original items array.
(Note that the items arrays within the output contain references to the same objects that were in the original items input, not copies of them. If you wanted copies you could add another .map() after the .filter().)
You can accomplish the desired result using a reduce. We are going to start with the original categories array and reduce the items array into it.
var items = [
{ id: '001', name: 'item1', description: 'description of item1', categoryId: 'cat1' },
{ id: '002', name: 'item2', description: 'description of item2', categoryId: 'cat2' },
{ id: '003', name: 'item3', description: 'description of item3', categoryId: 'cat1' },
{ id: '004', name: 'item4', description: 'description of item4' }
];
var categories = [
{ id: 'cat1', name: 'Category1' },
{ id: 'cat2', name: 'Category2' }
];
// Lets add the empty category at the beginning. This simplifies the logic.
categories.push({ id: '', name: '' });
// This is a function that will return a function to be used as a filter later on
function createFilter (category) {
return function (item) {
return item.id === category;
};
}
var mergedSet = items.reduce(function (previous, current) {
// Get the category ID of the current item, if it doesn't exist set to empty string
var categoryId = current.categoryId || '';
// Find the cateogry that matches the category ID
var category = previous.find(createFilter(categoryId));
// If the items property doesn't exists (we don't have any items), create an empty array
if (!category.items) { category.items = []; }
// Add the item the category
category.items.push(current);
// Return the current value that will be used in the next iteration.
// Note, the initial value of previous will be the intial value of categories.
return previous;
}, categories);
console.log(mergedSet);
/* Output
[
{ id: 'cat1',
name: 'Category1',
items:
[ { id: '001',
name: 'item1',
description: 'description of item1',
categoryId: 'cat1' },
{ id: '003',
name: 'item3',
description: 'description of item3',
categoryId: 'cat1' }
]
},
{ id: 'cat2',
name: 'Category2',
items:
[ { id: '002',
name: 'item2',
description: 'description of item2',
categoryId: 'cat2'
}
]
},
{ id: '',
name: '',
items:
[ { id: '004',
name: 'item4',
description: 'description of item4' } ] }
]
*/
Assuming the variables categories and items are assigned as you defined above:
const keyedCategories = _(categories)
.concat({ id: '', name: '' })
.keyBy('id')
.value();
const groupedItems = _.groupBy(items, (item) => _.get(item, 'categoryId', ''));
const result = _.reduce(groupedItems, (acc, value, key) => {
const { id: categoryId, name } = keyedCategories[key];
return _.concat(acc, { categoryId, name, items: value });
}, []);

Categories