mongodb get sum of value within time period - node.js

I am trying to find some records that has created within specific time period.
Then I want to calculate the sum of value of these record.
{"member_id":"3755","value":184607,"create_time":"2017-8-11 10:36:58"
{"member_id":"3234","value":74582,"create_time":"2017-8-11 10:36:58",
{"member_id":"4857","value":36776,"create_time":"2017-8-11 10:36:58",
{"member_id":"2042","value":15753,"create_time":"2017-8-11 10:36:58",
{"member_id":"1374","value":655103,"create_time":"2017-8-11 10:36:59"
{"member_id":"3777","value":595437,"create_time":"2017-8-11 10:36:59"
{"member_id":"5271","value":306364,"create_time":"2017-8-11 10:36:59"
{"member_id":"2143","value":164831,"create_time":"2017-8-11 10:36:59"
{"member_id":"1374","value":655103,"create_time":"2017-8-11 10:36:59"
{"member_id":"3777","value":595437,"create_time":"2017-8-11 10:36:59"
{"member_id":"5271","value":306364,"create_time":"2017-8-12 10:36:59"
{"member_id":"2143","value":164831,"create_time":"2017-8-12 11:28:59"
{"member_id":"3777","value":595437,"create_time":"2017-8-12 14:46:59"
{"member_id":"5271","value":306364,"create_time":"2017-8-13 11:36:59"
{"member_id":"2143","value":164831,"create_time":"2017-8-13 13:36:59"
...
Here are the code for getting the sum of value, how can I get the sum of value between 2017-8-11 10:36:00 and 2017-8-12 14:00:00
connection.aggregate([{
$match: match
},
{
$group: {
_id: null,
total: {
$sum: "$value"
}
}
}
], function(err, result) {
if (err) throw (err);
result = {
member_id: member_id,
total: result[0].total,
}
cb(result);
});

Don't store date as string. Store it with date format and Try below query
db.connection.aggregate([{
"$match" : { "create_time" : { "$gt" : new ISODate("2017-08-11T10:36:00.000Z"), "$lt" : new ISODate("2017-08-12T14:00:00.000Z") }},
"$group" : { "_id": "$member_id", "total": { "$sum": 1 }}
}])

let todayDate = new Date();
let beforeDate = new Date();
beforeDate.setDate(beforeDate.getDate() - 15); // 15 is days
db.collections.aggregate([
{
"$match":
{
"insertDate":
{
"$lte": todayDate,
"$gte": beforeDate
}
}
}
])
.exec()
.then((result) => {
//result
})
.catch((err) => {
// error
});

Related

check an array of string value with array of object in mongodb

I have array of strings like this
let fromHour = ['2.5','3','3.5']
let toHour = ['2.5','3','3.5']
I have an array of object saved in mongoDB
timeRange = [
{
from:'2.5',
to:'3'
},
{
from:'3',
to:'3.5'
}
]
I want to check if any of my array of string value exist in that object value
I have tried this but it give me this error ( Unrecognized expression '$match' )
checkAppoint = await Appointment.aggregate([
{
$project: {
date: myScheduleFinal[k].date,
status: { $in: ['pending', 'on-going'] },
timeRange: {
'$match': {
'from': { $in: fromHolder },
'to': { $in: toHolder },
},
},
},
},
]);
also I have tried this solution and it work for me but it take to much time so I am trying this with aggregate
checkAppoint = await Appointment.findOne({
date: myScheduleFinal[k].date,
status: { $in: ['pending', 'on-going'] },
timeRange:{$elemMatch:{
from:{$in:fromHolder},
to:{$in:toHolder}
}}
});
So anyone have a solution for that
Just try $elemMatch and $in operators,
using find() method
checkAppoint = await Appointment.find({
timeRange: {
$elemMatch: {
from: { $in: fromHour },
to: { $in: toHour }
}
}
})
Playground
using aggregate() method
checkAppoint = await Appointment.aggregate([
{
$match: {
timeRange: {
$elemMatch: {
from: { $in: fromHour },
to: { $in: toHour }
}
}
}
}
])
Playground
So I have found a way around to solve this problem and I will share the solution I used
First I want to minimize my request to mongodb so I am now making just one request that bring all the appointment with the required date
and I want to make it this way because my fromHour and toHour array will change many time through single request
helperArray => contains all the day I want to check it's range
let checkAppoint = await Appointment.find({
date: { $in: helperArray },
status: { $in: ['pending', 'on-going'] },
});
now inside my for loop I will go through that data
checkAppoint.filter((singleAppoint) => {
if (singleAppoint._doc.date === myScheduleFinal[k].date) {
singleAppoint._doc.timeRange.map((singleTime) => {
if (fromHolder.includes(singleTime.from)) {
busy = true;
}
});
}
});

Why does the mongodb aggregate $avg code return all of the documents ungrouped?

The same mongodb aggregate $avg code that returns only 1 row (expected) directly in mongodb, returns all of the documents (completely ungrouped) and without the $avg value also when run inside node.js. It is basically as I simply did a find on the entire collection and returned all of the documents with all of the columns. Why is the grouping ignored?
I ran the code directly inside mongodb and there there is only one element returned inside the array. This is what is expected as I wish to get the average price for all the documents inside the collection, hence the 'null' _id value.
I tried this with $sum too, but no difference. It is as if the code simply does not see the group section at all, but why does the exact same code work inside mongodb and not inside Node.js?
async function findHistoricalStakingAverage(days) {
let allAverage = await dailyDB.collection('price').aggregate(
{
$match: {
date: {
$lt: new Date(),
$gte: new Date(new Date().setDate(new Date().getDate() - days))
}
}
},
{
$group: {
_id: null,
avg: { $avg: "$staking_rate" }
}
}
).toArray();
return allAverage;
}
Below is the same command run directly in mongo with the expected output of only one item, the average value for all the documents:
> db.price.aggregate(
... {
... $match: {
... date: {
... $lt: new Date(),
... $gte: new Date(new Date().setDate(new Date().getDate() - 30))
... }
... }
... },
... {
... $group: {
... _id: null,
... avg: { $avg: "$staking_rate" }
... }
... }
... ).toArray()
[ { "_id" : null, "avg" : 13.848065601345667 } ]
>
In the code, You need to wrap aggregation pipeline stages inside [], otherwise it seems to not consider all stages of it :
async function findHistoricalStakingAverage(days) {
let allAverage = await dailyDB.collection('price').aggregate([
{
$match: {
date: {
$lt: new Date(),
$gte: new Date(new Date().setDate(new Date().getDate() - days))
}
}
},
{
$group: {
_id: null,
avg: { $avg: "$staking_rate" }
}
}
]).toArray();
return allAverage;
}

Mongoose Filter based on Dynamic Date Key with value

I have created an employee attendance application where attendances are logged and stored in a database. I have tried to obtain a count of all date-field with the value of "Present". Data are stored in the database like so :
"attendances": { <YYYY-MM-DD>: "value" } pair
// The values being "Absent" or "Present" whatever the case may be.
The problem is, I get a value of 0 whenever I try to count all the entries with "attendances": {"2019-08-28": "Present"}.
Can anyone help me find out what am doing wrong?
//Schema
const Schema = mongoose.Schema;
const employeeSchema = new Schema({
name: String,
department: String,
origin: String,
employDate: String,
attendances: Object
});
module.exports= Employee = mongoose.model('Employee', employeeSchema);
route.js
router.get('/',(req,res)=>{
Employee.collection.countDocuments({"attendances.date":"present"},(err,data)=>{
if(err){
res.status(500)
res.send(err)
}else{
res.status(200)
res.json(data)
}
})
})
//Data stored in MongoDB
{
"_id": "5d6565236574b1162c349d8f",
"name": "Benjamin Hall",
"department": "IT",
"origin": "Texas",
"employDate": "2019-08-27",
"__v": 0,
"attendances": {
"2019-08-28": "Sick"
}
},
{
"_id": "5d6367ee5b78d30c74be90e6",
"name": "Joshua Jaccob",
"department": "Marketing",
"origin": "new york",
"employDate": "2019-08-26",
"__v": 0,
"attendances": {
"2019-08-26": "Present",
"2019-08-27": "Sick"
}
},
If you want to find by property in embedded document you have to use dot notation
this will not work, because you are asking mongoo to find the document which have attendances object equal the same given object.
{ "attendances": {"2019-08-26": "Present"}}
this will work only if attendances object in your database contains only
{ "attendances": {"2019-08-26": "Present"}}
that's mean that you asking mongoo if the stored object is equal the given object and it will return false
{ "attendances": {"2019-08-26": "Present" , "2019-08-27": "Sick"}} == { "attendances": {"2019-08-26": "Present"}}
to do this you have to use dot notation
Employee.collection.countDocuments({"attendances.2019-08-26":"Present"},(err,data)=>{
if(err){
res.status(500)
res.send(err)
}else{
res.status(200)
res.json(data)
}
})
Since the dynamic date is part of an embedded document, to query on that field with a regex expression (for case insensitive search) you essentially need to use the dot notation { "attendance.2019-08-28": /present/i }, constructed using computed property names as:
const date = "2019-08-28" // dynamic date
const query = {
["attendances." + date]: /present/i // computed property name
}
Employee.countDocuments(query, (err, data) => {
if (err){
res.status(500).send(err)
} else{
res.status(200).json(data)
}
})
Note, countDocuments() function can be accessed directly on the Mongoose model.
For a date range query, say for example you want to return the count of attendances that were present for the last 30 days, you would need
to query with the aggregation framework which exposes operators like $objectToArray, $filter and $size to give you the count.
The above operators allow you to convert the attendances document into an array of key value pairs with $objectToArray which you can then filter based on the past 30 days criteria as well as the "present" value using $filter. To get the count, use the $size operator on the filtered array.
As an illustration, applying $objectToArray on the document
{
"2019-08-26": "Present",
"2019-08-27": "Sick"
}
returns
[
{ "k": "2019-08-26", "v": "Present" },
{ "k": "2019-08-27", "v": "Sick" }
]
To filter on the past n days you will need to first create a list of dates in that range i.e.
[
"2019-08-27",
"2019-08-26",
"2019-08-25",
...
]
which can be done in JavaScript as
function formatDate(date) {
var d = new Date(date),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
year = d.getFullYear();
if (month.length < 2) month = '0' + month;
if (day.length < 2) day = '0' + day;
return [year, month, day].join('-');
}
const listDatesForThePastDays = n => (
Array(n)
.fill(new Date())
.map((today, i) => today - 8.64e7 * i)
.map(formatDate)
)
This list can be used in the $filter as
{ "$filter": {
"input": { "$objectToArray": "$attendances" },
"cond": {
"$and": [
{ "$in": ["$$this.k", listDatesForThePastDays(30)] },
{ "$eq": ["$$this.v", "Present"] }
]
}
} }
And apply the $size operator to get the count:
{ "$size": {
"$filter": {
"input": { "$objectToArray": "$attendances" },
"cond": {
"$and": [
{ "$in": ["$$this.k", listDatesForThePastDays(30)] },
{ "$eq": ["$$this.v", "Present"] }
]
}
}
} }
Your overall query will look like
function formatDate(date) {
var d = new Date(date),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
year = d.getFullYear();
if (month.length < 2) month = '0' + month;
if (day.length < 2) day = '0' + day;
return [year, month, day].join('-');
}
const listDatesForThePastDays = n => (
Array(n)
.fill(new Date())
.map((today, i) => today - 8.64e7 * i)
.map(formatDate)
)
Employee.aggregate([
{ "$addFields": {
"numberofPresentAttendances": {
"$size": {
"$filter": {
"input": { "$objectToArray": "$attendances" },
"cond": {
"$and": [
{ "$in": ["$$this.k", listDatesForThePastDays(30)] },
{ "$eq": ["$$this.v", "Present"] }
]
}
}
}
}
} }
]).exec().
.then(results => {
console.log(results);
// results will be an array of employee documents with an extra field numberofPresentAttendances
})
.catch(console.error)
To get the count for all employees then you need to group all the documents as
Employee.aggregate([
{ "$group": {
"_id": null,
"totalPresent": {
"$sum": {
"$size": {
"$filter": {
"input": { "$objectToArray": "$attendances" },
"cond": {
"$and": [
{ "$in": ["$$this.k", listDatesForThePastDays(30)] },
{ "$eq": ["$$this.v", "Present"] }
]
}
}
}
}
}
} }
]).exec()
.then(results => {
console.log(results);
// results will be an array of employee documents with an extra field numberofPresentAttendances
})
.catch(console.error)

MongoDB: Set field value to 10 if less than 10, otherwise increment by one

Consider the following code:
findAndModify({id: id}, undefined, {$inc: {counter: 1}, {$max: {counter: 10})
This fails with an error because both $inc and $max try to update the same field.
But what if I want to set the counter to 10 if its value is less than 10 and if not, increment its value by 1? How do I do that in one atomic operation?
Is there a way to apply the updates sequentially in a single operation?
I don't think that can be done in a single operation. However, you can create an aggregate query with the conditional statements that you then use in your update.
(async () => {
try {
const results = await Model.aggregate([
{ '$match': { 'id': id } },
{ '$project': {
'counter': {
'$cond': [
{ '$lt': ['$counter', 10 ] },
10,
{ '$add': ['$counter', 1 ] }
]
}
} }
]).exec();
const data = results[0];
const { counter } = data;
const doc = await Model.findOneAndUpdate({ id },
{ '$set': { counter } },
{ new: true }
).exec();
console.log(doc);
}
catch (err) {
console.error(err);
}
})()
For an atomic update, include a query that restricts the increment for documents that only have counter less than the given ceiling value of 10:
findAndModify(
{
'id': id,
'counter': { '$lt': 10 }
}, undefined,
{ '$inc': { 'counter': 1 } },
callback
)

merge aggregation and find results in MongoDB

I have this model:
const recordSchema = new Schema({
user: {
type: Schema.ObjectId,
ref: 'Person',
required: true
},
date: {
type: Date,
default: Date.now()
},
time: {
type: Date,
default: Date.now()
}
});
So, when I make a HTTP Get request I receive an array of records:
[{/*record1*/}, {/*record2*/}, ...]
The point is that I'm using aggregation to get the number of records of each user (I got that cover), but I would like merge this with the find results to receive something like this:
{
"records": [{/*record1*/}, {/*record2*/}, ...],
"stats": [
{
"_id" : ObjectId("5b6393f2a1d3de31d9547f63"),
"count" : 3.0
},
{
"_id" : ObjectId("5b5d22d8b6195d1b6a5d2574"),
"count" : 17.0
}
]
}
So, how do I get this?
Note: I'm using this for data for some charts, should I handle this on node or on the front-end?
This can be done in the back-end and if you're using MongoDB Server version 3.4.4 or greater in your backend, $facet aggregate pipeline should cover your needs.
Consider the following example which runs two aggregate queries in the same aggregate pipeline using $facet: one returns the records for today and the other returns the counts for each user in the collection:
let start = new Date();
start.setHours(0,0,0,0);
let end = new Date();
end.setHours(23,59,59,999);
Record.aggregate([
{ '$facet': {
'records': [
{ '$match': {
'date': {
'$gte': start,
'$lte': end
}
} }
],
'stats': [
{ '$group': {
'_id': '$user',
'count': { '$sum': 1 }
} }
]
} }
]).exec((err, results) => {
if (err) {
console.error(err);
throw new Error(err);
}
const data = results[0];
console.log(JSON.stringify(data, null, 4));
})
For MongoDB 3.2 and below
1. Using Promises
const recordsQuery = Record.find({ 'date': {
'$gte': start, // date object representing start of today
'$lte': end // date object representing end of today
} }).lean().exec();
const statsQuery = Record.aggregate([
{ '$group': {
'_id': '$user',
'count': { '$sum': 1 }
} }
]).exec();
Promise.all([ recordsQuery, statsQuery ]).then(([ recordsData, statsData ]) => {
const records = recordsData[0];
const stats = statsData[0];
const data = { records, stats };
console.log(JSON.stringify(data, null, 4));
}).catch(err => console.error(err));
2. Using async/await
(async () => {
try {
const recordsQuery = await Record.find({ 'date': {
'$gte': start, // date object representing start of today
'$lte': end // date object representing end of today
} }).lean().exec();
const statsQuery = await Record.aggregate([
{ '$group': {
'_id': '$user',
'count': { '$sum': 1 }
} }
]).exec();
const records = recordsQuery[0];
const stats = statsQuery[0];
const data = { records, stats };
console.log(JSON.stringify(data, null, 4));
} catch (err) {
console.error(err);
}
})();
You can use the $lookup operator to link the original records for the users by Id
{
$lookup:
{
from: <collection to join>,
localField: <field from the input documents>,
foreignField: <field from the documents of the "from" collection>,
as: <output array field>
}
}

Resources