MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Gruppieren nach Datum mit lokaler Zeitzone in MongoDB

Allgemeines Problem im Umgang mit "lokalen Daten"

Darauf gibt es also eine kurze und eine lange Antwort. Der grundlegende Fall ist, dass Sie statt eines der "Datumsaggregationsoperatoren" stattdessen lieber die Datumsobjekte "rechnen" möchten und "müssen". Das Wichtigste hier ist, die Werte um den Offset von UTC für die gegebene lokale Zeitzone anzupassen und dann auf das erforderliche Intervall zu "runden".

Die "viel längere Antwort" und auch das zu berücksichtigende Hauptproblem besteht darin, dass Daten zu verschiedenen Jahreszeiten häufig Änderungen der "Sommerzeit" im Offset von UTC unterliegen. Das bedeutet also, dass Sie bei der Umrechnung in „Ortszeit“ für solche Aggregationszwecke genau überlegen sollten, wo die Grenzen für solche Änderungen liegen.

Es gibt noch eine weitere Überlegung:Unabhängig davon, was Sie tun, um in einem bestimmten Intervall zu "aggregieren", sollten die Ausgabewerte zumindest anfänglich als UTC ausgegeben werden. Dies ist eine bewährte Vorgehensweise, da die Anzeige in "Gebietsschema" wirklich eine "Client-Funktion" ist, und wie später beschrieben wird, haben die Client-Schnittstellen im Allgemeinen eine Möglichkeit, in der aktuellen Ländereinstellung anzuzeigen, die auf der Prämisse basiert, dass es tatsächlich gefüttert wurde Daten als UTC.

Ermitteln des Gebietsschema-Offsets und der Sommerzeit

Dies ist im Allgemeinen das Hauptproblem, das gelöst werden muss. Die allgemeine Mathematik zum "Runden" eines Datums auf ein Intervall ist der einfache Teil, aber es gibt keine wirkliche Mathematik, die Sie anwenden können, um zu wissen, wann solche Grenzen gelten, und die Regeln ändern sich in jedem Gebietsschema und oft jedes Jahr.

Hier kommt also eine „Bibliothek“ ins Spiel, und die beste Option hier nach Meinung des Autors für eine JavaScript-Plattform ist moment-timezone, die im Grunde eine „Obermenge“ von moment.js ist, einschließlich aller wichtigen „timezeone“-Funktionen, die wir wollen zu verwenden.

Moment Timezone definiert im Grunde eine solche Struktur für jede Gebietsschema-Zeitzone wie folgt:

{
    name    : 'America/Los_Angeles',          // the unique identifier
    abbrs   : ['PDT', 'PST'],                 // the abbreviations
    untils  : [1414918800000, 1425808800000], // the timestamps in milliseconds
    offsets : [420, 480]                      // the offsets in minutes
}

Wobei die Objekte natürlich viel sind größer in Bezug auf untils und offsets Eigenschaften tatsächlich erfasst. Aber das sind die Daten, auf die Sie zugreifen müssen, um zu sehen, ob es tatsächlich eine Änderung des Offsets für eine Zone gibt, wenn sich die Sommerzeit ändert.

Dieser Block des späteren Code-Listings ist das, was wir grundsätzlich verwenden, um einen gegebenen start zu bestimmen und end Wert für einen Bereich, in dem ggf. Sommerzeitgrenzen überschritten werden:

  const zone = moment.tz.zone(locale);
  if ( zone.hasOwnProperty('untils') ) {
    let between = zone.untils.filter( u =>
      u >= start.valueOf() && u < end.valueOf()
    );
    if ( between.length > 0 )
      branches = between
        .map( d => moment.tz(d, locale) )
        .reduce((acc,curr,i,arr) =>
          acc.concat(
            ( i === 0 )
              ? [{ start, end: curr }] : [{ start: acc[i-1].end, end: curr }],
            ( i === arr.length-1 ) ? [{ start: curr, end }] : []
          )
        ,[]);
  }

Betrachtet man das gesamte Jahr 2017 für Australia/Sydney locale wäre die Ausgabe:

[
  {
    "start": "2016-12-31T13:00:00.000Z",    // Interval is +11 hours here
    "end": "2017-04-01T16:00:00.000Z"
  },
  {
    "start": "2017-04-01T16:00:00.000Z",    // Changes to +10 hours here
    "end": "2017-09-30T16:00:00.000Z"
  },
  {
    "start": "2017-09-30T16:00:00.000Z",    // Changes back to +11 hours here
    "end": "2017-12-31T13:00:00.000Z"
  }
]

Was im Grunde zeigt, dass der Versatz zwischen der ersten Datumsfolge +11 Stunden betragen würde, sich dann zwischen den Daten in der zweiten Folge auf +10 Stunden ändert und dann für das Intervall, das das Ende des Jahres und das abdeckt, auf +11 Stunden zurückschaltet angegebenen Bereich.

Diese Logik muss dann in eine Struktur übersetzt werden, die von MongoDB als Teil einer Aggregationspipeline verstanden wird.

Mathematik anwenden

Das mathematische Prinzip hier zum Aggregieren zu einem beliebigen „gerundeten Datumsintervall“ beruht im Wesentlichen auf der Verwendung des Millisekundenwerts des dargestellten Datums, der auf die nächste Zahl „abgerundet“ wird, die das erforderliche „Intervall“ darstellt.

Sie tun dies im Wesentlichen, indem Sie den "Modulo" oder "Rest" des aktuellen Werts finden, der auf das erforderliche Intervall angewendet wird. Dann "subtrahieren" Sie diesen Rest vom aktuellen Wert, der einen Wert im nächsten Intervall zurückgibt.

Beispiel für das aktuelle Datum:

  var d = new Date("2017-07-14T01:28:34.931Z"); // toValue() is 1499995714931 millis
  // 1000 millseconds * 60 seconds * 60 minutes = 1 hour or 3600000 millis
  var v = d.valueOf() - ( d.valueOf() % ( 1000 * 60 * 60 ) );
  // v equals 1499994000000 millis or as a date
  new Date(1499994000000);
  ISODate("2017-07-14T01:00:00Z") 
  // which removed the 28 minutes and change to nearest 1 hour interval

Dies ist die allgemeine Mathematik, die wir auch in der Aggregationspipeline mit $subtract anwenden müssen und $mod Operationen, bei denen es sich um Aggregationsausdrücke handelt, die für die oben gezeigten mathematischen Operationen verwendet werden.

Die allgemeine Struktur der Aggregationspipeline ist dann:

    let pipeline = [
      { "$match": {
        "createdAt": { "$gte": start.toDate(), "$lt": end.toDate() }
      }},
      { "$group": {
        "_id": {
          "$add": [
            { "$subtract": [
              { "$subtract": [
                { "$subtract": [ "$createdAt", new Date(0) ] },
                switchOffset(start,end,"$createdAt",false)
              ]},
              { "$mod": [
                { "$subtract": [
                  { "$subtract": [ "$createdAt", new Date(0) ] },
                  switchOffset(start,end,"$createdAt",false)
                ]},
                interval
              ]}
            ]},
            new Date(0)
          ]
        },
        "amount": { "$sum": "$amount" }
      }},
      { "$addFields": {
        "_id": {
          "$add": [
            "$_id", switchOffset(start,end,"$_id",true)
          ]
        }
      }},
      { "$sort": { "_id": 1 } }
    ];

Die wichtigsten Teile hier, die Sie verstehen müssen, ist die Konvertierung von einem Date Objekt, wie es in MongoDB gespeichert ist, in Numeric die den internen Zeitstempelwert darstellt. Wir brauchen die "numerische" Form, und um dies zu tun, ist ein mathematischer Trick, bei dem wir ein BSON-Datum von einem anderen subtrahieren, was die numerische Differenz zwischen ihnen ergibt. Genau das bewirkt diese Anweisung:

{ "$subtract": [ "$createdAt", new Date(0) ] }

Jetzt müssen wir uns mit einem numerischen Wert befassen, wir können das Modulo anwenden und das von der numerischen Darstellung des Datums subtrahieren, um es zu "runden". Die "geradlinige" Darstellung davon ist also wie folgt:

{ "$subtract": [
  { "$subtract": [ "$createdAt", new Date(0) ] },
  { "$mod": [
    { "$subtract": [ "$createdAt", new Date(0) ] },
    ( 1000 * 60 * 60 * 24 ) // 24 hours
  ]}
]}

Dies spiegelt den gleichen mathematischen JavaScript-Ansatz wie zuvor gezeigt wider, wird jedoch auf die tatsächlichen Dokumentwerte in der Aggregationspipeline angewendet. Sie werden auch den anderen "Trick" dort bemerken, wo wir ein $add anwenden Operation mit einer anderen Darstellung eines BSON-Datums ab Epoche ( oder 0 Millisekunden ), bei der die "Addition" eines BSON-Datums zu einem "numerischen" Wert ein "BSON-Datum" zurückgibt, das die Millisekunden darstellt, die als Eingabe angegeben wurden.

Die andere Überlegung im aufgelisteten Code ist natürlich der tatsächliche "Offset" von UTC, der die numerischen Werte anpasst, um sicherzustellen, dass die "Rundung" für die aktuelle Zeitzone stattfindet. Dies wird in einer Funktion implementiert, die auf der früheren Beschreibung zum Ermitteln, wo die verschiedenen Offsets auftreten, basiert und ein Format zurückgibt, das in einem Aggregations-Pipeline-Ausdruck verwendet werden kann, indem die Eingabedaten verglichen und der korrekte Offset zurückgegeben werden.

Mit der vollständigen Erweiterung aller Details, einschließlich der Generierung der Handhabung dieser unterschiedlichen "Sommerzeit"-Zeitverschiebungen, würde dies dann wie folgt aussehen:

[
  {
    "$match": {
      "createdAt": {
        "$gte": "2016-12-31T13:00:00.000Z",
        "$lt": "2017-12-31T13:00:00.000Z"
      }
    }
  },
  {
    "$group": {
      "_id": {
        "$add": [
          {
            "$subtract": [
              {
                "$subtract": [
                  {
                    "$subtract": [
                      "$createdAt",
                      "1970-01-01T00:00:00.000Z"
                    ]
                  },
                  {
                    "$switch": {
                      "branches": [
                        {
                          "case": {
                            "$and": [
                              {
                                "$gte": [
                                  "$createdAt",
                                  "2016-12-31T13:00:00.000Z"
                                ]
                              },
                              {
                                "$lt": [
                                  "$createdAt",
                                  "2017-04-01T16:00:00.000Z"
                                ]
                              }
                            ]
                          },
                          "then": -39600000
                        },
                        {
                          "case": {
                            "$and": [
                              {
                                "$gte": [
                                  "$createdAt",
                                  "2017-04-01T16:00:00.000Z"
                                ]
                              },
                              {
                                "$lt": [
                                  "$createdAt",
                                  "2017-09-30T16:00:00.000Z"
                                ]
                              }
                            ]
                          },
                          "then": -36000000
                        },
                        {
                          "case": {
                            "$and": [
                              {
                                "$gte": [
                                  "$createdAt",
                                  "2017-09-30T16:00:00.000Z"
                                ]
                              },
                              {
                                "$lt": [
                                  "$createdAt",
                                  "2017-12-31T13:00:00.000Z"
                                ]
                              }
                            ]
                          },
                          "then": -39600000
                        }
                      ]
                    }
                  }
                ]
              },
              {
                "$mod": [
                  {
                    "$subtract": [
                      {
                        "$subtract": [
                          "$createdAt",
                          "1970-01-01T00:00:00.000Z"
                        ]
                      },
                      {
                        "$switch": {
                          "branches": [
                            {
                              "case": {
                                "$and": [
                                  {
                                    "$gte": [
                                      "$createdAt",
                                      "2016-12-31T13:00:00.000Z"
                                    ]
                                  },
                                  {
                                    "$lt": [
                                      "$createdAt",
                                      "2017-04-01T16:00:00.000Z"
                                    ]
                                  }
                                ]
                              },
                              "then": -39600000
                            },
                            {
                              "case": {
                                "$and": [
                                  {
                                    "$gte": [
                                      "$createdAt",
                                      "2017-04-01T16:00:00.000Z"
                                    ]
                                  },
                                  {
                                    "$lt": [
                                      "$createdAt",
                                      "2017-09-30T16:00:00.000Z"
                                    ]
                                  }
                                ]
                              },
                              "then": -36000000
                            },
                            {
                              "case": {
                                "$and": [
                                  {
                                    "$gte": [
                                      "$createdAt",
                                      "2017-09-30T16:00:00.000Z"
                                    ]
                                  },
                                  {
                                    "$lt": [
                                      "$createdAt",
                                      "2017-12-31T13:00:00.000Z"
                                    ]
                                  }
                                ]
                              },
                              "then": -39600000
                            }
                          ]
                        }
                      }
                    ]
                  },
                  86400000
                ]
              }
            ]
          },
          "1970-01-01T00:00:00.000Z"
        ]
      },
      "amount": {
        "$sum": "$amount"
      }
    }
  },
  {
    "$addFields": {
      "_id": {
        "$add": [
          "$_id",
          {
            "$switch": {
              "branches": [
                {
                  "case": {
                    "$and": [
                      {
                        "$gte": [
                          "$_id",
                          "2017-01-01T00:00:00.000Z"
                        ]
                      },
                      {
                        "$lt": [
                          "$_id",
                          "2017-04-02T03:00:00.000Z"
                        ]
                      }
                    ]
                  },
                  "then": -39600000
                },
                {
                  "case": {
                    "$and": [
                      {
                        "$gte": [
                          "$_id",
                          "2017-04-02T02:00:00.000Z"
                        ]
                      },
                      {
                        "$lt": [
                          "$_id",
                          "2017-10-01T02:00:00.000Z"
                        ]
                      }
                    ]
                  },
                  "then": -36000000
                },
                {
                  "case": {
                    "$and": [
                      {
                        "$gte": [
                          "$_id",
                          "2017-10-01T03:00:00.000Z"
                        ]
                      },
                      {
                        "$lt": [
                          "$_id",
                          "2018-01-01T00:00:00.000Z"
                        ]
                      }
                    ]
                  },
                  "then": -39600000
                }
              ]
            }
          }
        ]
      }
    }
  },
  {
    "$sort": {
      "_id": 1
    }
  }
]

Diese Erweiterung verwendet den $switch -Anweisung, um die Datumsbereiche als Bedingungen für die Rückgabe der angegebenen Offset-Werte anzuwenden. Dies ist die bequemste Form seit den "branches" -Argument entspricht direkt einem "Array", das die bequemste Ausgabe der "Ranges" ist, die durch Untersuchung des untils bestimmt werden die die Offset-"Cut-Points" für die angegebene Zeitzone im angegebenen Datumsbereich der Abfrage darstellen.

Es ist möglich, dieselbe Logik in früheren Versionen von MongoDB anzuwenden, indem eine „verschachtelte“ Implementierung von $cond verwendet wird stattdessen, aber es ist ein wenig chaotischer zu implementieren, also verwenden wir hier einfach die bequemste Methode zur Implementierung.

Sobald alle diese Bedingungen angewendet wurden, sind die "aggregierten" Daten tatsächlich diejenigen, die die "lokale" Zeit darstellen, wie sie durch das angegebene locale definiert ist . Dies bringt uns tatsächlich zur letzten Aggregationsstufe und dem Grund, warum sie dort ist, sowie zur späteren Handhabung, wie in der Auflistung gezeigt.

Endergebnisse

Ich habe bereits erwähnt, dass die allgemeine Empfehlung lautet, dass die "Ausgabe" immer noch die Datumswerte im UTC-Format mit mindestens einer Beschreibung zurückgeben sollte, und deshalb tut die Pipeline hier genau das, indem sie zuerst "von" UTC in lokal umwandelt Anwenden des Offsets beim "Runden", aber dann werden die endgültigen Zahlen "nach der Gruppierung" um denselben Offset zurückgesetzt, der für die "gerundeten" Datumswerte gilt.

Das Listing hier gibt hier "drei" verschiedene Ausgabemöglichkeiten als:

// ISO Format string from JSON stringify default
[
  {
    "_id": "2016-12-31T13:00:00.000Z",
    "amount": 2
  },
  {
    "_id": "2017-01-01T13:00:00.000Z",
    "amount": 1
  },
  {
    "_id": "2017-01-02T13:00:00.000Z",
    "amount": 2
  }
]
// Timestamp value - milliseconds from epoch UTC - least space!
[
  {
    "_id": 1483189200000,
    "amount": 2
  },
  {
    "_id": 1483275600000,
    "amount": 1
  },
  {
    "_id": 1483362000000,
    "amount": 2
  }
]

// Force locale format to string via moment .format()
[
  {
    "_id": "2017-01-01T00:00:00+11:00",
    "amount": 2
  },
  {
    "_id": "2017-01-02T00:00:00+11:00",
    "amount": 1
  },
  {
    "_id": "2017-01-03T00:00:00+11:00",
    "amount": 2
  }
]

Die eine Sache, die hier zu beachten ist, ist, dass für einen "Client" wie Angular jedes einzelne dieser Formate von seinem eigenen DatePipe akzeptiert würde, das tatsächlich das "locale-Format" für Sie übernehmen kann. Es kommt aber darauf an, wohin die Daten geliefert werden. "Gute" Bibliotheken werden sich der Verwendung eines UTC-Datums im aktuellen Gebietsschema bewusst sein. Wo dies nicht der Fall ist, müssen Sie möglicherweise selbst "stringifizieren".

Aber es ist eine einfache Sache, und Sie erhalten die meiste Unterstützung dafür, indem Sie eine Bibliothek verwenden, die ihre Manipulation der Ausgabe im Wesentlichen auf einem "gegebenen UTC-Wert" basiert.

Die Hauptsache hier ist, "zu verstehen, was Sie tun", wenn Sie so etwas wie die Aggregation auf eine lokale Zeitzone fragen. Ein solcher Prozess sollte Folgendes berücksichtigen:

  1. Die Daten können und werden oft aus der Perspektive von Personen in verschiedenen Zeitzonen betrachtet.

  2. Die Daten werden in der Regel von Personen in unterschiedlichen Zeitzonen bereitgestellt. In Kombination mit Punkt 1 speichern wir deshalb in UTC.

  3. Zeitzonen unterliegen in vielen Zeitzonen der Welt häufig einem sich ändernden "Offset" von der "Daylight Savings Time", und Sie sollten dies bei der Analyse und Verarbeitung der Daten berücksichtigen.

  4. Ungeachtet der Aggregationsintervalle „sollte“ die Ausgabe tatsächlich in UTC bleiben, wenn auch angepasst, um gemäß dem bereitgestellten Gebietsschema ein Intervall zu aggregieren. Dadurch kann die Präsentation an eine "Client"-Funktion delegiert werden, so wie es sein sollte.

Solange Sie diese Dinge im Hinterkopf behalten und wie in der Auflistung hier gezeigt anwenden, tun Sie alles richtig, um mit der Aggregation von Daten und sogar der allgemeinen Speicherung in Bezug auf ein bestimmtes Gebietsschema umzugehen.

Sie "sollten" dies also tun, und was Sie "nicht" tun sollten, ist aufzugeben und einfach das "Gebietsschemadatum" als Zeichenfolge zu speichern. Das wäre, wie beschrieben, ein sehr falscher Ansatz und verursacht nur weitere Probleme für Ihre Anwendung.

HINWEIS :Das einzige Thema, das ich hier überhaupt nicht anspreche, ist die Zusammenfassung zu einem "Monat" ( oder tatsächlich "Jahr" ) Intervall. "Monate" sind die mathematische Anomalie im gesamten Prozess, da die Anzahl der Tage immer variiert und daher eine ganz andere Logik erfordert, um angewendet zu werden. Das allein zu beschreiben ist mindestens so lang wie dieser Beitrag und wäre daher ein anderes Thema. Für allgemeine Minuten, Stunden und Tage, was der übliche Fall ist, ist die Mathematik hier "gut genug" für diese Fälle.

Vollständige Liste

Dies dient als "Demonstration" zum Basteln. Es verwendet die erforderliche Funktion, um die einzuschließenden Offset-Daten und -Werte zu extrahieren, und führt eine Aggregationspipeline über die bereitgestellten Daten aus.

Sie können hier alles ändern, beginnen aber wahrscheinlich mit dem locale und interval Parameter, und fügen Sie dann möglicherweise andere Daten und einen anderen start hinzu und end Termine für die Abfrage. Der Rest des Codes muss jedoch nicht geändert werden, um einfach Änderungen an diesen Werten vorzunehmen, und kann daher die Verwendung unterschiedlicher Intervalle (z. B. 1 hour) demonstrieren wie in der Frage gestellt) und verschiedene Gebietsschemata.

Wenn Sie beispielsweise gültige Daten liefern, die tatsächlich eine Aggregation in einem "1-Stunden-Intervall" erfordern würden, würde die Zeile in der Auflistung wie folgt geändert:

const interval = moment.duration(1,'hour').asMilliseconds();

Um einen Millisekundenwert für das Aggregationsintervall zu definieren, wie es für die Aggregationsvorgänge erforderlich ist, die an den Daten ausgeführt werden.

const moment = require('moment-timezone'),
      mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.Promise = global.Promise;
mongoose.set('debug',true);

const uri = 'mongodb://localhost/test',
      options = { useMongoClient: true };

const locale = 'Australia/Sydney';
const interval = moment.duration(1,'day').asMilliseconds();

const reportSchema = new Schema({
  createdAt: Date,
  amount: Number
});

const Report = mongoose.model('Report', reportSchema);

function log(data) {
  console.log(JSON.stringify(data,undefined,2))
}

function switchOffset(start,end,field,reverseOffset) {

  let branches = [{ start, end }]

  const zone = moment.tz.zone(locale);
  if ( zone.hasOwnProperty('untils') ) {
    let between = zone.untils.filter( u =>
      u >= start.valueOf() && u < end.valueOf()
    );
    if ( between.length > 0 )
      branches = between
        .map( d => moment.tz(d, locale) )
        .reduce((acc,curr,i,arr) =>
          acc.concat(
            ( i === 0 )
              ? [{ start, end: curr }] : [{ start: acc[i-1].end, end: curr }],
            ( i === arr.length-1 ) ? [{ start: curr, end }] : []
          )
        ,[]);
  }

  log(branches);

  branches = branches.map( d => ({
    case: {
      $and: [
        { $gte: [
          field,
          new Date(
            d.start.valueOf()
            + ((reverseOffset)
              ? moment.duration(d.start.utcOffset(),'minutes').asMilliseconds()
              : 0)
          )
        ]},
        { $lt: [
          field,
          new Date(
            d.end.valueOf()
            + ((reverseOffset)
              ? moment.duration(d.start.utcOffset(),'minutes').asMilliseconds()
              : 0)
          )
        ]}
      ]
    },
    then: -1 * moment.duration(d.start.utcOffset(),'minutes').asMilliseconds()
  }));

  return ({ $switch: { branches } });

}

(async function() {
  try {
    const conn = await mongoose.connect(uri,options);

    // Data cleanup
    await Promise.all(
      Object.keys(conn.models).map( m => conn.models[m].remove({}))
    );

    let inserted = await Report.insertMany([
      { createdAt: moment.tz("2017-01-01",locale), amount: 1 },
      { createdAt: moment.tz("2017-01-01",locale), amount: 1 },
      { createdAt: moment.tz("2017-01-02",locale), amount: 1 },
      { createdAt: moment.tz("2017-01-03",locale), amount: 1 },
      { createdAt: moment.tz("2017-01-03",locale), amount: 1 },
    ]);

    log(inserted);

    const start = moment.tz("2017-01-01", locale)
          end   = moment.tz("2018-01-01", locale)

    let pipeline = [
      { "$match": {
        "createdAt": { "$gte": start.toDate(), "$lt": end.toDate() }
      }},
      { "$group": {
        "_id": {
          "$add": [
            { "$subtract": [
              { "$subtract": [
                { "$subtract": [ "$createdAt", new Date(0) ] },
                switchOffset(start,end,"$createdAt",false)
              ]},
              { "$mod": [
                { "$subtract": [
                  { "$subtract": [ "$createdAt", new Date(0) ] },
                  switchOffset(start,end,"$createdAt",false)
                ]},
                interval
              ]}
            ]},
            new Date(0)
          ]
        },
        "amount": { "$sum": "$amount" }
      }},
      { "$addFields": {
        "_id": {
          "$add": [
            "$_id", switchOffset(start,end,"$_id",true)
          ]
        }
      }},
      { "$sort": { "_id": 1 } }
    ];

    log(pipeline);
    let results = await Report.aggregate(pipeline);

    // log raw Date objects, will stringify as UTC in JSON
    log(results);

    // I like to output timestamp values and let the client format
    results = results.map( d =>
      Object.assign(d, { _id: d._id.valueOf() })
    );
    log(results);

    // Or use moment to format the output for locale as a string
    results = results.map( d =>
      Object.assign(d, { _id: moment.tz(d._id, locale).format() } )
    );
    log(results);

  } catch(e) {
    console.error(e);
  } finally {
    mongoose.disconnect();
  }
})()