Calculate date difference in year, month, day - mongodb

I have the following query:
db.getCollection('user').aggregate([
{$unwind: "$education"},
{$project: {
duration: {"$divide":[{$subtract: ['$education.to', '$education.from'] }, 1000 * 60 * 60 * 24 * 365]}
}},
{$group: {
_id: '$_id',
"duration": {$sum: '$duration'}
}}]
])
Above query result is:
{
"_id" : ObjectId("59fabb20d7905ef056f55ac1"),
"duration" : 2.34794520547945
}
/* 2 */
{
"_id" : ObjectId("59fab630203f02f035301fc3"),
"duration" : 2.51232876712329
}
But what I want to do is get its duration in year+ month + day format, something like: 2 y, 3 m, 20 d.
One another point, if a course is going on the to field is null, and another field isGoingOn: true, so here I should calculate the duration by using current date instead of to field.
And user has array of course subdocuments
education: [
{
"courseName": "Java",
"from" : ISODate("2010-12-08T00:00:00.000Z"),
"to" : ISODate("2011-05-31T00:00:00.000Z"),
"isGoingOn": false
},
{
"courseName": "PHP",
"from" : ISODate("2013-12-08T00:00:00.000Z"),
"to" : ISODate("2015-05-31T00:00:00.000Z"),
"isGoingOn": false
},
{
"courseName": "Mysql",
"from" : ISODate("2017-02-08T00:00:00.000Z"),
"to" : null,
"isGoingOn": true
}
]
One another point is this: that date may be not continuous in one subdocument to the other subdocument. A user may have a course for 1 year, and then after two years, he/she started his/her next course for 1 year, and 3 months (it means this user has a total of 2 years and 3-month course duration).
What I want is get date difference of each subdocument in educations array, and sum those. Suppose in my sample data Java course duration is 6 month, and 22 days, PHP course duration is 1 year, and 6 months, and 22 days, and the last one is from 8 Feb 2017 till now, and it's going on, so my education duration is the sum of these intervals.

Please try this aggregation to get date difference in days,months and years, added multiple $addFields stage compute and reduce differences to date, month range without underflow, and the assumption here is 1 month = 30 days
pipeline
db.edu.aggregate(
[
{
$addFields : {
trainingPeriod : {
$map : {
input : "$education",
as : "t",
in : {
year: {$subtract: [{$year : {$ifNull : ["$$t.to", new Date()]}}, {$year : "$$t.from"}]},
month: {$subtract: [{$month : {$ifNull : ["$$t.to", new Date()]}}, {$month : "$$t.from"}]},
dayOfMonth: {$subtract: [{$dayOfMonth : {$ifNull : ["$$t.to", new Date()]}}, {$dayOfMonth : "$$t.from"}]}
}
}
}
}
},
{
$addFields : {
trainingPeriod : {
$map : {
input : "$trainingPeriod",
as : "d",
in : {
year: "$$d.year",
month: {$cond : [{$lt : ["$$d.dayOfMonth", 0]}, {$subtract : ["$$d.month", 1]}, "$$d.month" ]},
day: {$cond : [{$lt : ["$$d.dayOfMonth", 0]}, {$add : [30, "$$d.dayOfMonth"]}, "$$d.dayOfMonth" ]}
}
}
}
}
},
{
$addFields : {
trainingPeriod : {
$map : {
input : "$trainingPeriod",
as : "d",
in : {
year: {$cond : [{$lt : ["$$d.month", 0]}, {$subtract : ["$$d.year", 1]}, "$$d.year" ]},
month: {$cond : [{$lt : ["$$d.month", 0]}, {$add : [12, "$$d.month"]}, "$$d.month" ]},
day: "$$d.day"
}
}
}
}
},
{
$addFields : {
total : {
$reduce : {
input : "$trainingPeriod",
initialValue : {year : 0, month : 0, day : 0},
in : {
year: {$add : ["$$this.year", "$$value.year"]},
month: {$add : ["$$this.month", "$$value.month"]},
day: {$add : ["$$this.day", "$$value.day"]}
}
}
}
}
},
{
$addFields : {
total : {
year : "$total.year",
month : {$add : ["$total.month", {$floor : {$divide : ["$total.day", 30]}}]},
day : {$mod : ["$total.day", 30]}
}
}
},
{
$addFields : {
total : {
year : {$add : ["$total.year", {$floor : {$divide : ["$total.month", 12]}}]},
month : {$mod : ["$total.month", 12]},
day : "$total.day"
}
}
}
]
).pretty()
result
{
"_id" : ObjectId("5a895d4721cbd77dfe857f95"),
"education" : [
{
"courseName" : "Java",
"from" : ISODate("2010-12-08T00:00:00Z"),
"to" : ISODate("2011-05-31T00:00:00Z"),
"isGoingOn" : false
},
{
"courseName" : "PHP",
"from" : ISODate("2013-12-08T00:00:00Z"),
"to" : ISODate("2015-05-31T00:00:00Z"),
"isGoingOn" : false
},
{
"courseName" : "Mysql",
"from" : ISODate("2017-02-08T00:00:00Z"),
"to" : null,
"isGoingOn" : true
}
],
"trainingPeriod" : [
{
"year" : 0,
"month" : 5,
"day" : 23
},
{
"year" : 1,
"month" : 5,
"day" : 23
},
{
"year" : 1,
"month" : 0,
"day" : 10
}
],
"total" : {
"year" : 2,
"month" : 11,
"day" : 26
}
}
>

Well you could just simply use the existing date aggregation operators as opposed to using math to convert to "days" as you presently have:
db.getCollection('user').aggregate([
{ "$unwind": "$education" },
{ "$group": {
"_id": "$_id",
"years": {
"$sum": {
"$subtract": [
{ "$subtract": [
{ "$year": { "$ifNull": [ "$education.to", new Date() ] } },
{ "$year": "$education.from" }
]},
{ "$cond": {
"if": {
"$gt": [
{ "$month": { "$ifNull": [ "$education.to", new Date() ] } },
{ "$month": "$education.from" }
]
},
"then": 0,
"else": 1
}}
]
}
},
"months": {
"$sum": {
"$add": [
{ "$subtract": [
{ "$month": { "$ifNull": [ "$education.to", new Date() ] } },
{ "$month": "$education.from" }
]},
{ "$cond": {
"if": {
"$gt": [
{ "$month": { "$ifNull": ["$education.to", new Date() ] } },
{ "$month": "$education.from" }
]
},
"then": 0,
"else": 12
}}
]
}
},
"days": {
"$sum": {
"$add": [
{ "$subtract": [
{ "$dayOfYear": { "$ifNull": [ "$education.to", new Date() ] } },
{ "$dayOfYear": "$education.from" }
]},
{ "$cond": {
"if": {
"$gt": [
{ "$month": { "$ifNull": [ "$education.to", new Date() ] } },
{ "$month": "$education.from" }
]
},
"then": 0,
"else": 365
}}
]
}
}
}},
{ "$project": {
"years": {
"$add": [
"$years",
{ "$add": [
{ "$floor": { "$divide": [ "$months", 12 ] } },
{ "$floor": { "$divide": [ "$days", 365 ] } }
]}
]
},
"months": {
"$mod": [
{ "$add": [
"$months",
{ "$floor": {
"$multiply": [
{ "$divide": [ "$days", 365 ] },
12
]
}}
]},
12
]
},
"days": { "$mod": [ "$days", 365 ] }
}}
])
It is "sort of" an approximation on the "days" and "months" without the necessary operations to be "certain" of leap years, but it would get you the result which should be "near enough" for most purposes.
You can even do this without $unwind as long as your MongoDB version is 3.2 or greater:
db.getCollection('user').aggregate([
{ "$addFields": {
"duration": {
"$let": {
"vars": {
"edu": {
"$map": {
"input": "$education",
"as": "e",
"in": {
"$let": {
"vars": { "toDate": { "$ifNull": ["$$e.to", new Date()] } },
"in": {
"years": {
"$subtract": [
{ "$subtract": [
{ "$year": "$$toDate" },
{ "$year": "$$e.from" }
]},
{ "$cond": {
"if": { "$gt": [{ "$month": "$$toDate" },{ "$month": "$$e.from" }] },
"then": 0,
"else": 1
}}
]
},
"months": {
"$add": [
{ "$subtract": [
{ "$ifNull": [{ "$month": "$$toDate" }, new Date() ] },
{ "$month": "$$e.from" }
]},
{ "$cond": {
"if": { "$gt": [{ "$month": "$$toDate" },{ "$month": "$$e.from" }] },
"then": 0,
"else": 12
}}
]
},
"days": {
"$add": [
{ "$subtract": [
{ "$ifNull": [{ "$dayOfYear": "$$toDate" }, new Date() ] },
{ "$dayOfYear": "$$e.from" }
]},
{ "$cond": {
"if": { "$gt": [{ "$month": "$$toDate" },{ "$month": "$$e.from" }] },
"then": 0,
"else": 365
}}
]
}
}
}
}
}
}
},
"in": {
"$let": {
"vars": {
"years": { "$sum": "$$edu.years" },
"months": { "$sum": "$$edu.months" },
"days": { "$sum": "$$edu.days" }
},
"in": {
"years": {
"$add": [
"$$years",
{ "$add": [
{ "$floor": { "$divide": [ "$$months", 12 ] } },
{ "$floor": { "$divide": [ "$$days", 365 ] } }
]}
]
},
"months": {
"$mod": [
{ "$add": [
"$$months",
{ "$floor": {
"$multiply": [
{ "$divide": [ "$$days", 365 ] },
12
]
}}
]},
12
]
},
"days": { "$mod": [ "$$days", 365 ] }
}
}
}
}
}
}}
])
This is because from MongoDB 3.4 you can use $sum directly with an array of or any list of expressions in stages like $addFields or $project, and the $map can apply those same "date aggregation operator" expressions against each array element in place of doing $unwind first.
So the main math can really be done in one part of "reducing" the array, and then each total can be adjusted by the general "divisors" for the years, and the "modulo" or "remainder" from any overruns in the months and days.
Essentially returns:
{
"_id" : ObjectId("5a07688e98e4471d8aa87940"),
"education" : [
{
"courseName" : "Java",
"from" : ISODate("2010-12-08T00:00:00.000Z"),
"to" : ISODate("2011-05-31T00:00:00.000Z"),
"isGoingOn" : false
},
{
"courseName" : "PHP",
"from" : ISODate("2013-12-08T00:00:00.000Z"),
"to" : ISODate("2015-05-31T00:00:00.000Z"),
"isGoingOn" : false
},
{
"courseName" : "Mysql",
"from" : ISODate("2017-02-08T00:00:00.000Z"),
"to" : null,
"isGoingOn" : true
}
],
"duration" : {
"years" : 3.0,
"months" : 3.0,
"days" : 259.0
}
}
Given the 11th of November 2017

You can simplify your code by using client side processing with moment js library.
All the date time math is handled by moment js library. Use duration to calculate the reduced time diff
Use reduce to add the time diff across all the array elements followed by moment duration to output the time in years/months/days.
It solves two issues :
Gives you accurate difference in years month and days between two dates.
Gives you expected format.
For example:
var education = [
{
"courseName": "Java",
"from" : new Date("2010-12-08T00:00:00.000Z"),
"to" : new Date("2011-05-31T00:00:00.000Z"),
"isGoingOn": false
},
{
"courseName": "PHP",
"from" : new Date("2013-12-08T00:00:00.000Z"),
"to" : new Date("2015-05-31T00:00:00.000Z"),
"isGoingOn": false
},
{
"courseName": "Mysql",
"from" : new Date("2017-02-08T00:00:00.000Z"),
"to" : null,
"isGoingOn": true
}
];
var reducedDiff = education.reduce(function(prevVal, elem) {
if(elem.isGoingOn) elem.to = new Date();
var diffDuration = moment(elem.to).diff(moment(elem.from));
return prevVal + diffDuration;
}, 0);
var duration = moment.duration(reducedDiff);
alert(duration.years() +" y, " + duration.months() + " m, " + duration.days() + " d " );
var durationstr = duration.years() +" y, " + duration.months() + " m, " + duration.days() + " d ";
MongoDb integration:
var reducedDiff = db.getCollection('user').find({},{education:1}).reduce(function(...

Related

How to add two collections with single aggregation

I am new to MongoDb and would appreciate some help with this query. I wrote the following aggregation pipeline. I wrote the query from collection1 I got the output ("Conventional Energy" : 0.0036) and I wrote the query collection2 I got the output (LastMonthConsumption" : 2.08) but how to add two collection with single aggregation with(LastMonthConsumption" : 2.08 * Conventional Energy" : 0.0036/Conventional Energy" : 0.0036) this is my required output
I have this data in mongodb:
COLLECTION 1:DATA
{
"slcId" : "51",
"clientId" : "1",
"dcuId" : "1",
"type" : "L",
"officeId" : "200-24",
"lampStatus" : "OFF",
"cummulativeKWH" : 133.7,
"powerFactor" : 1.0,
"createDate" : ISODate("2018-09-06T00:01:34.816Z")
},
{
"slcId" : "52",
"clientId" : "1",
"dcuId" : "1",
"type" : "L",
"officeId" : "200-24",
"lampStatus" : "OFF",
"cummulativeKWH" : 133.7,
"powerFactor" : 1.0,
"createDate" : ISODate("2018-09-07T21:01:34.816Z")
}
COLLECTION2:DATA
{
"_class" : "MongoStreetLightMonthlyVo",
"timeId" : ISODate("2018-08-04T16:40:08.817Z"),
"vendor" : "CIMCON",
"slcId" : "123450",
"mongoStreetLightChildVo" : {
"totalConsumptionMtd" : 2.08,
"prevConsumptionMtd" : 3.45,
"perChargeKWH" : 9.85,
}
},
{
"_class" : "MongoStreetLightMonthlyVo",
"timeId" : ISODate("2018-09-04T16:40:08.817Z"),
"vendor" : "CIMCON",
"slcId" : "123450",
"mongoStreetLightChildVo" : {
"totalConsumptionMtd" : 2.08,
"prevConsumptionMtd" : 3.45,
"perChargeKWH" : 9.85,
}
}
Collection1:
db.collection1.aggregate([
{ $match:{"type" : "L"}},
{
$count: "TOTAL_Lights"
},
{ "$project": {
"Conventional Energy": {
"$divide": [
{ "$multiply": [
{ "$multiply": [ "$TOTAL_Lights" ,0.12 ] },
]},
1000
]
}
}},
])
output: {"Conventional Energy" : 0.0036}
Collection2:
db.collection2.aggregate(
[
// Stage 1
{
$group: {
_id:{year:{$year:"$timeId"},month:{$month:"$timeId"} },
LastMonthConsumption : {$sum:"$mongoStreetLightChildVo.totalConsumptionMtd"},
}
},
{
$redact: {
$cond: { if: { $and:[
{$eq: [ "$_id.year", {$year:new Date()} ]},
{$eq: [-1, {$subtract:[ "$_id.month", {$month:new Date()} ]}]}
]},
then: "$$KEEP",
else: "$$PRUNE"
}
}
},
{$project:{
_id:0,
LastMonthConsumption :1
}
}
]
);
output:{
"LastMonthConsumption" : 2.08
}
Expected output:
LastMonthConsumption - Conventional Energy/Conventional Energy*100
You can try below aggregation
db.collection2.aggregate([
{ "$group": {
"_id": { "year": { "$year": "$timeId" }, "month": { "$month": "$timeId" }},
"LastMonthConsumption": { "$sum": "$mongoStreetLightChildVo.totalConsumptionMtd" }
}},
{ "$redact": {
"$cond": {
"if": {
"$and": [
{ "$eq": ["$_id.year", { "$year": new Date() }] },
{ "$eq": [-1, { "$subtract": ["$_id.month", { "$month": new Date() }] }]
}
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$lookup": {
"from": "collection1",
"pipeline": [
{ "$match": { "type": "L" } },
{ "$count": "TOTAL_Lights" },
{ "$project": {
"ConventionalEnergy": {
"$divide": [{ "$multiply": [{ "$multiply": ["$TOTAL_Lights", 0.12] }] }, 1000]
}
}}
],
"as": "ConventionalEnergy"
}},
{ "$project": {
"_id": 0,
"totalConsumption": {
"$multiply": [
{
"$divide": [
{
"$subtract": [
"$LastMonthConsumption",
{ "$arrayElemAt": ["$ConventionalEnergy.ConventionalEnergy", 0] }
]
},
{ "$arrayElemAt": ["$ConventionalEnergy.ConventionalEnergy", 0] }
]
},
100
]
}
}}
])

How to Implement $bucket to group by multiple fields

At first bucket by age and boundaries is [0,20,30,40,50,200]
db.user.aggregate(
{$project: {_id:0, age:{$subtract:[{$year:new Date()}, {$year:"$birthDay"}]} } },
{$bucket:{
groupBy:"$age",
boundaries:[0,20,30,40,50,200]
}},
{ $project:{ _id:0,age:"$_id",count:1 } }
)
got below result
{ "count" : 5, "age" : 20 }
{ "count" : 1, "age" : 30 }
then further I want to stat every age range count of each city
{ city : "SH", age: 20, count: 2 }
{ city : "BJ", age: 20, count: 3 }
{ city : "BJ", age: 30, count: 1 }
So in this case how to implement it ?
In addition
db.user.aggregate(
{ $project: {_id:0, city:1, age:{$subtract:[{$year:new Date()}, {$year:"$birthDay"}]} } },
{ $group: { _id:"$city",ages:{$push:"$age"} } },
{ $project: {_id:0, city:"$_id",ages:1} }
)
{ "city" : "SH", "ages" : [ 26, 26 ] }
{ "city" : "BJ", "ages" : [ 27, 26, 26, 36 ] }
What you are talking about is actually implemented with $switch, within a regular $group stage:
db.user.aggregate([
{ "$group": {
"_id": {
"city": "$city",
"age": {
"$let": {
"vars": {
"age": { "$subtract" :[{ "$year": new Date() },{ "$year": "$birthDay" }] }
},
"in": {
"$switch": {
"branches": [
{ "case": { "$lt": [ "$$age", 20 ] }, "then": 0 },
{ "case": { "$lt": [ "$$age", 30 ] }, "then": 20 },
{ "case": { "$lt": [ "$$age", 40 ] }, "then": 30 },
{ "case": { "$lt": [ "$$age", 50 ] }, "then": 40 },
{ "case": { "$lt": [ "$$age", 200 ] }, "then": 50 }
]
}
}
}
}
},
"count": { "$sum": 1 }
}}
])
With the results:
{ "_id" : { "city" : "BJ", "age" : 30 }, "count" : 1 }
{ "_id" : { "city" : "BJ", "age" : 20 }, "count" : 3 }
{ "_id" : { "city" : "SH", "age" : 20 }, "count" : 2 }
The $bucket pipeline stage only takes a single field path. You can have multiple accumulators via the "output" option, but the "groupBy" is a single expression.
Note you can also use $let here in preference to a separate $project pipeline stage to calculate the "age".
N.B If you actually throw some erroneous expressions to $bucket you will get errors about $switch, which should hint to you that this is how it is implemented internally.
If you are worried about coding in the $switch then just generate it:
var ranges = [0,20,30,40,50,200];
var branches = [];
for ( var i=1; i < ranges.length; i++) {
branches.push({ "case": { "$lt": [ "$$age", ranges[i] ] }, "then": ranges[i-1] });
}
db.user.aggregate([
{ "$group": {
"_id": {
"city": "$city",
"age": {
"$let": {
"vars": {
"age": {
"$subtract": [{ "$year": new Date() },{ "$year": "$birthDay" }]
}
},
"in": {
"$switch": { "branches": branches }
}
}
}
},
"count": { "$sum": 1 }
}}
])
Supply another implementation by using Map-Reduce
db.user.mapReduce(
function(){
var age = new Date().getFullYear() - this.birthDay.getFullYear();
var ages = [0,20,30,40,50,200]
for(var i=1; i<ages.length; i++){
if(age < ages[i]){
emit({city:this.city,age:ages[i-1]},1);
break;
}
}
},
function(key, counts){
return Array.sum(counts);
},
{ out: "user_city_age_count" }
)

Group and count over a start and end range

If I have data in the following format:
[
{
_id: 1,
startDate: ISODate("2017-01-1T00:00:00.000Z"),
endDate: ISODate("2017-02-25T00:00:00.000Z"),
type: 'CAR'
},
{
_id: 2,
startDate: ISODate("2017-02-17T00:00:00.000Z"),
endDate: ISODate("2017-03-22T00:00:00.000Z"),
type: 'HGV'
}
]
Is it possible to retrieve data grouped by 'type', but also with a count of the type for each of month in a given date range e.g. between 2017/1/1 to 2017/4/1 would return:
[
{
_id: 'CAR',
monthCounts: [
/*January*/
{
from: ISODate("2017-01-1T00:00:00.000Z"),
to: ISODate("2017-01-31T23:59:59.999Z"),
count: 1
},
/*February*/
{
from: ISODate("2017-02-1T00:00:00.000Z"),
to: ISODate("2017-02-28T23:59:59.999Z"),
count: 1
},
/*March*/
{
from: ISODate("2017-03-1T00:00:00.000Z"),
to: ISODate("2017-03-31T23:59:59.999Z"),
count: 0
},
]
},
{
_id: 'HGV',
monthCounts: [
{
from: ISODate("2017-01-1T00:00:00.000Z"),
to: ISODate("2017-01-31T23:59:59.999Z"),
count: 0
},
{
from: ISODate("2017-02-1T00:00:00.000Z"),
to: ISODate("2017-02-28T23:59:59.999Z"),
count: 1
},
{
from: ISODate("2017-03-1T00:00:00.000Z"),
to: ISODate("2017-03-31T23:59:59.999Z"),
count: 1
},
]
}
]
The returned format is not really important, but what I am trying to achieve is in a single query to retrieve a number of counts for the same grouping (one per month). The input could be simply a start and end date to report from or more likely it could be an array of the date ranges to group by.
The algorithm for this is to basically "iterate" values between the interval of the two values. MongoDB has a couple of ways to deal with this, being what has always been present with mapReduce() and with new features available to the aggregate() method.
I'm going expand on your selection to deliberately show an overlapping month since your examples did not have one. This will result in the "HGV" values appearing in "three" months of output.
{
"_id" : 1,
"startDate" : ISODate("2017-01-01T00:00:00Z"),
"endDate" : ISODate("2017-02-25T00:00:00Z"),
"type" : "CAR"
}
{
"_id" : 2,
"startDate" : ISODate("2017-02-17T00:00:00Z"),
"endDate" : ISODate("2017-03-22T00:00:00Z"),
"type" : "HGV"
}
{
"_id" : 3,
"startDate" : ISODate("2017-02-17T00:00:00Z"),
"endDate" : ISODate("2017-04-22T00:00:00Z"),
"type" : "HGV"
}
Aggregate - Requires MongoDB 3.4
db.cars.aggregate([
{ "$addFields": {
"range": {
"$reduce": {
"input": { "$map": {
"input": { "$range": [
{ "$trunc": {
"$divide": [
{ "$subtract": [ "$startDate", new Date(0) ] },
1000
]
}},
{ "$trunc": {
"$divide": [
{ "$subtract": [ "$endDate", new Date(0) ] },
1000
]
}},
60 * 60 * 24
]},
"as": "el",
"in": {
"$let": {
"vars": {
"date": {
"$add": [
{ "$multiply": [ "$$el", 1000 ] },
new Date(0)
]
},
"month": {
}
},
"in": {
"$add": [
{ "$multiply": [ { "$year": "$$date" }, 100 ] },
{ "$month": "$$date" }
]
}
}
}
}},
"initialValue": [],
"in": {
"$cond": {
"if": { "$in": [ "$$this", "$$value" ] },
"then": "$$value",
"else": { "$concatArrays": [ "$$value", ["$$this"] ] }
}
}
}
}
}},
{ "$unwind": "$range" },
{ "$group": {
"_id": {
"type": "$type",
"month": "$range"
},
"count": { "$sum": 1 }
}},
{ "$sort": { "_id": 1 } },
{ "$group": {
"_id": "$_id.type",
"monthCounts": {
"$push": { "month": "$_id.month", "count": "$count" }
}
}}
])
The key to making this work is the $range operator which takes values for a "start" and and "end" as well as an "interval" to apply. The result is an array of values taken from the "start" and incremented until the "end" is reached.
We use this with startDate and endDate to generate the possible dates in between those values. You will note that we need to do some math here since the $range only takes a 32-bit integer, but we can take the milliseconds away from the timestamp values so that is okay.
Because we want "months", the operations applied extract the month and year values from the generated range. We actually generate the range as the "days" in between since "months" are difficult to deal with in math. The subsequent $reduce operation takes only the "distinct months" from the date range.
The result therefore of the first aggregation pipeline stage is a new field in the document which is an "array" of all the distinct months covered between startDate and endDate. This gives an "iterator" for the rest of the operation.
By "iterator" I mean than when we apply $unwind we get a copy of the original document for every distinct month covered in the interval. This then allows the following two $group stages to first apply a grouping to the common key of "month" and "type" in order to "total" the counts via $sum, and next $group makes the key just the "type" and puts the results in an array via $push.
This gives the result on the above data:
{
"_id" : "HGV",
"monthCounts" : [
{
"month" : 201702,
"count" : 2
},
{
"month" : 201703,
"count" : 2
},
{
"month" : 201704,
"count" : 1
}
]
}
{
"_id" : "CAR",
"monthCounts" : [
{
"month" : 201701,
"count" : 1
},
{
"month" : 201702,
"count" : 1
}
]
}
Note that the coverage of "months" is only present where there is actual data. Whilst possible to produce zero values over a range, it requires quite a bit of wrangling to do so and is not very practical. If you want zero values then it is better to add that in post processing in the client once the results have been retrieved.
If you really have your heart set on the zero values, then you should separately query for $min and $max values, and pass these in to "brute force" the pipeline into generating the copies for each supplied possible range value.
So this time the "range" is made externally to all documents, and you then use a $cond statement into the accumulator to see if the current data is within the grouped range produced. Also since the generation is "external", we really don't need the MongoDB 3.4 operator of $range, so this can be applied to earlier versions as well:
// Get min and max separately
var ranges = db.cars.aggregate(
{ "$group": {
"_id": null,
"startRange": { "$min": "$startDate" },
"endRange": { "$max": "$endDate" }
}}
).toArray()[0]
// Make the range array externally from all possible values
var range = [];
for ( var d = new Date(ranges.startRange.valueOf()); d <= ranges.endRange; d.setUTCMonth(d.getUTCMonth()+1)) {
var v = ( d.getUTCFullYear() * 100 ) + d.getUTCMonth()+1;
range.push(v);
}
// Run conditional aggregation
db.cars.aggregate([
{ "$addFields": { "range": range } },
{ "$unwind": "$range" },
{ "$group": {
"_id": {
"type": "$type",
"month": "$range"
},
"count": {
"$sum": {
"$cond": {
"if": {
"$and": [
{ "$gte": [
"$range",
{ "$add": [
{ "$multiply": [ { "$year": "$startDate" }, 100 ] },
{ "$month": "$startDate" }
]}
]},
{ "$lte": [
"$range",
{ "$add": [
{ "$multiply": [ { "$year": "$endDate" }, 100 ] },
{ "$month": "$endDate" }
]}
]}
]
},
"then": 1,
"else": 0
}
}
}
}},
{ "$sort": { "_id": 1 } },
{ "$group": {
"_id": "$_id.type",
"monthCounts": {
"$push": { "month": "$_id.month", "count": "$count" }
}
}}
])
Which produces the consistent zero fills for all possible months on all groupings:
{
"_id" : "HGV",
"monthCounts" : [
{
"month" : 201701,
"count" : 0
},
{
"month" : 201702,
"count" : 2
},
{
"month" : 201703,
"count" : 2
},
{
"month" : 201704,
"count" : 1
}
]
}
{
"_id" : "CAR",
"monthCounts" : [
{
"month" : 201701,
"count" : 1
},
{
"month" : 201702,
"count" : 1
},
{
"month" : 201703,
"count" : 0
},
{
"month" : 201704,
"count" : 0
}
]
}
MapReduce
All versions of MongoDB support mapReduce, and the simple case of the "iterator" as mentioned above is handled by a for loop in the mapper. We can get output as generated up to the first $group from above by simply doing:
db.cars.mapReduce(
function () {
for ( var d = this.startDate; d <= this.endDate;
d.setUTCMonth(d.getUTCMonth()+1) )
{
var m = new Date(0);
m.setUTCFullYear(d.getUTCFullYear());
m.setUTCMonth(d.getUTCMonth());
emit({ id: this.type, date: m},1);
}
},
function(key,values) {
return Array.sum(values);
},
{ "out": { "inline": 1 } }
)
Which produces:
{
"_id" : {
"id" : "CAR",
"date" : ISODate("2017-01-01T00:00:00Z")
},
"value" : 1
},
{
"_id" : {
"id" : "CAR",
"date" : ISODate("2017-02-01T00:00:00Z")
},
"value" : 1
},
{
"_id" : {
"id" : "HGV",
"date" : ISODate("2017-02-01T00:00:00Z")
},
"value" : 2
},
{
"_id" : {
"id" : "HGV",
"date" : ISODate("2017-03-01T00:00:00Z")
},
"value" : 2
},
{
"_id" : {
"id" : "HGV",
"date" : ISODate("2017-04-01T00:00:00Z")
},
"value" : 1
}
So it does not have the second grouping to compound to arrays, but we did produce the same basic aggregated output.

Aggregate by timestamp and Sum by float

I have a set of data in mongoDB that I have to sum up grouped by $timestamp. I succeeded in grouping them day by day, but now I need to sum them by another field.
Example data:
[
{
_id: "1442",
timestamp: "1458080642000",
iden: "15",
scores_today: "0.000000",
scores_total: "52337.000000"
}
]
My code
var project = {
"$project":{
"_id" : 0,
"y": {
"$year": {
"$add": [
new Date(0), "$timestamp"
]
}
},
"m": {
"$month": {
"$add": [
new Date(0), "$timestamp"
]
}
},
"d": {
"$dayOfMonth": {
"$add": [
new Date(0), "$timestamp"
]
}
},
"iden" : "$iden",
"totalTd" : "$scores_today"
"total" : "$scores_today_total"
}
},
group = {
"$group": {
"_id": {
"mac" : "$mac",
"year": "$y",
"month": "$m",
"day": "$d"
},
count : { "$sum" : "$total"}
countOther : { "$sum" : "$totalTd" }
}
};
mongoDB.collection('raw').aggregate([ project, group ]).toArray....
I'm not able to sum them. What I need to change?
I need to group them day by day (and this works ) and by iden ( works ) then sum up differents scores.

Group By Hour using UNIX time stamp in mongodb

I required records with the output of gender, count, and updated hour for two days.
db.FaceData.aggregate([ {$match: { 'Timestamp' : { $gte : 1448121600000, $lt : 1448294399000 }, 'DID' : "ABFR001" }}, {$group: { _id: {'Gen': '$Gen'}, count : { $sum : 1 } }} ]);
output:
------
{ "_id" : { "Gen" : 1 }, "count" : 3055 }
{ "_id" : { "Gen" : 0 }, "count" : 2866 }
In the above output I have to group by hour for two days, For Example, Every hour I need Gender, Count for 2days.
Timestamp is in millisecond.
You would need a mechanism to get the actual date object from the unix timestamp, one way is to add the timestamp to a zero-milliseconds Date() object, using the $add operator in the $project stage before the actual grouping aggregation pipeline.
Once you get the date, extract the hour part by using the $hour operator, something like the following:
db.FaceData.aggregate([
{
"$match": {
"Timestamp" : { $gte : 1448121600000, $lt : 1448294399000 },
"DID" : "ABFR001"
}
},
{
$project : {
"hourPart" : {
"$hour": { "$add": [ new Date(0), "$Timestamp" ] }
},
"Gen": 1
}
},
{
"$group": {
"_id": "$hourPart",
"Gen_0_count" : {
"$sum": {
"$cond": [ { "$eq": [ "$Gen", 0 ] }, 1, 0 ]
}
},
"Gen_1_count" : {
"$sum": {
"$cond": [ { "$eq": [ "$Gen", 1 ] }, 1, 0 ]
}
}
}
}
]);
{"$match": {
"Timestamp" : { $gte : 1448121600000, $lt : 1448294399000 },
"DID" : "ABFR001"
}} ,
{ "$group" : {
"_id" : {
"$divide" : [{ "$subtract" : [{"$divide" : ["$Timestamp", 1000]}, { "$mod" : [{"$divide" : ["$Tstmp", 1000]}, 3600] }] }, 3600 ]
},
"Male" : {
"$sum": {
"$cond": [ { "$eq": [ "$Gen", 0 ] }, 1, 0 ]
}
},
"Female" : {
"$sum": {
"$cond": [ { "$eq": [ "$Gen", 1 ] }, 1, 0 ]
}
}
} }