Format date within $project in aggregate function nodejs - node.js

I am looking to format a date within aggregate $project pipeline in 12 hour format.
$project: {
UpdateDt:
{
$dateToString: { format: "%m/%d/%Y ,%H:%M:%S", date: "$UpdateDt", timezone: "GMT" }
},
}
I tried using above code but this does not seem to work,also I want the date format as :
8/31/2017, 10:30:00 AM GMT
With the code above ,I get the output as 09/14/2017,15:07:10 the requirement is for the date to be in 12 hour format with GMT appended,same as achieved with .toLocaleString() function.
Please suggest any way/workaround for same.

Demo - https://mongoplayground.net/p/fIlimwXuOWK
In 1st pipeline - $addFields add a new field UpdateNewDt with {date, hour, time} details in GMT.
2nd pipeline - $set UpdateNewDt.hour convert to 12 hour clock and add AM and PM eg:- 12 - 12 PM, 02- 02 AM, 23- 11 PM
3rd pipeline - $set combine data back to a string
4th pipeline - $project - UpdateDt
db.collection.aggregate([
{
"$addFields": {
UpdateNewDt: {
date: { $dateToString: { format: "%m/%d/%Y ,", date: "$UpdateDt", timezone: "GMT" }},
hour: { $dateToString: { format: "%H", date: "$UpdateDt", timezone: "GMT" }},
time: { $dateToString: { format: ":%M:%S", date: "$UpdateDt", timezone: "GMT" }}
}
}
},
{
$set: {
"UpdateNewDt.hour": {
$cond: {
"if": { $gt: [ { "$toInt": "$UpdateNewDt.hour" }, 11 ] },
"then": {
"$concat": [
{
$cond: [
{ $eq: [{ "$toInt": "$UpdateNewDt.hour"},12 ] },
"12",
{ $toString: {"$subtract": [ { "$toInt": "$UpdateNewDt.hour" }, 12 ] }}
]
},
" PM" ]
},
"else": { "$concat": [ "$UpdateNewDt.hour", " AM" ] }
}
}
}
},
{
$set: {
"UpdateNewDt": {
"$concat": [
"$UpdateNewDt.date",
{ "$arrayElemAt": [{ "$split": [ "$UpdateNewDt.hour", " " ] }, 0 ] },
"$UpdateNewDt.time",
" ",
{ "$arrayElemAt": [{ "$split": [ "$UpdateNewDt.hour", " " ] }, 1 ] }
]
}
}
},
{
$project: { UpdateDt: "$UpdateNewDt" }
}
])
Input -
[
{
"key": 1,
UpdateDt: ISODate("2021-04-06T02:07:47.231Z")
},
{
"key": 2,
UpdateDt: ISODate("2021-04-06T22:07:47.231Z")
},
{
"key": 3,
UpdateDt: ISODate("2021-04-06T12:07:47.231Z")
}
]
Output -
[
{
"UpdateDt": "04/06/2021 ,02:07:47 AM",
"_id": ObjectId("5a934e000102030405000000")
},
{
"UpdateDt": "04/06/2021 ,10:07:47 PM",
"_id": ObjectId("5a934e000102030405000001")
},
{
"UpdateDt": "04/06/2021 ,12:07:47 PM",
"_id": ObjectId("5a934e000102030405000002")
}
]

Hi As I understood your concern you can use below script
Collection
"users": [
{
"_id": 3,
"ts": ISODate("2016-06-18T18:30:00.288Z")
}
]
Query
db.users.aggregate([
{
$project: {
"date": {
"$dateToString": {
"format": "%m/%d/%Y ,%H:%M:%S",
"date": "$ts",
"timezone": "GMT"
}
}
}
}
])
Output
[
{
"_id": 3,
"date": "06/18/2016 ,18:30:00"
}
]

I think that is not supported.
Here is the official Docs for $dateToString: https://docs.mongodb.com/manual/reference/operator/aggregation/dateToString/
For the specified available formats, there is only 24-hour clock.
If it isn't necessary for you to format that in the aggregate step, I would suggest you date-fns package. You can import only the format function and you can format the Date in format that you need.
Here is the package: https://www.npmjs.com/package/date-fns
Here is the Docs: https://date-fns.org/v2.19.0/docs/format

Related

Fetching last 30 days of Mongo records, summing by date, and filling in missing dates in range

I have a Mongo database filled with "Events" records, that look like this:
{
timestamp: 2022-03-15T22:11:34.711Z,
_id: new ObjectId("62310f16b0d71321e887a905")
}
Using a NodeJs server, I need to fetch the last 30 days of Events, grouped/summed by date, and any dates within that 30 days with no records need to be filled with 0.
Using this code I can get the correct events, grouped/summed by date:
Event.aggregate( [
{
$match: {
timestamp: {
$gte: start,
$lte: end,
}
}
},
{
$project: {
date: {
$dateToParts: { date: "$timestamp" }
},
}
},
{
$group: {
_id: {
date: {
year: "$date.year",
month: "$date.month",
day: "$date.day"
}
},
"count": { "$sum": 1 }
}
}
] )
This will return something like this:
[
{
"_id": {
"date": {
"year": 2022,
"month": 3,
"day": 14
}
},
"count": 3
},
{
"_id": {
"date": {
"year": 2022,
"month": 3,
"day": 15
}
},
"count": 8
},
]
I also have this Javascript code to generate the last 30 days of dates:
const getDateRange = (start, end) => {
const arr = [];
for(let dt = new Date(start); dt <= end; dt.setDate(dt.getDate() + 1)){
arr.push(new Date(dt));
}
return arr;
};
const subtractDays = (date, days) => {
return new Date(date.getTime() - (days * 24 * 60 * 60 * 1000));
}
const end = new Date();
const start = subtractDays(end, 30);
const range = getDateRange(start, end);
Which returns something like this:
[
2022-03-09T01:13:10.769Z,
2022-03-10T01:13:10.769Z,
2022-03-11T01:13:10.769Z,
2022-03-12T01:13:10.769Z,
2022-03-13T01:13:10.769Z,
...
]
It seems like I have all the pieces, but I'm having trouble putting all this together to do what I need in an efficient way. Any push in the right direction would be appreciated.
Whenever one has to work with date/time arithmetic then I recommend a library like moment.js
const end = moment().startOf('day').toDate();
const start = moment().startOf('day').subtract(30, 'day').toDate();
In MongoDB version 5.0 you can use $dateTrunc(), which is shorter than $dateToParts and { year: "$date.year", month: "$date.month", day: "$date.day" }
You need to put all data in an array ({$group: {_id: null, data: { $push: "$$ROOT" }}) and then at missing elements with $ifNull:
event.aggregate([
{
$match: {
timestamp: { $gte: start, $lte: end }
}
},
{
$group: {
_id: { $dateTrunc: { date: "$timestamp", unit: "day" } },
count: { $sum: 1 }
}
},
{ $project: {timestamp: "$_id", count: 1, _id: 0} },
{
$group: {
_id: null,
data: { $push: "$$ROOT" }
}
},
{
$set: {
data: {
$map: {
input: { $range: [0, 30] },
as: "i",
in: {
$let: {
vars: {
day: { $dateAdd: { startDate: start, amount: "day", unit: "$$i" } }
},
in: {
$ifNull: [
{
$first: {
$filter: {
input: "$data",
cond: { $eq: ["$$this.timestamp", "$$day"] }
}
}
},
{ timestamp: "$$day", count: 0 }
]
}
}
}
}
}
}
},
{ $unwind: "$data" }
])
$range operator supports only integer values, that's the reason for using $let. Otherwise, if you prefer to use the external generated range, it would be
{
$set: {
data: {
$map: {
input: range,
as: "day",
in: {
$ifNull: [
{
$first: {
$filter: {
input: "$data",
cond: { $eq: ["$$this.timestamp", "$$day"] }
}
}
},
{ timestamp: "$$day", count: 0 }
]
}
}
}
}
}
And for MongoDB version 5.1 you may have a look at $densify
Use aggregation stage densify if you're using MongoDB version 5.1 or later. But for lower version, below query can be used.
db.collection.aggregate([
{
$match: {
timestamp: {
$gte: {
"$date": "2022-03-01T00:00:00.000Z"
},
$lte: {
"$date": "2022-03-31T23:59:59.999Z"
},
}
}
},
{
$project: {
date: {
$dateToParts: {
date: "$timestamp"
}
},
}
},
{
$group: {
_id: {
date: {
year: "$date.year",
month: "$date.month",
day: "$date.day"
}
},
"count": {
"$sum": 1
}
}
},
{
"$group": {
"_id": null,
"originData": {
"$push": "$$ROOT"
}
}
},
{
"$project": {
"_id": 0,
"data": {
"$concatArrays": [
{
"$map": {
"input": {
"$range": [
0,
30,
1
]
},
"in": {
"$let": {
"vars": {
"date": {
"$add": [
{
"$date": "2022-03-01T00:00:00.000Z"
},
{
"$multiply": [
"$$this",
86400000
]
}
]
}
},
"in": {
"_id": {
"date": {
"day": {
"$dayOfMonth": "$$date"
},
"month": {
"$month": "$$date"
},
"year": {
"$year": "$$date"
}
}
},
"count": 0
}
}
}
}
},
"$originData"
]
}
}
},
{
"$unwind": "$data"
},
{
$group: {
_id: {
date: {
year: "$data._id.date.year",
month: "$data._id.date.month",
day: "$data._id.date.day"
}
},
"count": {
"$sum": "$data.count"
}
}
},
{
"$sort": {
"_id.date.year": 1,
"_id.date.month": 1,
"_id.date.day": 1
}
}
])
Link to online playground. https://mongoplayground.net/p/5I0I04HoHXm

How to get the Sum from independent two subdocument Arrays in mongoose

I have this schema
var salesExpenseSchema = new Schema({
date : {
month: Number
},
sales: [{amount : Schema.Types.Decimal128}],
expenses: [{amount : Schema.Types.Decimal128}]
});
Example of a database record is like this
{
_id:'5dbac5dfa1488240cbc4f838',
date:{month:11},
sales:[{amount:3000},{amount:5000}],
expenses: [{amount:5000},{amount:500}]
},
{
_id:'5dbac5dfa1488240cbc4f838',
date:{month:10},
sales:[{amount:2000},{amount:5000}],
expenses: [{amount:500},{amount:800}]
},
{
_id:'5dbac5dfa1488240cbc4f838',
date:{month:09},
sales:[{amount:2000},{amount:4000}],
expenses: [{amount:200},{amount:300}]
}
Now I want to get the Summation of sales and expenses.
I have used Aggregate with $unwind for both sales and expenses like this below:
SalesExpense.aggregate([
{$unwind: "$sales"},
{$unwind: "$expenses"},
{$group:{
_id:'$_id',
sales:{$sum: "$sales.sellPrices"},
expenses:{$sum: "$expenses.amount"},
}
},
But the problem is... If one array document has data and the other has no data, then it gives 0 ie, the real summation isn't obtained. This is to say, if there are sales but no expenses then their sum becomes 0, and vice-versa.
I want to get the summation for both sales and expenses regardless of one of them not having data. How do I achieve this?
EDIT:
I have edited the question and added the date object in my schema and in the database records: I want to make this summation based on each month, that is to say... each month to have its own sales and expenses... Sort of a timeline with each month having its own sales and expenses.
I have tried using $group before $project
{$group:{
_id:'$date.month'}}
But it seems not to give the expected results.
I want an output like this one:
[
{
"month": "11",
"sales": {
"$numberDecimal": "8000"
},
"expenses": {
"$numberDecimal": "5500"
}
},
{
"month": "10",
"sales": {
"$numberDecimal": "7000"
},
"expenses": {
"$numberDecimal": "1100"
}
},
{
"month": "09",
"sales": {
"$numberDecimal": "6000"
},
"expenses": {
"$numberDecimal": "500"
}
},
]
How can I achieve this?
You can group by month and get the totals like this:
db.collection.aggregate([
{
$group: {
_id: "$date.month",
"sales": {
"$sum": {
"$sum": "$sales.amount"
}
},
"expenses": {
"$sum": {
"$sum": "$expenses.amount"
}
}
}
}
])
Sample Data:
[
{
_id: "5dbac5dfa1488240cbc4f838",
date: {
month: 11
},
sales: [
{
amount: 1
},
{
amount: 2
}
],
expenses: []
},
{
_id: "5dbac5dfa1488240cbc4f839",
date: {
month: 11
},
sales: [
{
amount: 5
},
{
amount: 6
}
],
expenses: [
{
amount: 7
},
{
amount: 8
}
]
},
{
_id: "5dbac5dfa1488240cbc4f840",
date: {
month: 12
},
sales: [],
expenses: [
{
amount: 7
},
{
amount: 8
}
]
}
]
Result:
[
{
"_id": 12,
"expenses": 15,
"sales": 0
},
{
"_id": 11,
"expenses": 15,
"sales": 14
}
]
Playground:
https://mongoplayground.net/p/K9ofoZx5ORI

Aggregate by Day produces duplicate day results

My mongodb aggregation query should return daily, weekly, monthly, yearly data within specific date ranges.
Example: let's assume dates between 1st Feb to 1st March.
the data for daily should be: 1st, 2nd, 3rd..
Weekly period will be: 1st Feb, 8th Feb, 15th Feb, 22nd Feb ..
monthly period will be 1st Feb, 1st march ..
Look at example below:
Let's say my API accepts: startDate, endDate, interval as body params.
req.body will be something like this:
{
startDate: "",
endDate: "",
platform: "",
interval: "daily" // could be "weekly", "monthly", "yearly"
}
These params will be passed to my model where I have some aggregation code which will be mentioned below:
MessagesSchema.statics.totalMessages = ( startDate, endDate, platform, interval ) => {
return Messages.aggregate([{
$match: {
platform: platform,
timestamp: {
$gte: new Date(startDate),
$lte: new Date(endDate)
}
}
},
{
$project: {
timestamp: {
$dateToString: {
format: '%Y-%m-%d',
date: '$timestamp'
}
}
}
},
{
$group: {
_id: {
timestamp: '$timestamp'
},
count: {
$sum: 1
}
}
},
{
$sort: {
'_id.timestamp': 1
}
}
]).exec();
Let's assume Weekly data from 1st Feb 2019 - 1st March 2019;
expected result:
[
{
"_id": {
"timestamp": "2019-02-01"
},
"count": 2
},
{
"_id": {
"timestamp": "2019-02-08"
},
"count": 2
},
{
"_id": {
"timestamp": "2019-02-15"
},
"count": 2
}
]
actual result:
[
{
"_id": {
"timestamp": "2019-02-01"
},
"count": 2
},
{
"_id": {
"timestamp": "2019-03-02"
},
"count": 2
},
{
"_id": {
"timestamp": "2019-03-02"
},
"count": 2
}
]

Mongo aggregate - Split interval value into months affected

I have a tricky aggregation of data in mongo and I have no idea how to achieve it directly in mongo without no later data processing.
Here is an simplified example of documents in my collection
[
{
"from" : ISODate("2017-01-15T00:00:00.000Z"),
"to" : ISODate("2017-02-15T00:00:00.000Z"),
"value" : 1000
},
{
"from" : ISODate("2017-02-01T00:00:00.000Z"),
"to" : ISODate("2017-02-28T00:00:00.000Z"),
"value" : 2000
},
{
"from" : ISODate("2017-02-20T00:00:00.000Z"),
"to" : ISODate("2017-03-14T00:00:00.000Z"),
"value" : 1000
}
]
No I would like to get monthly sum of values belonging to a specific month.
[
{janurary: 500}, /* 1/2 of interval id 1 is January so take half the value */
{february: 2833}, /* 500 + 2000 + 333 */
{march: 666}, /* 2/3 of interval id 3 is March */
]
Calculation has to be precise so I can't simplify things by saying all months have exactly 30 days. But what I can do is provide this information from code for each month of the interval. So it should be possible to provide this query information january2017 = 31 days, february2017 = 28 days, march2017 = 31 days
I know I could do this in my node.js code but there might be A LOT of documents in that DB so I would rather not fetch all of these to server to perform the calculation.
Pah, I hope somebody else comes up with a nicer answer but here is one way of getting there:
db.collection.aggregate({
$addFields: {
dayFrom: { $dayOfMonth: "$from" },
dayTo: { $dayOfMonth: "$to" },
monthFrom: { $month: "$from" },
monthTo: { $month: "$to" },
numberOfDays: { $subtract: [ { $dayOfMonth: "$to" }, { $dayOfMonth: "$from" } ] },
numberOfMonths: { $subtract: [ { $month: "$to" }, { $month: "$from" } ] },
}
}, {
$addFields: {
numberOfDaysInFromMonth: { $dayOfMonth: { $subtract: [ { $dateFromParts : { year: { $year: "$from" }, month: { $add: [ "$monthFrom", 1 ] }, day: 1 } }, 1 ] } },
}
}, {
$addFields: {
numberOfDaysAccountingForFromMonth: { $subtract: [ { $add: [ "$numberOfDaysInFromMonth", 1 ] }, "$dayFrom" ] },
numberOfDaysAccountingForToMonth: { $subtract: [ "$dayTo", 1 ] }, // assuming the "to" day does not count anymore
}
}, {
$addFields: {
totalNumberOfDays: { $add: [ "$numberOfDaysAccountingForFromMonth", "$numberOfDaysAccountingForToMonth" ] }
}
}, {
$addFields: {
percentageAccountingForFromMonth: { $divide: [ "$numberOfDaysAccountingForFromMonth", "$totalNumberOfDays" ] },
percentageAccountingForToMonth: { $divide: [ "$numberOfDaysAccountingForToMonth", "$totalNumberOfDays" ] },
}
}, {
$facet: {
"from": [{
$group: {
_id: "$monthFrom",
sum: { $sum: { $multiply: [ "$value", "$percentageAccountingForFromMonth" ] } }
}
}],
"to": [{
$group: {
_id: "$monthTo",
sum: { $sum: { $multiply: [ "$value", "$percentageAccountingForToMonth" ] } }
}
}]
}
}, {
$project: {
total: { $concatArrays: [ "$from", "$to" ] }
}
}, {
$unwind: "$total"
}, {
$group: {
_id: "$total._id",
sum: { $sum: "$total.sum" }
}
})
Some remarks:
You will need to refine that to match your precise definition of
what forms part of a date range and how to count the number of days
("is 2018-01-30 to 2018-01-31 one day or is it two days?").
You might be able to beautify that query using $let and
some nesting. I thought it would be easier to use subsequent $addFields stages to make the beast easier to follow through.
The code does not support from and to values that touch more than two months (e.g. 2018-01-01 to 2018-03-01).

Getting error like "date is not defind" using nodejs with mogodb? datetime store as a timestamp in db

i am getting data using nodejs with MongoDB. I have objects in the MongoDB. and now I am going for the getting data using date wise and my datetime filed is a timestamp. and I want to get data from start date to end date using MongoDB.
must imp note i want to print date in the my expected op.
here I this is my objects =>
{
"_id" : ObjectId("595be16ee04602135828e25c"),
"Action" : "Comment",
"datetime" : 1507099928000 // 4th oct 2017 convert date just for info here write
},
{
"_id" : ObjectId("595be16ee04602135828e25c"),
"Action" : "Comment",
"datetime" : 1508139441716 // 16th oct 2017 convert date just for info here write
}
{
"_id" : ObjectId("595be16ee04602135828e25c"),
"Action" : "Comment",
"datetime" : 1508139441716 // 16th oct 2017 convert date just for info here write
}
{
"_id" : ObjectId("595be16ee04602135828e25c"),
"Action" : "Like",
"datetime" : 1508139441716 // 16th oct 2017 convert date just for info here write
},
this is my query =>
InstaAc.aggregate([
{
"$match": {
"_id": ObjectId("595be16ee04602135828e25c"),
"datetime": {
"$lte": 1508141028150, "$gte": 1507622568000
}
},
"Action": {
$in: ["Comment", "Like"]
}
},
{
"$addFields": {
"datetime": {
"$add": [new Date(0), "$datetime"]
}
}
},
{
"$group": {
"_id": {
"$dateToString": {
"format": "%d-%m-%Y",
"date": "datetime"
}
},
"commentcount": { $sum: { $cond: [{ $eq: ["Action", "Comment"] }, 1, 0] } },
"likecount": { $sum: { $cond: [{ $eq: ["Action", "Like"] }, 1, 0] } },
}
},
{
"$sort": {
"_id": 1
}
}
]).exec(function (err, data) {
if (err) {
console.log(err);
}
else {
console.log(data);
}
})
this is my query above and I am getting an error like this "date is not defined"
please, anyone, know how can fix this then please help me.
my excepted o/p =>
{ _id: '16-10-2017', commentcount: 2 ,likecount:1},
Before you run your query create a date object and use that object instead of new Date(0).
var myDate = new Date(0);
And in your query
"$addFields": {
"datetime": {
"$add": [myDate, "$datetime"]
}

Resources