YourWorldTime

कूट में समय क्षेत्र: एक व्यावहारिक मार्गदर्शिका

· 6 मिनट पठन

समय क्षेत्र का लगभग हर दोष चार भूलों में से किसी एक तक जाता है। इसके बजाय क्या करना चाहिए — जावास्क्रिप्ट, पाइथन, जावा, Go और SQL में, केवल नियम नहीं बल्कि तर्क के साथ।

उत्पादन में समय क्षेत्र का लगभग हर दोष चार भूलों में से किसी एक तक जाता है: स्थानीय समय संचित करना, क्षेत्र के बजाय अंतर संचित करना, ग़लत परत पर रूपांतरण करना, या हाथ से अंतर-तालिका बनाना। आगे का सब कुछ इन्हीं से बचने से निकलता है।

नियम 1: क्षणों को UTC में संचित कीजिए

क्षण समय-रेखा पर एक बिंदु है। स्थानीय समय दीवार-घड़ी का पाठ है, जिसे कुछ भी अर्थ देने से पहले एक क्षेत्र और एक निःसंदिग्धीकरण नीति चाहिए।

-- अच्छा: एक क्षण
created_at TIMESTAMPTZ NOT NULL

-- बुरा: बिना क्षेत्र की दीवार-घड़ी का पाठ
created_at TIMESTAMP NOT NULL

PostgreSQL का TIMESTAMPTZ क्षेत्र संचित नहीं करता — वह लिखते समय UTC पर सामान्यीकृत करता है और पढ़ते समय रूपांतरित। «यह कब हुआ» के लिए आपको ठीक यही चाहिए।

नियम 2: क्षेत्र-पहचानक संचित कीजिए, अंतर नहीं

यदि आपको जानना है कि कहाँ — किसी आवर्ती आयोजन के लिए, उपयोक्ता की पसंद के लिए, किसी प्रतिष्ठान के खुलने के घंटों के लिए — तो +01:00 नहीं, Europe/London संचित कीजिए।

अंतर एक ही क्षण के बारे में तथ्य है। क्षेत्र नियमों का ऐसा समुच्चय है जो किसी सरकार के मन बदलने को भी झेल जाता है। ब्रिटेन जनवरी में +00:00 पर है और जुलाई में +01:00 पर; इनमें से किसी एक को संचित कर उसे «उपयोक्ता का क्षेत्र» कहना आधे साल तक ग़लत रहता है।

event_start_local  TIMESTAMP   NOT NULL,  -- उन्होंने जो दीवार-घड़ी का समय चुना
event_time_zone    TEXT        NOT NULL,  -- 'Europe/London'
event_start_utc    TIMESTAMPTZ NOT NULL   -- हल किया गया क्षण, नियम बदलें तो पुनर्गणित

आवर्ती आयोजन में स्थानीय समय और क्षेत्र सत्य के स्रोत हैं; UTC का क्षण व्युत्पन्न अनुक्रमणिका है। यदि tzdata बदले — और वह साल में कई बार बदलता है — तो व्युत्पन्न स्तंभ फिर से बना लिया जाता है और उपयोक्ता के 09:00, 09:00 ही रहते हैं।

नियम 3: किनारे पर रूपांतरण कीजिए

व्यापार-तर्क क्षणों में काम करता है। पठनीय स्थानीय समय में रूपांतरण अंतिम संभव क्षण पर होता है, उस परत में जो जानती है कि पढ़ रहा कौन है।

निष्कर्ष और भी अधिक मायने रखता है: परिसेवक के अपने क्षेत्र को कभी व्यवहार में रिसने मत दीजिए। अपने परिनियोजन परिवेश में TZ=UTC नियत कीजिए, और कूट के हर उस मार्ग को दोष मानिए जिसका परिणाम परिसेवक की स्थानिक व्यवस्था पर निर्भर हो। यह विफलताओं का वह वर्ग है जो तभी प्रकट होता है जब कोई दूसरे क्षेत्र में परिनियोजन करता है।

नियम 4: अंतर-तालिका कभी मत बनाइए

हर आधुनिक कार्यकाल-परिवेश IANA का डेटाबेस साथ लाता है। उसका उपयोग कीजिए।

जावास्क्रिप्ट

// किसी क्षेत्र में स्वरूपण
new Intl.DateTimeFormat('en-GB', {
  timeZone: 'Asia/Kathmandu',
  dateStyle: 'medium',
  timeStyle: 'short',
}).format(new Date());

// बिना लाइब्रेरी अंतर निकालना: क्षेत्र की दीवार-घड़ी की UTC से तुलना
function offsetMinutes(timeZone, instant) {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone,
    hourCycle: 'h23',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
  }).formatToParts(new Date(instant));
  const get = (type) => Number(parts.find((p) => p.type === type).value);
  const asUtc = Date.UTC(
    get('year'),
    get('month') - 1,
    get('day'),
    get('hour'),
    get('minute'),
    get('second'),
  );
  return (asUtc - Math.floor(instant / 1000) * 1000) / 60000;
}

hourCycle: 'h23' स्पष्ट रूप से नियत कीजिए। ICU के कुछ संस्करण en-US के अंतर्गत मध्यरात्रि को घंटा 24 बताते हैं, जिससे तारीख़ एक दिन खिसक जाती है और यह ढूँढ़ने में सचमुच कष्टकर दोष है।

पाइथन

from datetime import datetime, timezone
from zoneinfo import ZoneInfo  # 3.9 से मानक लाइब्रेरी

now = datetime.now(timezone.utc)
local = now.astimezone(ZoneInfo("Asia/Kathmandu"))

datetime.utcnow() कभी मत बरतिए। वह ऐसी भोली तिथि लौटाता है जो UTC जैसी दिखती है पर कोई क्षेत्र नहीं रखती, और उसे क्षेत्र-सचेत तिथियों से मिलाने पर सबसे बुरे क्षण अपवाद उठता है। सही आह्वान datetime.now(timezone.utc) है।

जावा

Instant now = Instant.now();
ZonedDateTime local = now.atZone(ZoneId.of("Asia/Kathmandu"));

java.time अच्छी तरह रचित है: समय-रेखा के बिंदुओं के लिए Instant, बिना क्षेत्र की दीवार-घड़ी के पाठों के लिए LocalDateTime, और दोनों साथ के लिए ZonedDateTime। प्रकार-प्रणाली वही भेद बलपूर्वक लागू करती है जिस पर यह मार्गदर्शिका बार-बार ज़ोर देती है।

Go

loc, _ := time.LoadLocation("Asia/Kathmandu")
local := time.Now().In(loc)

LoadLocation तंत्र की tzdata पढ़ता है। ख़ाली कंटेनर में वह होती ही नहीं — उसे द्विआधारी फ़ाइल में जड़ने के लिए _ "time/tzdata" आयात कीजिए, वरना हर क्षेत्र-खोज चुपचाप UTC पर गिर जाएगी।

साल के वे दो दिन

आगे बढ़ाना एक घंटा मिटा देता है; पीछे करना एक घंटा दोहरा देता है। एक नीति चुनिए, उसे लगातार लागू कीजिए, और उपयोक्ता को बताइए कि आपने क्या किया:

  • अनस्तित्व स्थानीय समय (अमेरिका की वसंत तिथि पर 02:30): खाई के आकार जितना आगे बढ़ाइए, ताकि 02:30 बनकर 03:30 हो जाए।
  • संदिग्ध स्थानीय समय (अमेरिका की शरद तिथि पर 01:30): पहला, यानी पूर्ववर्ती अवसर लीजिए।

ये वही नीतियाँ हैं जो यह साइट बरतती है, और ये ECMAScript के Temporal की disambiguation: 'compatible' वाली मूलभूत सेटिंग से मेल खाती हैं — मेल खिलाना सार्थक है ताकि आपका व्यवहार मंच से सहमत रहे।

खाई हमेशा एक घंटे की नहीं होती। लॉर्ड हाउ द्वीप 30 मिनट खिसकता है, इसलिए आकार को मान लेने के बजाय संक्रमण से पढ़िए।

परीक्षण

चार प्रकरण अधिकांश को पकड़ लेते हैं:

  1. आधे घंटे वाला क्षेत्रAsia/Kolkata (+05:30)। पूरे घंटों की मान्यताएँ पकड़ता है।
  2. 45 मिनट वाला क्षेत्रAsia/Kathmandu (+05:45)। आधे घंटे की मान्यताएँ पकड़ता है।
  3. दक्षिणी गोलार्ध का क्षेत्रAustralia/Sydney। «गर्मी माने जून» को पकड़ता है।
  4. लॉर्ड हाउ द्वीपAustralia/Lord_Howe। कूट में जड़े 60 मिनट के खिसकाव को पकड़ता है, और कुछ भी उसे नहीं पकड़ेगा।

जिन क्षेत्रों में आपके उपयोक्ता वास्तव में हैं, उनमें से हर एक के लिए एक आगे बढ़ाने और एक पीछे करने की तिथि जोड़िए, और सतत एकीकरण में tzdata का संस्करण नियत कर दीजिए ताकि डेटाबेस का कोई अद्यतन ग़लत कारण से किसी निर्माण को विफल न कर दे।

आवर्ती आयोजन अलग समस्या हैं

एकबारगी आयोजन एक क्षण है। आवर्ती आयोजन एक नियम है, और दोनों को अलग संचयन चाहिए।

«हर मंगलवार 09:00 बजे Europe/London में» का अर्थ «इस क्षण से हर 604800 सेकंड» नहीं है। दोनों उसी क्षण अलग हो जाते हैं जब ब्रिटेन अपनी घड़ियाँ बदलता है: नियम बैठक को स्थानीय 09:00 पर बनाए रखता है, जबकि अंतराल उसे 08:00 या 10:00 पर खींच ले जाता है।

नियम संचित कीजिए — स्थानीय समय, क्षेत्र-पहचानक, पुनरावृत्ति का प्रतिरूप — और उससे क्षण उत्पन्न कीजिए। जब tzdata बदले, उत्पन्न किए गए क्षण फिर से बनाइए; उन्हें प्रवासित मत कीजिए। नियम ही वह है जिस पर उपयोक्ता ने सहमति दी थी।

असुविधाजनक प्रकरण साल की उन्हीं दो असंततियों से निकलते हैं। हर दिन 02:30 की बैठक पाएगी कि एक वसंत दिवस पर वह समय होता ही नहीं, और एक शरद दिवस पर वह दो बार होता है। एक नीति चुनिए, लगातार लागू कीजिए, और जान लीजिए कि भिन्न कैलेंडर प्रणालियों ने भिन्न नीतियाँ चुनी हैं — इसीलिए वही आवर्ती आयोजन साल में ठीक दो दिन दो लोगों के कैलेंडरों में एक घंटे के फ़र्क़ से दिख सकता है।

Temporal पर एक टिप्पणी

ECMAScript का Temporal अंतरापृष्ठ Date की जगह ऐसे प्रकार लाता है जो क्षणों, दीवार-घड़ी के समयों और क्षेत्र-सहित तिथि-समयों में भेद करते हैं, java.time की तरह। Temporal.ZonedDateTime क्षेत्र-पहचानक रखता है; Temporal.Instant समय-रेखा पर एक बिंदु है; Temporal.PlainDateTime बिना क्षेत्र का दीवार-घड़ी पाठ है, और प्रकार-प्रणाली आपको इन्हें गड्डमड्ड करने से रोकती है।

इसका disambiguation विकल्प ठीक वही साल-के-दो-दिन वाली नीति उजागर करता है जिस पर यह मार्गदर्शिका बार-बार लौटती है: 'compatible' (मूलभूत — खाइयों के आर-पार आगे बढ़ाइए, संदिग्ध जोड़े में से पूर्ववर्ती लीजिए), 'earlier', 'later' और 'reject'। अंतिम वाला हर उस जगह विचारणीय है जहाँ चुपचाप एक घंटा चूकना त्रुटि दिखाने से बुरा हो।