Finally I got an answer following this answer. It uses the Aggregation Pipeline.
First of all, I decided to use Date/ISODate type to store birthdays, instead of individual year/month/day fields. So my test collection would have these entries:
{
"birthday" : ISODate("1985-04-16T10:00:00.000Z"),
"name" : "J"
}
{
"birthday" : ISODate("1985-09-16T11:00:00.000Z"),
"name" : "E"
}
{
"birthday" : ISODate("1950-11-11T11:00:00.000Z"),
"name" : "M"
}
{
"birthday" : ISODate("1947-05-06T10:00:00.000Z"),
"name" : "D"
}
Then, I constructed a few structures to use in aggregation pipeline:
- In
pDayOfYear get sequential year of the year of the birthday and today, using $dayOfYear operator.
- In
pLeapYear subtract 1 to dayofYear field if birthday was in a leap year and calculate the difference between each birthday and today.
- In
pPast add 365 to past birthdays to have positiveDiff field.
- In
pSort sort results by positiveDiff field in descending order, so we can can have a list of next birthdays
- In
pFirst get only the first result.
Query would look like:
pDayOfYear = {
"$project": {
"birthday": 1,
"todayDayOfYear": {"$dayOfYear": new ISODate()},
"leap": {"$or": [
{"$eq": [0, {"$mod": [{"$year": "$birthday"}, 400]}]},
{"$and": [
{"$eq": [0, {"$mod": [{"$year": "$birthday"}, 4]}]},
{"$ne": [0, {"$mod": [{"$year": "$birthday"}, 100]}]}
]}
]},
"dayOfYear": {"$dayOfYear": "$birthday"}
}
}
pLeapYear = {
"$project": {
"birthday": 1,
"todayDayOfYear": 1,
"dayOfYear": {
"$subtract": [
"$dayOfYear",
{
"$cond": [
{"$and":
["$leap",
{"$gt": ["$dayOfYear", 59]}
]},
1,
0]
}
]},
"diff": { "$subtract": [ "$dayOfYear", "$todayDayOfYear"] }
}
}
pPast = {
"$project": {
"diff": 1,
"birthday": 1,
"positiveDiff": {
"$cond": {
"if": { "$lt": ["$diff", 0 ]},
"then": { "$add": ["$diff", 365] },
"else": "$diff"
},
}
}
}
pSort = {
"$sort": {
"positiveDiff": 1
}
}
pFirst = {
"$group": {
"_id": "first_birthday",
"first": {
"$first": "$$ROOT"
}
}
}
db.getCollection('tests').aggregate(pDayOfYear, pLeapYear, pPast, pSort, pFirst);
So I would get next birthday ID and I could query the field by ID. One can get next birthday of an specific date changing todayDayOfYearfield by any date.
I'm open to any change, specially readability and efficiency improvements.