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 todayDayOfYear
field by any date.
I'm open to any change, specially readability and efficiency improvements.