Compare two arrays, get out the duplicates - javascript

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);

Related

How do I create an array of objects with a nested array based on a similar key?

I have an array that looks something like this
const example = [
{ id: '1', name: 'Person 1', organization: { id: '11', name: 'Organization A' } },
{ id: '2', name: 'Person 2', organization: { id: '12', name: 'Organization A' } },
{ id: '3', name: 'Person 3', organization: { id: '13', name: 'Organization B' } },
];
As you can see, the organization name is something I want to key off of and create a data structure like this:
const output = [
// data.value will be their ID
{
organizationName: 'Organization A',
data: [
{ label: 'Person 1', value: '1' },
{ label: 'Person 2', value: '2' },
],
},
{
organizationName: 'Organization B',
data: [
{ label: 'Person 3', value: '3' },
],
},
]
What I've tried
I know I want to use reduce for something like this, but I feel like I'm off:
const providerOptions = externalPeople.data.reduce((acc, currentValue) => {
const {
organization: { name: organizationName },
} = currentValue;
if (organizationName) {
acc.push({ organization: organizationName, data: [] });
} else {
const { name: externalPersonName, id } = currentValue;
acc[acc.length - 1].data.push({ name: externalPersonName, value: id });
}
return acc;
}, [] as any);
However the output comes out to something like this:
[
{organizationName: 'Organization A', data: []},
{organizationName: 'Organization A', data: []},
{organizationName: 'Organization B', data: []},
];
data doesn't seem to get anything pushed inside the array in this reduce function, and the organization name get duplicated... what am I doing wrong?
Easiest way is to use an Map/Set/or object to keep track of orgs you create. This way you are not searching in the array to see if the organization was found already. After you are done, you can create the array you want from the object.
const externalPeople = {
data : [
{ id: '1', name: 'Person 1', organization: { id: '11', name: 'Organization A' } },
{ id: '2', name: 'Person 2', organization: { id: '12', name: 'Organization A' } },
{ id: '3', name: 'Person 3', organization: { id: '13', name: 'Organization B' } },
],
};
const providerOptions = Object.values(externalPeople.data.reduce((acc, currentValue) => {
const {
organization: { name: organizationName },
name: externalPersonName,
id
} = currentValue;
// Is the org new? Yes, create an entry for it
if (!acc[organizationName]) {
acc[organizationName] = { organization: organizationName, data: [] };
}
// push the person to the organization
acc[organizationName].data.push({ name: externalPersonName, value: id });
return acc;
}, {}));
console.log(providerOptions)
Here is another solution
const example = [
{ id: '1', name: 'Person 1', organization: { id: '11', name: 'Organization A' } },
{ id: '2', name: 'Person 2', organization: { id: '12', name: 'Organization A' } },
{ id: '3', name: 'Person 3', organization: { id: '13', name: 'Organization B' } },
];
const result = example.reduce((res, entry) => {
const recordIndex = res.findIndex(rec => rec.organizationName === entry.organization.name);
if(recordIndex >= 0) {
res[recordIndex].data.push({ label: entry.name, value: entry.id});
} else {
const record = {
organizationName: entry.organization.name,
data: [{ label: entry.name, value: entry.id }]
};
res.push(record);
}
return res;
}, []);
console.log(result);
You are not checking if the value is already present in your accumulation acc
You can check it with a simple find in the if statement since it's an array
const providerOptions = externalPeople.data.reduce((acc, currentValue) => {
const {
organization: { name: organizationName },
} = currentValue;
//Check if organization is not present already
if (!acc.find(a => a.organization === organizationName)) {
//Add also the data of the element your are processing
acc.push({ organization: organizationName, data: [{label: currentValue.name, value: currentValue.id}] });
} else {
const { name: externalPersonName, id } = currentValue;
acc[acc.length - 1].data.push({ label: externalPersonName, value: id });
}
return acc;
}, [] as any);
I also added the data of the first element of the group you create when adding the organization.
The result should be as your expected output:
[
{
organization: 'Organization A',
data: [
{ label: 'Person 1', value: '1' },
{ label: 'Person 2', value: '2' }
]
},
{
organization: 'Organization B',
data: [
{ label: 'Person 3', value: '3' }
]
}
]
Hope it helps!
Compare this solution (using Lodash) with other solutions. Which one emphasises your intentions at most? This is why we use Lodash in our company - to maintain code as declarative as we can, because code readability, with minimum cognitive overload, is most important goal during coding.
const persons = [
{ id: '1', name: 'Person 1', organization: { id: '11', name: 'Organization A' } },
{ id: '2', name: 'Person 2', organization: { id: '12', name: 'Organization A' } },
{ id: '3', name: 'Person 3', organization: { id: '13', name: 'Organization B' } },
];
const personsByOrganizations = _.groupBy(persons, 'organization.name')
const output = _.map(personsByOrganizations, (persons, organizationName) => ({
organizationName,
data: _.map(persons, ({ name, id }) => ({
label: name,
value: id
}))
}))
Something like that with using a Set?
result = [...new Set(example.map(d => d.organization.name))].map(label => {
return {
organizationName: label,
data: example.filter(d => d.organization.name === label).map(d => {
return {label: d.name, value: d.id}
})
}
})
`

get total price from array of objects per specific user using .reduce in javascript

I have this array of objects
const items = [
{
id: '121',
itemDate: '2022-04-28',
itemName: 'testname1',
itemCategory: 'Category A',
itemPrice: { price: '100', currency: 'GBP' },
createdBy: {
username: 'user1',
name: 'Name 1',
date: '2022-04-28T22:41:59',
},
},
{
id: '122',
itemDate: '2022-04-28',
itemName: 'testname2',
itemCategory: 'Category B',
itemPrice: { price: '100', currency: 'GBP' },
createdBy: {
username: 'user2',
name: 'Name 2',
date: '2022-04-28T22:42:44',
},
},
{
id: '122',
itemDate: '2022-04-28',
itemName: 'testname3',
itemCategory: 'Category C',
itemPrice: { price: '200', currency: 'GBP' },
createdBy: {
username: 'user2',
name: 'Name 2',
date: '2022-04-28T22:43:16',
},
},
]
Code I'm using:
items.reduce(function (c, x) {
if (!c[x.createdBy.username])
c[x.createdBy.username] = {
username: x.createdBy.username,
total: 0,
}
c[x.createdBy.username].total += Number(x.itemPrice.price)
return c
}, [])
This part gives me the following output:
items :>> [
user1: { username: 'user1', total: 100},
user2: { username: 'user2', total: 300}
]
So I tried this to get rid of the object names:
let output = []
let totalSum = 0
for (const username in items) {
let temp = {
username: items[username].username,
total: items[username].total,
}
totalSum = totalSum + items[username].total
output.push(temp)
}
output.push({ username: 'allUsers', total: totalSum })
return output
And final output is as I want it now:
output :>> [
{ username: 'user1', total: 100 },
{ username: 'user2', total: 300 },
{ username: 'allUsers', total: 400}
]
My two questions...
Is there a way to update the .reduce part so that I'd get an object without the name at the beggining, without having to use the for loop?
Is there also a way to implement the part that would sum up all the totals?
Thank you
Code Sample (without comments/description)
const groupAndAdd = arr => (
Object.values(
arr.reduce(
(acc, {createdBy : {username}, itemPrice: {price}}) => {
acc.allUsers ??= { username: 'allUsers', total: 0};
acc.allUsers.total += +price;
if (username in acc) {
acc[username].total += +price;
} else {
acc[username] = {username, total: +price};
}
return acc;
},
{}
)
)
);
Presented below is a working demo to achieve the desired objective, with notes/comments to help understand.
Code Snippet
// method to group by user and sum prices
const groupAndAdd = arr => (
// extract the values from the intermediate result-object
Object.values(
arr.reduce( // generate result as object
(acc, {createdBy : {username}, itemPrice: {price}}) => {
// above line uses de-structuring to directly access username, price
// below uses logical nullish assignment to set-up "allUsers"
acc.allUsers ??= { username: 'allUsers', total: 0};
// accumulate the "price" to the all-users "total"
acc.allUsers.total += +price;
// if "acc" (accumulator) has "username", simply add price to total
if (username in acc) {
acc[username].total += +price;
} else {
// create an object for the "username" with initial total as "price"
acc[username] = {username, total: +price};
}
// always return the "acc" accumulator for ".reduce()"
return acc;
},
{} // initially set the "acc" to empty object
)
) // if required, use ".sort()" to move the all-users to last position in array
);
const items = [{
id: '121',
itemDate: '2022-04-28',
itemName: 'testname1',
itemCategory: 'Category A',
itemPrice: {
price: '100',
currency: 'GBP'
},
createdBy: {
username: 'user1',
name: 'Name 1',
date: '2022-04-28T22:41:59',
},
},
{
id: '122',
itemDate: '2022-04-28',
itemName: 'testname2',
itemCategory: 'Category B',
itemPrice: {
price: '100',
currency: 'GBP'
},
createdBy: {
username: 'user2',
name: 'Name 2',
date: '2022-04-28T22:42:44',
},
},
{
id: '122',
itemDate: '2022-04-28',
itemName: 'testname3',
itemCategory: 'Category C',
itemPrice: {
price: '200',
currency: 'GBP'
},
createdBy: {
username: 'user2',
name: 'Name 2',
date: '2022-04-28T22:43:16',
},
},
];
console.log('group and add prices per user: ', groupAndAdd(items));
.as-console-wrapper { max-height: 100% !important; top: 0 }
Explanation
Inline comments added to the snippet above.
PS: If you'd like to add value to stackoverflow community,
Please consider reading: What to do when my question is answered
Thank you !
For your first question, you're initialising correctly as an array, but you're using just object. Two ways you can do this.
First Option
let something = items.reduce(function(c, x) {
if (!c[x.createdBy.username])
c[x.createdBy.username] = {
username: x.createdBy.username,
total: 0,
}
c[x.createdBy.username].total += Number(x.itemPrice.price)
return c
}, {});
something = Object.values(something);
Second Option
I was thinking of using just push, but seems it's not possible, so the above is the only option.
Using push is possible, but it'll get too complicated by checking with find and updating the correct array element.
For your second question of summing up all the totals, you can use the simple syntax of:
const sum = arr.reduce((a, c) => a + c, 0);
This is the minimum code you need for array of numbers to be summed.

Javascript -How remove item from nested object?

I'm trying to remove an item from a nested object. object named "categories" that contains several categories and each category has several businesses. something like bellow object:
let categories = [{
name: 'Home',
biz: [{
name: 'Business 1',
id: 50
}, {
name: 'Business 2',
id: 52
}, {
name: 'Business n',
id: 53
}]
}, {
name: 'Car',
biz: [{
name: 'Business 1',
id: 62
}, {
name: 'Business 2',
id: 66
}, {
name: 'Business n',
id: 67
}]
}];
What I'm trying to do is removing one of this businesses that selected by user and return the whole object without mutating original state.
so far I did something like bellow and it's working fine bu I'm not sure if I'm doing this the right way or the wrong way. I appreciate if you guys help me by review or refactor this code:
categories.map((cat, inedx) => {
return { ...cat, biz: [...cat.biz.filter(bz => bz.id!== 66)]}
});
reduce to the rescue :-)
const fn = (categories, id) => {
return categories.reduce((r, x) => {
return r.concat({ ...x, biz: x.biz.filter(x => x.id !== id) });
}, []);
}
console.log(fn(categories, 66));
let categories = [{
name: 'Home',
biz: [{
name: 'Business 1',
id: 50
}, {
name: 'Business 2',
id: 52
}, {
name: 'Business n',
id: 53
}]
}, {
name: 'Car',
biz: [{
name: 'Business 1',
id: 62
}, {
name: 'Business 2',
id: 66
}, {
name: 'Business n',
id: 67
}]
}];
categories.forEach(el => el.biz = el.biz.filter(e => e.id !== 66));
console.log("removed biz 66", categories)

Map list of objects with sub array of objects

I have this data structure that i want to map in an es6 one-liner fashion:
const vehicles = [
{
id: 'vehicle1',
items: [
{
id: 'contract1'
name: 'Contract 1',
},
],
},
{
id: 'vehicle1',
items: [
{
id: 'contract2'
name: 'Contract 2',
},
],
},
{
id: 'vehicle2',
items: [
{
id: 'contract3'
name: 'Contract 3',
},
],
},
{
id: 'vehicle2',
items: [
{
id: 'contract4'
name: 'Contract 4',
},
],
},
]
I would like to collect this in a list like this:
const result = [
{
id: 'vehicle1',
items: [
{
id: 'contract1'
name: 'Contract 1',
},
{
id: 'contract2'
name: 'Contract 2',
},
],
},
{
id: 'vehicle2',
items: [
{
id: 'contract3'
name: 'Contract 3',
},
{
id: 'contract4'
name: 'Contract 4',
},
],
},
]
So the vehicles in list is unique and items is in one list.
I tried this but it only collects vehicles in list:
const res = vehicles.reduce((acc, vehicle) => acc.set(vehicle.id, vehicle), new Map())
How can I do this the 'ES6 way'?
Map would be not a good choice for this type of result, Map used mostly when you have to modify and get same structure. You can use reduce for this.
var data = [{
id: 'vehicle1',
items: [{
id: 'contract1',
name: 'Contract 1'
}]
},
{
id: 'vehicle1',
items: [{
id: 'contract2',
name: 'Contract 2'
}]
},
{
id: 'vehicle2',
items: [{
id: 'contract3',
name: 'Contract 3'
}]
},
{
id: 'vehicle2',
items: [{
id: 'contract4',
name: 'Contract 4'
}]
}
];
var result = {};
data.forEach(val => {
if (result[val.id])
result[val.id].items = result[val.id].items.concat(val.items);
else
result[val.id] = val
});
result = Object.values(result);
console.log(result);
You were on the right path. Here it is:
const res = vehicles.reduce((m,v)=>m.set(v.id, [...v.items, ...(m.get(v.id)||[])]), new Map)
This use array destructuring to concat items.
You can use Array.prototype.reduce to aggregate the input by id and Object.keys to get the output in the desired format
const vehicles=[{id:'vehicle1',items:[{id:'contract1',name:'Contract 1'}]},{id:'vehicle1',items:[{id:'contract2',name:'Contract 2'}]},{id:'vehicle2',items:[{id:'contract3',name:'Contract 3'}]},{id:'vehicle2',items:[{id:'contract4',name:'Contract 4'}]}];
const grouped = vehicles.reduce((all, {id, items}) => {
if (!all.hasOwnProperty(id)) all[id] = { id, items: [] };
all[id].items.push(...items);
return all;
}, {});
const result = Object.keys(grouped).map(k => grouped[k]);
console.log(result);
Not a one-liner but it returns desired result and uses ES6 Map.
const data = [{"id":"vehicle1","items":[{"id":"contract1","name":"Contract 1"}]},{"id":"vehicle1","items":[{"id":"contract2","name":"Contract 2"}]},{"id":"vehicle2","items":[{"id":"contract3","name":"Contract 3"}]},{"id":"vehicle2","items":[{"id":"contract4","name":"Contract 4"}]}]
const res = data.reduce((acc, {id, items}) => {
if(!acc.get(id)) acc.set(id, {id, items});
else acc.get(id).items.push(...items);
return acc;
}, new Map())
console.log([...res.values()])
Well, its not a one liner but it can be...if you delete all the line breaks :D
const convert = () => {
const vMap = vehicles.reduce((acc, vehicle) => {
if (acc[vehicle.id]) {
acc[vehicle.id].items.push(...vehicle.items);
} else {
acc[vehicle.id] = vehicle;
}
return acc;
}, {});
return Object.keys(vMap).map(k => vMap[k]);
};
convert();
Nearly, you could get the grouped items in a map and map the map with the wanted id and itmes property.
const
vehicles = [{ id: 'vehicle1', items: [{ id: 'contract1', name: 'Contract 1', }] }, { id: 'vehicle1', items: [{ id: 'contract2', name: 'Contract 2', }] }, { id: 'vehicle2', items: [{ id: 'contract3', name: 'Contract 3', }] }, { id: 'vehicle2', items: [{ id: 'contract4', name: 'Contract 4' }] }],
result = Array.from(
vehicles.reduce((acc, { id, items }) =>
acc.set(id, (acc.get(id) || []).concat(items)), new Map()),
([id, items]) => ({ id, items })
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

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

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, ' '));

Categories