Reduce an array of elements into an object in MongoDB - mongodb

I have a MongoDB collection named Venue with elements of type:
{
venue: "Grand Hall",
sections: [{
name: "Lobby",
drinks: [{
name: "Vodka",
quantity: 3
}, {
name: "Red Wine",
quantity: 1
}]
}, {
name: "Ballroom",
drinks: [{
name: "Vodka",
quantity: 22
}, {
name: "Red Wine",
quantity: 50
}]
}]
}
I want to calculate the total amounts of each drink for the party. So I want my result to be something like that:
{
venue: "Grand Hall",
sections: 2,
drinks: [{
name: "Vodka",
quantity: 25
}, {
name: "Red Wine",
quantity: 51
}]
}

$unwind - Deconstruct sections array into multiple documents.
$unwind - Deconstruct sections.drinks array into multiple documents.
$group - Group by venue and sections.drinks.name. Perform sum for quantity.
$group - Group by venue. Perform count for grouped result in previous stage. And add the document into drinks array.
db.collection.aggregate([
{
$unwind: "$sections"
},
{
$unwind: "$sections.drinks"
},
{
$group: {
_id: {
venue: "$venue",
drink_name: "$sections.drinks.name"
},
quantity: {
$sum: "$sections.drinks.quantity"
}
}
},
{
$group: {
_id: "$_id.venue",
section: {
$sum: 1
},
drinks: {
$push: {
name: "$_id.drink_name",
quantity: "$quantity"
}
}
}
}
])
Demo # Mongo Playground

There are lots of ways to do this. Here's another one using "$reduce" and "$map", etc.
db.Venue.aggregate({
"$match": {
"venue": "Grand Hall"
}
},
{
"$set": {
"sections": {"$size": "$sections"},
"drinks": {
"$reduce": {
"input": { // flatten array of drink objects
"$reduce": {
"input": "$sections.drinks",
"initialValue": [],
"in": {"$concatArrays": ["$$value", "$$this"]}
}
},
"initialValue": [],
"in": {
"$let": {
"vars": { // position of drink in $$value, or -1 if not found
"idx": {"$indexOfArray": ["$$value.name", "$$this.name"]}
},
"in": {
"$cond": [
{"$eq": ["$$idx", -1]}, // not found?
{"$concatArrays": ["$$value", ["$$this"]]}, // not found, add object
{ // found, so update object in $$value by summing quantities
"$map": {
"input": "$$value",
"as": "val",
"in": {
"$cond": [
{"$eq": ["$$val.name", "$$this.name"]}, // right object?
{ // yes, update quantity
"name": "$$val.name",
"quantity": {"$sum": ["$$val.quantity", "$$this.quantity"]}
},
"$$val" // wrong object for update, so just keep it
]
}
}
}
]
}
}
}
}
}
}
})
Try it on mongoplayground.net.

Related

MongoDB aggregate $group $sum that matches date inside array of objects

I'll explain my problem here and i'll put a tldr at the bottom summarizing the question.
We have a collection called apple_receipt, since we have some apple purchases in our application. That document has some fields that we will be using on this aggregation. Those are: price, currency, startedAt and history. Price, currency and startedAt are self-explanatory. History is a field that is an array of objects containing a price and startedAt. So, what we are trying to accomplish is a query that gets every document between a date of our choice, for example: 06-06-2020 through 10-10-2022 and get the total price combined of all those receipts that have a startedAt between that. We have a document like this:
{
price: 12.9,
currency: 'BRL',
startedAt: 2022-08-10T16:23:42.000+00:00
history: [
{
price: 12.9,
startedAt: 2022-05-10T16:23:42.000+00:00
},
{
price: 12.9,
startedAt: 2022-06-10T16:23:42.000+00:00
},
{
price: 12.9,
startedAt: 2022-07-10T16:23:42.000+00:00
}
]
}
If we query between dates 06-06-2022 to 10-10-2022, we would have a return like this: totalPrice: 38,7.
-total price of the 3 objects that have matched the date inside that value range-
I have tried this so far:
AppleReceipt.aggregate([
{
$project: {
price: 1,
startedAt: 1,
currency: 1,
history: 1,
}
},
{
$unwind: {
path: "$history",
preserveNullAndEmptyArrays: true,
}
},
{
$match: {
$or: [
{ startedAt: {$gte: new Date(filters.begin), $lt: new Date(filters.end)} },
]
}
},
{
$group: {
_id: "$_id",
data: { $push: '$$ROOT' },
totalAmountHelper: { $sum: '$history.price' }
}
},
{
$unwind: "$data"
},
{
$addFields: {
totalAmount: { $add: ['$totalAmountHelper', '$data.price'] }
}
}
])
It does bring me the total value but I couldn't know how to take into consideration the date to make the match stage to only get the sum of the documents that are between that date.
tl;dr: Want to make a query that gets the total sum of the prices of all documents that have startedAt between the dates we choose. Needs to match the ones inside history field - which is an array of objects, and also the startedAt outside of the history field.
https://mongoplayground.net/p/lOvRbX24QI9
db.collection.aggregate([
{
$set: {
"history_total": {
"$reduce": {
"input": "$history",
"initialValue": 0,
"in": {
$sum: [
{
"$cond": {
"if": {
$and: [
{
$gte: [
new Date("2022-06-06"),
{
$dateFromString: {
dateString: "$$this.startedAt"
}
}
]
},
{
$lt: [
{
$dateFromString: {
dateString: "$$this.startedAt"
}
},
new Date("2022-10-10")
]
},
]
},
"then": "$$this.price",
"else": 0
}
},
"$$value",
]
}
}
}
}
},
{
$set: {
"history_total": {
"$sum": [
"$price",
"$history_total"
]
}
}
}
])
Result:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"currency": "BRL",
"history": [
{
"price": 12.9,
"startedAt": "2022-05-10T16:23:42.000+00:00"
},
{
"price": 12.9,
"startedAt": "2022-06-10T16:23:42.000+00:00"
},
{
"price": 12.9,
"startedAt": "2022-07-10T16:23:42.000+00:00"
}
],
"history_total": 325.79999999999995,
"price": 312.9,
"startedAt": "2022-08-10T16:23:42.000+00:00"
}
]
Kudos goes to #user20042973

MongoDB group by and SUM by array

I'm new in mongoDB.
This is one example of record from collection:
{
supplier: 1,
type: "sale",
items: [
{
"_id": ObjectId("60ee82dd2131c5032342070f"),
"itemBuySum": 10
},
{
"_id": ObjectId("60ee82dd2131c50323420710"),
"itemBuySum": 10,
},
{
"_id": ObjectId("60ee82dd2131c50323420713"),
"itemBuySum": 10
},
{
"_id": ObjectId("60ee82dd2131c50323420714"),
"itemBuySum": 20
}
]
}
I need to group by TYPE field and get the SUM. This is output I need:
{
supplier: 1,
sales: 90,
returns: 170
}
please check Mongo playground for better understand. Thank you!
$match - Filter documents.
$group - Group by type and add item into data array which leads to the result like:
[
[/* data 1 */],
[/* data 2 */]
]
$project - Decorate output document.
3.1. First $reduce is used to flatten the nested array to a single array (from Result (2)) via $concatArrays.
3.2. Second $reduce is used to aggregate $sum the itemBuySum.
db.collection.aggregate({
$match: {
supplier: 1
},
},
{
"$group": {
"_id": "$type",
"supplier": {
$first: "$supplier"
},
"data": {
"$push": "$items"
}
}
},
{
"$project": {
_id: 0,
"supplier": "$supplier",
"type": "$_id",
"returns": {
"$reduce": {
"input": {
"$reduce": {
input: "$data",
initialValue: [],
in: {
"$concatArrays": [
"$$value",
"$$this"
]
}
}
},
"initialValue": 0,
"in": {
$sum: [
"$$value",
"$$this.itemBuySum"
]
}
}
}
}
})
Sample Mongo Playground
db.collection.aggregate([
{
$match: {
supplier: 1
},
},
{
"$group": {
"_id": "$ID",
"supplier": {
"$first": "$supplier"
},
"sale": {
"$sum": {
"$cond": {
"if": {
"$eq": [
"$type",
"sale"
]
},
"then": {
"$sum": "$items.itemBuySum"
},
"else": {
"$sum": 0
}
}
}
},
"returns": {
"$sum": {
"$sum": {
"$cond": {
"if": {
"$eq": [
"$type",
"return"
]
},
"then": {
"$sum": "$items.itemBuySum"
},
"else": {
"$sum": 0
}
}
}
}
}
}
},
{
"$project": {
_id: 0,
supplier: 1,
sale: 1,
returns: 1
}
}
])

Mongoose Summing Up subdocument array elements having same alias

I have a document like this:
_id:'someId',
sales:
[
{
_id:'111',
alias:'xxx',
amount:500,
name: Apple, //items with same alias always have same name and quantity
quantity:2
},
{
_id:'222',
alias:'abc',
amount:100,
name: Orange,
quantity:14
},
{
_id:'333',
alias:'xxx',
amount:300,
name: Apple, //items with same alias always have same name and quantity
quantity:2
}
]
The alias field is here to 'group' items/documents whenever they appear to have same alias i.e to be 'embeded' as one with the amount summed up.
I need to display some sort of a report in such a way that those elements which have same alias they should be displayed as ONE and the others which doesn't share same alias to remain as they are.
Example, For the sample document above, I need an output like this
[
{
alias:'xxx',
amount:800
},
{
alias:'abc',
amount:100
}
]
WHAT I HAVE TRIED
MyShop.aggregate([
{$group:{
_id: "$_id",
sales:{$last :"$sales"}
},
{$project:{
"sales.amount":1
}}
}
])
This just displays as a 'list' regardless of the alias. How do I achieve summing up amount based on the alias?
You can achieve this using $group
db.collection.aggregate([
{
$unwind: "$sales"
},
{
$group: {
_id: {
_id: "$_id",
alias: "$sales.alias"
},
sales: {
$first: "$sales"
},
_idsInvolved: {
$push: "$sales._id"
},
amount: {
$sum: "$sales.amount"
}
}
},
{
$group: {
_id: "$_id._id",
sales: {
$push: {
$mergeObjects: [
"$sales",
{
alias: "$_id.alias",
amount: "$amount",
_idsInvolved: "$_idsInvolved"
}
]
}
}
}
}
])
Mongo Playground
You can use below aggregation
db.collection.aggregate([
{
"$addFields": {
"sales": {
"$map": {
"input": {
"$setUnion": [
"$sales.alias"
]
},
"as": "m",
"in": {
"$let": {
"vars": {
"a": {
"$filter": {
"input": "$sales",
"as": "d",
"cond": {
"$eq": [
"$$d.alias",
"$$m"
]
}
}
}
},
"in": {
"amount": {
"$sum": "$$a.amount"
},
"alias": "$$m",
"_idsInvolved": "$$a._id"
}
}
}
}
}
}
}
])
MongoPlayground

add new field in mongoDB projections

I have a mongo collection (store details) like below...
{
_id: "",
storeName: "store1",
items: [
{
itemName: "mongo",
itemPrice: 20,
itemAvailablity: 100
},
{
itemName: "apples",
itemPrice: 50,
itemAvailablity: 70
}
]
},
{
_id: "",
storeName: "store2",
items: [
{
itemName: "banana",
itemPrice: 10,
itemAvailablity: 30
},
{
itemName: "apple",
itemPrice: 45,
itemAvailablity: 90
}
]
},
{
_id: "",
storeName: "store3",
items: [
{
itemName: "apple",
itemPrice: 10,
itemAvailablity: 30
},
{
itemName: "mongo",
itemPrice: 30,
itemAvailablity: 50
}
]
}
from the above data, I want to get particular item details along with storeName.
If I want to get "mongo" details from all stores then my expected output will be like
[
{
itemName: "mongo",
itemPrice: 20,
itemAvailablity: 100,
storeName: "store1"
},
{
itemName: "mongo",
itemPrice: 30,
itemAvailablity: 50,
storeName: "store3"
}
]
I try with different mongo aggregation queries but I didn't get the output as I expect
can anyone help me out of this
thank you
You can achieve this via this aggregation:
db.collection.aggregate([
{
$project: {
storeName: "$$CURRENT.storeName",
items: {
$filter: {
input: "$items",
as: "item",
cond: { $eq: ["$$item.itemName","mongo"] }
}
}
}
},
{ $unwind: "$items" },
{ $addFields: { "items.storeName": "$storeName"} },
{ $replaceRoot: { newRoot: "$items" }}
])
You can see it working here
You can use below aggregation
db.collection.aggregate([
{ "$match": { "items.itemName": "mongo" }},
{ "$unwind": "$items" },
{ "$match": { "items.itemName": "mongo" }},
{ "$addFields": { "items.storeName": "$storeName" }},
{ "$replaceRoot": { "newRoot": "$items" }}
])
MongoPlayground
Or either you can do this way
db.collection.aggregate([
{ "$match": { "items.itemName": "mongo" }},
{ "$addFields": {
"items": {
"$map": {
"input": {
"$filter": {
"input": "$items",
"as": "item",
"cond": { "$eq": ["$$item.itemName", "mongo"]}
}
},
"as": "item",
"in": { "$mergeObjects": ["$$item", { "storeName": "$storeName" }] }
}
}
}},
{ "$unwind": "$items" },
{ "$replaceRoot": { "newRoot": "$items" }}
])
MongoPlayground
db.collection.aggregate(
// Pipeline
[
// Stage 1
{
$match: {
items: {
$elemMatch: {
"itemName": "mongo"
}
}
}
},
// Stage 2
{
$project: {
items: {
$filter: {
input: "$items",
as: "item",
cond: {
$eq: ["$$item.itemName", 'mongo']
}
}
},
storeName: 1
}
},
// Stage 3
{
$addFields: {
"items.storeName": '$storeName'
}
},
]
);

mongodb query nested array with date field

this is my document .
"calendar": {
"_id": "5cd26a886458720f7a66a3b8",
"hotel": "5cd02fe495be1a4f48150447",
"calendar": [
{
"_id": "5cd26a886458720f7a66a413",
"date": "1970-01-01T00:00:00.001Z",
"rooms": [
{
"_id": "5cd26a886458720f7a66a415",
"room": "5cd17d82ca56fe43e24ae5d3",
"price": 10,
"remaining": 8,
"reserved": 0
},
{
"_id": "5cd26a886458720f7a66a414",
"room": "5cd17db6ca56fe43e24ae5d4",
"price": 12,
"remaining": 8,
"reserved": 0
},
{
"_id": "5cd26a886458720f7a66a34",
"room": "5cd17db6ca45fe43e24ae5e7",
"price": 0,
"remaining": 0,
"reserved": 0
}
]
},
}
and this is my shema:
const calendarSchema = mongoose.Schema({
hotel: {
type: mongoose.Schema.ObjectId,
ref: "Hotel",
required: true
},
city: {
type: mongoose.Schema.ObjectId,
ref: "City"
},
calendar: [
{
date: Date,
rooms: [
{
room: {
type: mongoose.Schema.ObjectId,
ref: "Room",
required: true
},
price: {
type: Number
},
remaining: {
type: Number
},
reserved: {
type: Number
}
}
]
}
]
});
First of all, as you can see my calendar stores hotelId and CityId and included another calendar that contains some objects. There is nothing fancy here. The query has two conditions as below:
1.Our specific filter is located whole dates between startDate and endDate
2.Mentioned filter only shows the room's prices and remaining ( Not included zero num ).
And after injecting this conditions, query must return only the rooms that are matched with my filter.
I tried some query but the outcome is not my result .
db.calendars.find({
'calendar': {
'$elemMatch': {
date: {
'$lt': ISODate("2019-05-09T09:37:24.005Z"),
'$lt': ISODate("2019-06-05T09:37:24.005Z")
},
"rooms.$.price": { '$gt': 0 },
"rooms.$.remaining": { '$gt': 0 }
}
}
})
Unfortunately this is not THAT easy as you describe, this cannot be done with just a find assuming you want to project ONLY (and all) the rooms that match.
However with an aggregate this is possible, it would look like this:
db.calendars.aggregate([
{
$project:
{
"rooms": {
$filter: {
input: {
"$map": {
"input": "$calendar",
"as": "cal",
"in": {
"$cond": [
{
$and: [{$gt: ["$$cal.date", ISODate("2019-05-09T09:37:24.005Z")]},
{$lt: ["$$cal.date", ISODate("2019-06-05T09:37:24.005Z")]},]
},
{
"rooms": {
"$filter": {
"input": "$$cal.rooms",
"as": "room",
"cond": {
$and: [{"$gt": ["$$room.price", 0]},
{"$gt": ["$$room.remaining", 0]}]
}
}
},
date: "$$cal.date"
},
null
]
}
},
},
as: 'final',
cond: {$size: {$ifNull: ["$$final.rooms", []]}}
}
},
}
},
{
$match: {
"rooms.0": {$exists: true}
}
}
])