"स्थानीय तिथियों" से निपटने की सामान्य समस्या
तो इसका एक छोटा सा जवाब भी है और एक लंबा जवाब भी। मूल मामला यह है कि किसी भी "डेट एग्रीगेशन ऑपरेटर्स" का उपयोग करने के बजाय आप इसके बजाय डेट ऑब्जेक्ट्स पर वास्तव में "गणित" करना चाहते हैं और "जरूरत" करना चाहते हैं। यहां प्राथमिक बात यह है कि दिए गए स्थानीय समय क्षेत्र के लिए यूटीसी से ऑफसेट द्वारा मूल्यों को समायोजित करें और फिर आवश्यक अंतराल पर "गोल" करें।
"बहुत लंबा उत्तर" और विचार करने की मुख्य समस्या में यह शामिल है कि तिथियां अक्सर वर्ष के अलग-अलग समय पर यूटीसी से ऑफसेट में "डेलाइट सेविंग टाइम" परिवर्तनों के अधीन होती हैं। तो इसका मतलब है कि इस तरह के एकत्रीकरण उद्देश्यों के लिए "स्थानीय समय" में परिवर्तित करते समय, आपको वास्तव में विचार करना चाहिए कि ऐसे परिवर्तनों की सीमाएं कहां मौजूद हैं।
एक और विचार यह भी है, कि कोई फर्क नहीं पड़ता कि आप किसी दिए गए अंतराल पर "एग्रीगेट" करने के लिए क्या करते हैं, आउटपुट मान "चाहिए" कम से कम शुरू में यूटीसी के रूप में सामने आते हैं। यह अच्छा अभ्यास है क्योंकि "लोकेल" को प्रदर्शित करना वास्तव में एक "क्लाइंट फ़ंक्शन" है, और जैसा कि बाद में वर्णित किया गया है, क्लाइंट इंटरफेस में आमतौर पर वर्तमान लोकेल में प्रदर्शित करने का एक तरीका होगा जो इस आधार पर होगा कि यह वास्तव में खिलाया गया था यूटीसी के रूप में डेटा।
लोकेल ऑफ़सेट और डेलाइट सेविंग्स निर्धारित करना
यह आम तौर पर मुख्य समस्या है जिसे हल करने की आवश्यकता है। एक अंतराल के लिए एक तिथि को "गोल" करने के लिए सामान्य गणित सरल हिस्सा है, लेकिन कोई वास्तविक गणित नहीं है जिसे आप यह जानने के लिए लागू कर सकते हैं कि ऐसी सीमाएं कब लागू होती हैं, और नियम हर लोकेल में और अक्सर हर साल बदलते हैं।
तो यह वह जगह है जहां एक "लाइब्रेरी" आती है, और जावास्क्रिप्ट प्लेटफॉर्म के लिए लेखकों की राय में यहां सबसे अच्छा विकल्प मोमेंट-टाइमज़ोन है, जो मूल रूप से पल का "सुपरसेट" है। सभी महत्वपूर्ण "टाइमज़ोन" सुविधाओं सहित हम चाहते हैं उपयोग करने के लिए।
मोमेंट टाइमज़ोन मूल रूप से प्रत्येक लोकेल टाइमज़ोन के लिए इस तरह की संरचना को परिभाषित करता है:
{
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
. के संबंध में बड़ा और offsets
संपत्ति वास्तव में दर्ज की गई। लेकिन यह वह डेटा है जिसे आपको यह देखने के लिए एक्सेस करने की आवश्यकता है कि क्या वास्तव में दिन के उजाले में बचत परिवर्तन वाले क्षेत्र के लिए ऑफसेट में कोई बदलाव है।
बाद की कोड सूची का यह ब्लॉक वह है जिसे हम मूल रूप से निर्धारित करने के लिए उपयोग करते हैं 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 }] : []
)
,[]);
}
Australia/Sydney
. के लिए पूरे 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"
}
]
जो मूल रूप से पता चलता है कि तारीखों के पहले अनुक्रम के बीच ऑफसेट +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 } }
];
यहां आपको जिन मुख्य भागों को समझने की जरूरत है, वह है Date
. से रूपांतरण MongoDB में संग्रहीत वस्तु के रूप में Numeric
आंतरिक टाइमस्टैम्प मान का प्रतिनिधित्व करना। हमें "संख्यात्मक" फॉर्म की आवश्यकता है, और ऐसा करने के लिए गणित की एक चाल है जहां हम एक बीएसओएन तिथि को दूसरे से घटाते हैं जो उनके बीच संख्यात्मक अंतर उत्पन्न करता है। यह कथन ठीक यही करता है:
{ "$subtract": [ "$createdAt", new Date(0) ] }
अब हमारे पास निपटने के लिए एक संख्यात्मक मान है, हम मॉड्यूल को लागू कर सकते हैं और इसे "गोल" करने के लिए तारीख के संख्यात्मक प्रतिनिधित्व से घटा सकते हैं। तो इसका "सीधा" प्रतिनिधित्व इस प्रकार है:
{ "$subtract": [
{ "$subtract": [ "$createdAt", new Date(0) ] },
{ "$mod": [
{ "$subtract": [ "$createdAt", new Date(0) ] },
( 1000 * 60 * 60 * 24 ) // 24 hours
]}
]}
जो पहले दिखाए गए समान जावास्क्रिप्ट गणित दृष्टिकोण को प्रतिबिंबित करता है लेकिन एकत्रीकरण पाइपलाइन में वास्तविक दस्तावेज़ मानों पर लागू होता है। आप वहां अन्य "ट्रिक" भी नोट करेंगे जहां हम एक $add
. लागू करते हैं युग (या 0 मिलीसेकंड) के रूप में एक बीएसओएन तिथि के एक और प्रतिनिधित्व के साथ संचालन जहां एक "संख्यात्मक" मान के लिए बीएसओएन तिथि का "जोड़", मिलीसेकंड का प्रतिनिधित्व करने वाली "बीएसओएन तिथि" देता है जो इसे इनपुट के रूप में दिया गया था।
बेशक सूचीबद्ध कोड में अन्य विचार यह यूटीसी से वास्तविक "ऑफसेट" है जो वर्तमान समय क्षेत्र के लिए "गोल" सुनिश्चित करने के लिए संख्यात्मक मानों को समायोजित कर रहा है। यह एक फ़ंक्शन में लागू किया जाता है, जहां अलग-अलग ऑफ़सेट होते हैं, और इनपुट तिथियों की तुलना करके और सही ऑफ़सेट लौटाकर एक एग्रीगेशन पाइपलाइन एक्सप्रेशन में प्रयोग योग्य के रूप में एक प्रारूप देता है।
उन अलग-अलग "डेलाइट सेविंग्स" टाइम ऑफ़सेट को संभालने की पीढ़ी सहित सभी विवरणों के पूर्ण विस्तार के साथ, ऐसा होगा:
[
{
"$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"
. के बाद से यह सबसे सुविधाजनक फ़ॉर्म है तर्क सीधे "सरणी" से मेल खाता है, जो कि untils
की जांच द्वारा निर्धारित "श्रेणियों" का सबसे सुविधाजनक आउटपुट है क्वेरी की आपूर्ति की गई दिनांक सीमा पर दिए गए समय क्षेत्र के लिए ऑफ़सेट "कट-पॉइंट" का प्रतिनिधित्व करना।
$cond
के "नेस्टेड" कार्यान्वयन का उपयोग करके MongoDB के पुराने संस्करणों में समान तर्क लागू करना संभव है इसके बजाय, लेकिन इसे लागू करना थोड़ा गड़बड़ है, इसलिए हम यहां कार्यान्वयन में सबसे सुविधाजनक विधि का उपयोग कर रहे हैं।
एक बार उन सभी शर्तों को लागू करने के बाद, "समेकित" तिथियां वास्तव में "स्थानीय" समय का प्रतिनिधित्व करती हैं, जैसा कि आपूर्ति किए गए locale
द्वारा परिभाषित किया गया है। . यह वास्तव में हमें लाता है कि अंतिम एकत्रीकरण चरण क्या है, और इसके होने का कारण और साथ ही बाद की हैंडलिंग जैसा कि लिस्टिंग में दिखाया गया है।
अंतिम परिणाम
मैंने पहले उल्लेख किया था कि सामान्य सिफारिश यह है कि "आउटपुट" को अभी भी कम से कम कुछ विवरण के यूटीसी प्रारूप में दिनांक मानों को वापस करना चाहिए, और इसलिए यहां पाइपलाइन पहले "यूटीसी" से स्थानीय में परिवर्तित करके कर रही है। "राउंडिंग" के दौरान ऑफ़सेट लागू करना, लेकिन फिर "ग्रुपिंग के बाद" अंतिम नंबरों को उसी ऑफ़सेट द्वारा फिर से समायोजित किया जाता है जो "गोल" दिनांक मानों पर लागू होता है।
यहाँ लिस्टिंग "तीन" विभिन्न आउटपुट संभावनाओं को यहाँ देती है:
// 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
}
]
यहां ध्यान देने वाली बात यह है कि एंगुलर जैसे "क्लाइंट" के लिए, उन प्रारूपों में से प्रत्येक को अपने स्वयं के डेटपाइप द्वारा स्वीकार किया जाएगा जो वास्तव में आपके लिए "लोकेल प्रारूप" कर सकता है। लेकिन यह इस बात पर निर्भर करता है कि डेटा की आपूर्ति कहां की जाती है। "अच्छे" पुस्तकालयों को वर्तमान लोकेल में यूटीसी तिथि का उपयोग करने के बारे में पता होगा। जहां ऐसा नहीं है, वहां आपको खुद को "कड़ाई" देने की आवश्यकता हो सकती है।
लेकिन यह एक साधारण सी बात है, और इसके लिए आपको एक पुस्तकालय का उपयोग करके सबसे अधिक समर्थन मिलता है जो अनिवार्य रूप से "दिए गए यूटीसी मूल्य" से आउटपुट के हेरफेर को आधार बनाता है।
यहां मुख्य बात यह है कि "यह समझना कि आप क्या कर रहे हैं" जब आप किसी स्थानीय समय क्षेत्र को एकत्रित करने जैसी कोई चीज पूछते हैं। ऐसी प्रक्रिया पर विचार करना चाहिए:
-
डेटा को अलग-अलग समय क्षेत्रों में लोगों के दृष्टिकोण से देखा जा सकता है और अक्सर देखा जा सकता है।
-
डेटा आम तौर पर अलग-अलग समय क्षेत्रों में लोगों द्वारा प्रदान किया जाता है। बिंदु 1 के साथ, यही कारण है कि हम यूटीसी में स्टोर करते हैं।
-
टाइमज़ोन अक्सर दुनिया के कई टाइमज़ोन में "डेलाइट सेविंग टाइम" से बदलते "ऑफ़सेट" के अधीन होते हैं, और डेटा का विश्लेषण और प्रसंस्करण करते समय आपको इसका हिसाब देना चाहिए।
-
एकत्रीकरण अंतराल के बावजूद, आउटपुट "चाहिए" वास्तव में यूटीसी में रहना चाहिए, यद्यपि प्रदान किए गए स्थान के अनुसार अंतराल पर कुल समायोजित करने के लिए। यह प्रस्तुति को "क्लाइंट" फ़ंक्शन को सौंपने के लिए छोड़ देता है, जैसा कि इसे करना चाहिए।
जब तक आप उन बातों को ध्यान में रखते हैं और ठीक वैसे ही लागू करते हैं जैसे यहां दी गई सूची प्रदर्शित करती है, तो आप किसी दिए गए स्थान के संबंध में तिथियों के एकत्रीकरण और यहां तक कि सामान्य भंडारण से निपटने के लिए सभी सही चीजें कर रहे हैं।
तो आपको ऐसा करना चाहिए, और जो आपको "नहीं करना चाहिए" वह छोड़ रहा है और बस "लोकेल डेट" को एक स्ट्रिंग के रूप में संग्रहीत कर रहा है। जैसा कि बताया गया है, यह एक बहुत ही गलत तरीका होगा और आपके आवेदन के लिए और समस्याओं के अलावा और कुछ नहीं करेगा।
<ब्लॉककोट>नोट :जिस एक विषय पर मैं यहां बिल्कुल भी ध्यान नहीं देता, वह है एक "महीना" (या वास्तव में "वर्ष") मध्यान्तर। "महीने" पूरी प्रक्रिया में गणितीय विसंगति है क्योंकि दिनों की संख्या हमेशा बदलती रहती है और इस प्रकार इसे लागू करने के लिए तर्क के एक पूरे सेट की आवश्यकता होती है। यह वर्णन करते हुए कि अकेले कम से कम इस पद के रूप में लंबे समय तक है, और इसलिए एक और विषय होगा। सामान्य मिनटों, घंटों और दिनों के लिए जो सामान्य मामला है, उन मामलों के लिए यहां गणित "काफी अच्छा" है।
पूरी सूची
यह छेड़छाड़ करने के लिए एक "प्रदर्शन" के रूप में कार्य करता है। यह ऑफसेट तिथियों और मूल्यों को शामिल करने के लिए आवश्यक फ़ंक्शन को नियोजित करता है और आपूर्ति किए गए डेटा पर एक एकत्रीकरण पाइपलाइन चलाता है।
आप यहां कुछ भी बदल सकते हैं, लेकिन संभवत:locale
. से शुरू करेंगे और interval
पैरामीटर, और फिर शायद अलग-अलग डेटा जोड़ें और अलग-अलग start
और end
क्वेरी के लिए तारीखें। लेकिन शेष कोड को केवल उन मानों में से किसी में परिवर्तन करने के लिए बदलने की आवश्यकता नहीं है, और इसलिए विभिन्न अंतरालों (जैसे 1 hour
का उपयोग करके प्रदर्शित किया जा सकता है। जैसा कि प्रश्न में पूछा गया है) और विभिन्न स्थान।
उदाहरण के लिए, एक बार वैध डेटा की आपूर्ति करने के बाद जिसे वास्तव में "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();
}
})()