This utility helps convert times from UTC to a specified timezone in mongo (database-side rather than application side). This is crucial when aggregating data by date ranges (for example aggregate by day/week/month in a specific timezone). This cannot be done application-side since the bucketing will be wrong.
A naive solution simply applies a constant offset to all dates. However, when the dataset contains varying timezone offsets (e.g. daylight savings time) then this method will yield incorrect results. This utility solves that issue.
- Specify start/end range
- Specify target timezone
- Generate $cond expression
The generator takes a start date, end date, and a target timezone. The source timezone is assumed UTC.
generator(start, end, timezone) { ... }
The generator returns a mongo $cond expression that can be used directly for projecting. The function assumes that the time is in a field called time
. This can easily be changed as needed.
// specify start, end, and target timezone
const start = moment.utc("2016-01-01T00:00:00Z");
const end = moment.utc("2017-01-01T00:00:00Z");
const tz = "Europe/Berlin";
// generate the $cond expression
const cond = generator(start, end, tz);
// create a projection
db.collection(DB_COLLECTION).aggregate([
{
$project: {
name: "$name",
time_orig: "$time",
time_conv: cond
}
}
]).toArray((err, res) => {
console.log(res);
});
The generator works like this:
First, fetch zone object from moment-timezone
for the timezone
Then, find all { until, offset }
pairs for the specified start-end range
Recursively build the $cond expression:
$cond: {
if: { $lt: ["$time", new Date(until)] },
then: { $subtract: ["$time", offset] },
else: {
$cond: { ... }
}
}
For a start-end range that doesn't contain any boundaries, the condition will look something like this:
{
"$subtract": ["$time", -3600000]
}
For a more complicated start-end range containing one or more boundaries, the condition will be a bit more complicated:
{
"$cond": {
"if": {
"$lt": ["$time",new Date("2016-03-27T01:00:00.000Z")]
},
"then": {
"$subtract": ["$time",-3600000]
},
"else": {
"$cond": {
"if": {
"$lt": ["$time",new Date("2016-10-30T01:00:00.000Z")]
},
"then": {
"$subtract": ["$time",-7200000]
},
"else": {
"$subtract": ["$time",-3600000]
}
}
}
}
}