The aggregation structure, of course, is the right approach - everything that requires JS on the server is a performance issue, while aggregations are performed on the server in native code.
While it is possible to convert a birthday to dates of upcoming birthdays, and then run a range request, I prefer to do it a little differently.
The only "prerequisite is to calculate the current day of the year." There are ways to do this in different languages , so you can do this at the application level before invoking aggregation by passing this number to it. I was going to name my todayDayOfYear , but I realized that you can let the aggregation infrastructure understand this based on today, so the only variable is today.
var today=new Date();
I accept the document, including the name and birthday, I change the options accordingly
var p1 = { "$project" : { "_id" : 0, "name" : 1, "birthday" : 1, "todayDayOfYear" : { "$dayOfYear" : today }, "dayOfYear" : { "$dayOfYear" : "$birthday"} } };
Now ask how many days from today to the next birthday:
var p2 = { "$project" : { "name" : 1, "birthday" : 1, "daysTillBirthday" : { "$subtract" : [ { "$add" : [ "$dayOfYear", { "$cond" : [{"$lt":["$dayOfYear","$todayDayOfYear"]},365,0 ] } ] }, "$todayDayOfYear" ] } } };
Exclude everything except those that are within the required range:
var m = { "$match" : { "daysTillBirthday" : { "$lt" : 31 } } };
Now run the aggregation with:
db.collection.aggregate( p1, p2, m );
to return a list of names, birthdays, and days before a birthday for all lucky people whose birthday is within 30 days.
EDIT
@ Sean999 caught an interesting extreme case - people who were born in a leap year after February 28 will count them. The following is an aggregation that is correctly configured for this:
var p1 = { "$project" : { "_id" : 0, "name" : 1, "birthday" : 1, "todayDayOfYear" : { "$dayOfYear" : ISODate("2014-03-09T12:30:51.515Z") }, "leap" : { "$or" : [ { "$eq" : [ 0, { "$mod" : [ { "$year" : "$birthday" }, 400 ] } ] }, { "$and" : [ { "$eq" : [ 0, { "$mod" : [ { "$year" : "$birthday" }, 4 ] } ] }, { "$ne" : [ 0, { "$mod" : [ { "$year" : "$birthday" }, 100 ] } ] } ] } ] }, "dayOfYear" : { "$dayOfYear" : "$birthday" } } }; var p1p = { "$project" : { "name" : 1, "birthday" : 1, "todayDayOfYear" : 1, "dayOfYear" : { "$subtract" : [ "$dayOfYear", { "$cond" : [ { "$and" : [ "$leap", { "$gt" : [ "$dayOfYear", 59 ] } ] }, 1, 0 ] } ] } } }
p2 and m remain the same as above.
Test input:
db.birthdays.find({},{name:1,birthday:1,_id:0}) { "name" : "Ally", "birthday" : ISODate("1975-06-12T00:00:00Z") } { "name" : "Ben", "birthday" : ISODate("1968-04-03T00:00:00Z") } { "name" : "Mark", "birthday" : ISODate("1949-12-23T00:00:00Z") } { "name" : "Paul", "birthday" : ISODate("2014-03-04T15:59:05.374Z") } { "name" : "Paul", "birthday" : ISODate("2011-02-07T00:00:00Z") } { "name" : "Sean", "birthday" : ISODate("2004-01-31T00:00:00Z") } { "name" : "Tim", "birthday" : ISODate("2008-02-28T00:00:00Z") } { "name" : "Sandy", "birthday" : ISODate("2005-01-31T00:00:00Z") } { "name" : "Toni", "birthday" : ISODate("2009-02-28T00:00:00Z") } { "name" : "Sam", "birthday" : ISODate("2005-03-31T00:00:00Z") } { "name" : "Max", "birthday" : ISODate("2004-03-31T00:00:00Z") } { "name" : "Jen", "birthday" : ISODate("1971-04-03T00:00:00Z") } { "name" : "Ellen", "birthday" : ISODate("1996-02-28T00:00:00Z") } { "name" : "Fanny", "birthday" : ISODate("1996-02-29T00:00:00Z") } { "name" : "Gene", "birthday" : ISODate("1996-03-01T00:00:00Z") } { "name" : "Edgar", "birthday" : ISODate("1997-02-28T00:00:00Z") } { "name" : "George", "birthday" : ISODate("1997-03-01T00:00:00Z") }
Output:
db.birthdays.aggregate( p1, p1p, p2, {$sort:{daysTillBirthday:1}}); { "name" : "Sam", "birthday" : ISODate("2005-03-31T00:00:00Z"), "daysTillBirthday" : 22 } { "name" : "Max", "birthday" : ISODate("2004-03-31T00:00:00Z"), "daysTillBirthday" : 22 } { "name" : "Ben", "birthday" : ISODate("1968-04-03T00:00:00Z"), "daysTillBirthday" : 25 } { "name" : "Jen", "birthday" : ISODate("1971-04-03T00:00:00Z"), "daysTillBirthday" : 25 } { "name" : "Ally", "birthday" : ISODate("1975-06-12T00:00:00Z"), "daysTillBirthday" : 95 } { "name" : "Mark", "birthday" : ISODate("1949-12-23T00:00:00Z"), "daysTillBirthday" : 289 } { "name" : "Sean", "birthday" : ISODate("2004-01-31T00:00:00Z"), "daysTillBirthday" : 328 } { "name" : "Sandy", "birthday" : ISODate("2005-01-31T00:00:00Z"), "daysTillBirthday" : 328 } { "name" : "Paul", "birthday" : ISODate("2011-02-07T00:00:00Z"), "daysTillBirthday" : 335 } { "name" : "Tim", "birthday" : ISODate("2008-02-28T00:00:00Z"), "daysTillBirthday" : 356 } { "name" : "Toni", "birthday" : ISODate("2009-02-28T00:00:00Z"), "daysTillBirthday" : 356 } { "name" : "Ellen", "birthday" : ISODate("1996-02-28T00:00:00Z"), "daysTillBirthday" : 356 } { "name" : "Fanny", "birthday" : ISODate("1996-02-29T00:00:00Z"), "daysTillBirthday" : 356 } { "name" : "Edgar", "birthday" : ISODate("1997-02-28T00:00:00Z"), "daysTillBirthday" : 356 } { "name" : "Gene", "birthday" : ISODate("1996-03-01T00:00:00Z"), "daysTillBirthday" : 357 } { "name" : "George", "birthday" : ISODate("1997-03-01T00:00:00Z"), "daysTillBirthday" : 357 } { "name" : "Paul", "birthday" : ISODate("2014-03-04T15:59:05.374Z"), "daysTillBirthday" : 360 }
You can see that people with the same birthday have the same number of days before the birthday, whether they were born in a leap year or not. You can now complete the step for the cut slice.
EDIT
Starting with version 3.5.11, there are several date manipulation expressions in the aggregation pipeline that greatly simplify writing. In particular, the expression $ dateFromParts allows you to build a date from different parts, allowing this aggregation:
var today = new Date(); var a1 = {$addFields:{ today:{$dateFromParts:{year:{$year:today},month:{$month:today},day:{$dayOfMonth:today}}}, birthdayThisYear:{$dateFromParts:{year:{$year:today}, month:{$month:"$birthday"}, day:{$dayOfMonth:"$birthday"}}}, birthdayNextYear:{$dateFromParts:{year:{$add:[1,{$year:today}]}, month:{$month:"$birthday"}, day:{$dayOfMonth:"$birthday"}}} }}; var a2 = {$addFields:{ nextBirthday:{$cond:[ {$gte:[ "$birthdayThisYear", "$today"]}, "$birthdayThisYear", "$birthdayNextYear"]} }}; var p1 = {$project:{ name:1, birthday:1, daysTillNextBirthday:{$divide:[ {$subtract:["$nextBirthday", "$today"]}, 24*60*60*1000 /* milliseconds in a day */ ]}, _id:0 }}; var s1 = {$sort:{daysTillNextBirthday:1}}; db.birthdays.aggregate([ a1, a2, p1, s1 ]);
You can set "today" to any date (leap year or not) and see that the calculation is now always correct and much easier.