복붙노트

[MONGODB] MongoDB의에서 현지 시간대와 날짜 별 그룹

MONGODB

MongoDB의에서 현지 시간대와 날짜 별 그룹

나는 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;
            });

문제는 ISO 시간대에 MongoDB를 저장 날짜입니다. 나는 영역 차트를 표시하기위한 데이터를해야합니다.

나는 현지 시간대와 날짜별로 그룹화하려는. 때 그룹에 의해 날짜에 타임 오프셋 추가 할 수있는 방법은 무엇입니까?

해결법

  1. ==============================

    1.그래서이 짧은 대답과도 같은 긴 대답이있다. 기본 케이스는 날짜가 대신 객체 대신에 "필요"를 대신 오히려 싶다 "날짜 집계 사업자"및 중 하나를 사용하여 실제로 "수학을"는 것이다. 여기에 주요 점은 주어진 지역 시간대에 대한 UTC로부터의 오프셋 (offset)하고 필요한 간격 다음 "라운드"값을 조정하는 것입니다.

    그래서이 짧은 대답과도 같은 긴 대답이있다. 기본 케이스는 날짜가 대신 객체 대신에 "필요"를 대신 오히려 싶다 "날짜 집계 사업자"및 중 하나를 사용하여 실제로 "수학을"는 것이다. 여기에 주요 점은 주어진 지역 시간대에 대한 UTC로부터의 오프셋 (offset)하고 필요한 간격 다음 "라운드"값을 조정하는 것입니다.

    고려해야 할 "더 이상 대답은"또한 큰 문제는 날짜가 종종 올해의 서로 다른 시간에 UTC로부터의 오프셋 (offset)에 "일광 절약 시간"변경 될 수 있습니다 것을 포함한다. 이러한 통합을 위해 "현지 시간"을 변환 할 때 이러한 변경에 대한 경계가 존재하는 경우, 당신은 정말 고려해야 할이 수단 그래서.

    또 다른 고려 사항은 주어진 간격으로 "총"에 무엇을 상관없이, 출력 값은 적어도 처음 UTC로 나와 "해야한다"고되고도있다. 이 디스플레이부터 정말 "클라이언트 기능"이며, 후술하는 바와 같이, 클라이언트 인터페이스는 일반적으로는 사실에 공급 하였다는 전제를 기반으로합니다 본 로케일로 표시하는 방법이있을 것이다 "로케일"좋은 방법입니다 UTC와 같은 데이터.

    이 요구 사항이 해결 될 것을 일반적으로 큰 문제이다. 간격을 "반올림"날짜에 대한 일반적인 수학은 간단한 부분이지만, 당신이 그런 경계를 적용 할 때 알고에 적용 할 수있는 실제 수학이 없다, 그리고 규칙이 모두 다르 종종 매년 변경합니다.

    는 "라이브러리"에 제공하고, 자바 스크립트 플랫폼에 대한 저자의 의견에 여기에 최선의 선택이 중요 "timezeone"우리가 원하는 기능을 모두 포함 moment.js의 "상위는"기본적으로 순간 시간대 인 곳이다 그래서 쓰다.

    순간 시간대는 기본적으로 각 로케일 시간대 등을 위해 이러한 구조를 정의한다 :

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

    어디 물론 객체는 실제로 기록 된 untils 및 오프셋 속성에 대해 훨씬 더 크다. 그러나 그것은 당신이 실제로 존 주어진 일광 절약 변경 오프셋에 변화가 있는지 확인하기 위해 액세스에 필요한 데이터입니다.

    나중에 코드 목록의이 블록은 우리가 기본적으로있는 경우, 일광 절약 경계가 교차되는 범위의 시작과 끝 값을 주어진 결정하는 데 사용할 것입니다 :

      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의 전체를 보면이의 출력이 될 것 로케일 :

    [
      {
        "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"
      }
    ]
    

    이는 기본적으로 다음 그 제 2 시퀀스 사이의 날짜 10시간로 변경하고 11시간 것 오프셋 기간의 제 1 시퀀스 사이 연말 및 간격에 피복 위로 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
    

    이것은 우리가 또한 빼고 $를 사용하여 집계 파이프 라인에 적용 할 필요가 일반 수학이다 위에 표시 같은 수학 연산에 사용되는 집계 표현식 $ 모드 작업.

    집계 파이프 라인의 일반적인 구조는 다음입니다 :

        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 객체의 변환이다. 우리는 "숫자"양식을해야하고,이 작업을 수행하는 것은 우리가 그들 사이의 숫자 차이를 산출하는 서로 하나 BSON 날짜를 빼기 수학의 트릭이다. 이것은이 문이 바로 이러한 작업을 수행하는 것입니다 :

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

    이제 우리가 처리 할 수있는 숫자 값을 가지고, 우리는 모듈을 적용하고 그것을 "라운드"를 위해 날짜의 숫자 표현에서 그 뺄 수 있습니다. 이것의 "직선"표현이 같다 그래서 :

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

    어떤 거울 앞에 나온하지만 집계 파이프 라인의 실제 문서 값에 적용되는 동일한 자바 스크립트 수학 접근 방식을. 또한 "숫자"값에 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
        }
      }
    ]
    

    그 확장은 주어진 오프셋 값을 반환 할 때까지 조건으로 날짜 범위를 적용하기 위해 $ 스위치 문을 사용하고 있습니다. 이것은 "분기"부터 가장 편리한 형태 인자 않는 주어진 시간대에 대한 오프셋 "커트 포인트"를 나타내는 untils 검사에 의해 결정되는 "범위"의 가장 편리한 출력 인 "배열"직접 대응 제공된 날짜에 쿼리의 범위.

    대신 $ 콘드의 "중첩"구현을 사용하여 MongoDB를 이전 버전의 같은 논리를 적용 할 수 있지만 우리는 여기 구현에 가장 편리한 방법을 사용하고 그래서, 구현하는 조금 지저분하다.

    그 모든 조건이 적용되면, "집계"날짜 실제로 제공된 로케일에 의해 정의 된 "로컬"시간을 나타내는 것들이다. 이것은 실제로 최종 집계 단계가 무엇인지 우리에게 가져다, 그것은 나중에 목록에서 입증 된 바와 같이 처리뿐만 아니라이 이유.

    나는 일반적인 권장 사항은 "출력"아직 여기 파이프 라인이 처음으로 지역에 UTC "에서"변환하여 일을 정확히입니다 때문에 적어도 몇 가지 설명의 UTC 형식으로 날짜 값을 반환하고해야한다는 것을 앞에서 언급 한 "반올림"때 옵셋 적용하지만, 그 "원형"날짜 값을 적용함으로써 동일한 오프셋이다 재조정 위로 "그룹화 한 후"최종 번호.

    여기에 목록 여기에 "세"다른 출력 가능성으로 제공합니다 :

    // 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
      }
    ]
    

    여기서주의의 한 가지는 "클라이언트"에 대한 같은 각도로 그 형식의 모든 하나 하나가 실제로 당신을 위해 "로케일 형식을"할 수있는 그것의 자신의 DatePipe에 의해 허용 될 수 있다는 점이다. 그러나 데이터가 공급되는 위치에 따라 달라집니다. "좋은"라이브러리는 현재 로케일의 UTC 날짜를 사용하여 인식합니다. 그런 경우가없는 경우에는, 당신은 자신을 "캐릭터 라인 화"해야 할 수도 있습니다.

    그러나 그것은 간단한 일이고, 당신은 이는 "주어진 UTC 값"본질적으로베이스 출력의 그것의 조작 라이브러리를 사용하여이에 대한 대부분의 지원을받을.

    여기에서 중요한 것은 로컬 시간대로 집계 등의 일을 부탁 할 때 "당신이 무엇을하고 있는지 이해"하는 것입니다. 이러한 과정을 고려해야합니다 :

    만큼 당신이 마음에 그런 것들을 유지하고 바로 여기에 목록을 보여줍니다처럼 적용으로, 당신은 지정된 로케일에 대한 날짜의 집계 심지어 일반 저장 처리에 대한 모든에게 옳은 일을하고있다.

    당신이이 일을 "해야"그래서, 그리고 무슨 일을 할 "하지 말아야하는 것은"입니다 포기하고 단순히 문자열로 "로케일 날짜를"저장. 바와 같이, 그것은 매우 잘못된 접근하고 응용 프로그램에 대한 것도 있지만 더 문제가 발생하지 않을 것이다.

    이 어설프게에 "데모"의 역할을한다. 이는 날짜와 오프셋 값을 추출하는 데 필요한 기능을 포함 할를 사용하여 제공된 데이터를 통해 집계를 실행하는 파이프 라인.

    여기에 아무것도 변경할 수 있지만, 아마도 로케일과 간격 매개 변수와 함께 시작됩니다, 그리고 아마 쿼리에 대해 서로 다른 데이터와 다른 시작과 끝 날짜를 추가합니다. 그러나 나머지 코드는 단순히 그 값 중 하나를 변경할로 변경 될 필요가 없다, 따라서 다른 간격 (질문에 질문으로 같은 1 시간)과 다른 로케일을 사용하여 입증 할 수 있습니다.

    예를 들어, 실제로 다음 목록의 라인은 다음과 같이 변경됩니다 "1 개 시간 간격"에서 집계를 필요로 유효한 데이터를 공급 번 :

    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();
      }
    })()
    
  2. from https://stackoverflow.com/questions/45038711/group-by-date-with-local-time-zone-in-mongodb by cc-by-sa and MIT license