javascript - 按日期与MongoDB中的本地时区分组

标签 javascript node.js mongodb mongoose aggregation-framework

我是mongodb的新手。以下是我的查询。

Model.aggregate()
            .match({ 'activationId': activationId, "t": { "$gte": new Date(fromTime), "$lt": new Date(toTime) } })
            .group({ '_id': { 'date': { $dateToString: { format: "%Y-%m-%d %H", date: "$datefield" } } }, uniqueCount: { $addToSet: "$mac" } })
            .project({ "date": 1, "month": 1, "hour": 1, uniqueMacCount: { $size: "$uniqueCount" } })
            .exec()
            .then(function (docs) {
                return docs;
            });

问题是mongodb将日期存储在iso时区中。我需要这些数据来显示面积图。

我想按日期与本地时区分组。分组方式时,有什么方法可以将timeoffset添加到日期中?

最佳答案

处理“本地日期”的一般问题

因此,对此有一个简短的答案,也有一个长长的答案。基本情况是,您不想使用任何"date aggregation operators",而是想要并且“需要”实际上是对日期对象“算术”。在这里,主要的事情是根据给定的本地时区通过与UTC的偏移来调整值,然后“舍入”到所需的间隔。

“更长的答案”也是要考虑的主要问题,它涉及的日期通常是一年中不同时间与UTC的偏移量中的“夏令时”变化。因此,这意味着在出于此类汇总目的而转换为“本地时间”时,您确实应该考虑此类更改的边界存在于何处。

还有另一个考虑因素,就是无论您做什么以给定的时间间隔“聚合”,输出值“应该”至少应最初以UTC的形式出现。这是一个好习惯,因为显示到“语言环境”实际上是一个“客户端功能”,并且如稍后所述,客户端界面通常会在当前语言环境中具有一种显示方式,该方式将基于实际上已被喂入的前提数据作为UTC。

确定语言环境的偏移量和夏时制

这通常是需要解决的主要问题。将日期“四舍五入”为间隔的通用数学运算很简单,但是您没有真正的数学可用于了解何时应用此类边界,并且规则在每个区域(通常每年)都在变化。

因此,这就是“库”的出现,在作者看来,对于JavaScript平台,最好的选择是moment-timezone,它基本上是moment.js的“超集”,包括我们要使用的所有重要的“timezeone”功能。

Moment时区基本上将每个区域时区的结构定义为:

{
    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
}

当然,相对于实际记录的untilsoffsets属性,对象的大了大。但是,这是您需要访问的数据,以便在夏时制更改的情况下,查看某个区域的偏移量是否确实有更改。

后面的代码 list 的这一部分基本上是我们用来确定给定范围的startend值的地方,该界限跨越了夏时制边界(如果有):
  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 }] : []
          )
        ,[]);
  }

在整个2017年的Australia/Sydney语言环境中,其输出为:
[
  {
    "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"
  }
]

这基本上表明,在第一个日期序列之间,偏移量将为+11小时,然后在第二个序列中的日期之间偏移为+10小时,然后切换到+11小时,以覆盖到年底和指定范围。

然后,需要将此逻辑转换为MongoDB将其理解为聚合管道一部分的结构。

应用数学

此处,用于汇总到任何“取整日期间隔”的数学原理基本上取决于使用所表示日期的毫秒值,该值将“取整”至最接近的数字(表示所需的“间隔”)。

实际上,您可以通过找到应用于所需间隔的当前值的“模”或“余数”来执行此操作。然后,您从当前值中“减去”剩余的值,该值将以最近的间隔返回一个值。

例如,给定当前日期:
  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

这是我们还需要使用 $subtract $mod 操作在聚合管道中应用的常规数学,这是用于上述相同数学运算的聚合表达式。

聚合管道的一般结构如下:
    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 } }
    ];

您需要了解的主要部分是从存储在MongoDB中的Date对象到表示内部时间戳值的Numeric的转换。我们需要“数字”形式,这是一种数学技巧,其中我们从另一个中减去一个BSON日期,从而得出它们之间的数值差异。这正是该语句的作用:
{ "$subtract": [ "$createdAt", new Date(0) ] }

现在我们有一个要处理的数值,我们可以应用模并从日期的数值表示中减去它,以便对其进行“四舍五入”。因此,此“直接”表示形式如下:
{ "$subtract": [
  { "$subtract": [ "$createdAt", new Date(0) ] },
  { "$mod": [
    { "$subtract": [ "$createdAt", new Date(0) ] },
    ( 1000 * 60 * 60 * 24 ) // 24 hours
  ]}
]}

它反射(reflect)了与前面所示相同的JavaScript数学方法,但适用于聚合管道中的实际文档值。您还将注意到另一个“窍门”,在那里我们应用 $add 操作,并以另一个BSON日期(从纪元(或0毫秒)开始)表示,其中将BSON日期的“加法”添加到“数字”值,返回“BSON日期”,代表输入的毫秒数。

当然,所列代码中的其他考虑因素是它与UTC的实际“偏移”,它正在调整数值,以确保在当前时区发生“舍入”。这是在一个功能的基础上实现的,该功能基于查找发生不同偏移的较早描述,并通过比较输入日期并返回正确的偏移来返回在聚合管道表达式中可用的格式。

随着所有细节的全面扩展,包括处理这些不同的“夏令时”时间偏移,将产生以下结果:
[
  {
    "$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
    }
  }
]

该扩展使用 $switch 语句,以便将日期范围作为条件何时返回给定的偏移值。这是最方便的形式,因为"branches"参数确实直接对应于“数组”,这是“范围”的最方便输出,该范围是通过检查表示给定时区上偏移“cut-points”的untils所确定的。提供的查询日期范围。

可以使用 $cond 的“嵌套”实现在MongoDB的早期版本中应用相同的逻辑,但是实现起来有点麻烦,因此我们在这里仅使用最方便的实现方法。

一旦应用了所有这些条件,“汇总”的日期实际上就是表示由提供的locale定义的“本地”时间的日期。实际上,这使我们进入了最终的聚合阶段,以及进入该阶段的原因以及 list 中演示的后续处理。

最终结果

我之前提到过,总体建议是,“输出”仍应至少以某种描述的UTC格式返回日期值,因此这正是管道首先通过将“从” UTC转换为本地而来的方式。在“四舍五入”时应用偏移量,但是“分组后”的最终数字将通过与“四舍五入”日期值相同的偏移量重新调整回去。

这里的 list 给出了“三种”不同的输出可能性,如下所示:
// 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
  }
]

这里要注意的一件事是,对于诸如Angular之类的“客户端”,其自己的DatePipe会接受这些格式中的每一个,而ojit_a可以实际为您执行“语言环境格式”。但这取决于将数据提供到何处。 “良好”库将意识到在当前语言环境中使用UTC日期。如果不是这种情况,那么您可能需要对自己进行“字符串化”。

但这是一件简单的事情,您可以通过使用一个库来获得最大的支持,该库实际上是基于对“给定的UTC值”的输出进行操作的。

这里最主要的是在您询问诸如汇总到本地时区之类的事情时“了解您在做什么”。这样的过程应考虑:
  • 可以并且经常从不同时区的人们的角度查看数据。
  • 数据通常由不同时区的人们提供。结合第1点,这就是我们存储在UTC中的原因。
  • 时区通常在世界许多时区中与“夏时制”相对应的“偏移”有所变化,因此在分析和处理数据时应考虑到这一点。
  • 不管聚合间隔如何,输出“应该”实际上都保留在UTC中,尽管已根据提供的语言环境将其调整为按间隔进行聚合。这样就可以将演示文稿委派给“客户端”功能。

  • 只要牢记这些内容并按照此处的 list 所示进行应用,就可以针对给定的语言环境执行日期汇总甚至常规存储的所有正确操作。

    因此,您“应该”这样做,而“不应该”这样做就是放弃并将“语言环境日期”简单地存储为字符串。如上所述,这将是一种非常不正确的方法,只会给您的应用程序带来更多问题。

    NOTE: The one topic I do not touch on here at all is aggregating to a "month" ( or indeed "year" ) interval. "Months" are the mathematical anomaly in the whole process since the number of days always varies and thus requires a whole other set of logic in order to apply. Describing that alone is at least as long as this post, and therefore would be another subject. For general minutes, hours, and days which is the common case, the math here is "good enough" for those cases.



    完整 list

    这是修补的“演示”。它使用所需的函数来提取要包括的偏移日期和值,并对提供的数据运行汇总管道。

    您可以在此处进行任何更改,但可能以localeinterval参数开头,然后可以为查询添加不同的数据以及不同的startend日期。但是,无需更改其余代码即可简单地对这些值中的任何一个进行更改,因此可以使用不同的时间间隔(例如问题中要求的1 hour)和不同的语言环境进行演示。

    例如,一旦提供了实际上需要以“1小时间隔”聚合的有效数据,则 list 中的行将更改为:
    const interval = moment.duration(1,'hour').asMilliseconds();
    

    为了根据在日期上执行的聚合操作的要求,为聚合间隔定义毫秒值。
    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();
      }
    })()
    

    关于javascript - 按日期与MongoDB中的本地时区分组,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45038711/

    相关文章:

    javascript - 我尝试在弹出窗口打开的情况下设置滚动动画,但它不起作用

    javascript - CSS Flex + 固定尺寸容器高度 + 由于溢出 :hidden 禁用部分绘制的元素

    javascript - 使用 chrome 扩展程序如何将 html 添加到页面 body 标签的正下方?

    javascript - Flash -/路线未加载

    javascript - 在重定向期间将 token 从 API 传递到前端

    node.js - 如何让5个API获取请求并将数据传递到一个 View ?

    node.js - yarn 安装中的意外 token

    mongodb - NodeJS Mongoose 数据库随机访问不返回结果

    java - 使用 Java、Jersey、MongoDB 和 Json 的简单记录器 API

    javascript - 如何使用 Mongoose 只更新 MongoDB 中的一个属性?