मशीन-रीडेबल ज़ोन (MRZ) की हर तारीख़ छह अंकों की होती है: दो साल के, दो महीने के, दो दिन के। सदी नहीं होती। ICAO जो नमूना पासपोर्ट प्रकाशित करता है, उसमें जन्म तिथि 740812 लिखी है, और ज़ोन में कुछ भी नहीं बताता कि यह 1974 है या 2074। आप जानते हैं कि यह 1974 है, क्योंकि पैदा होने से पहले किसी के पास पासपोर्ट नहीं होता, और यही एक तर्क पूरा नियम है। इसे बिल्कुल सही लागू करना, उस दिन भी जब यह मायने रखता है, किसी तय कट-ऑफ़ साल से थोड़ी ज़्यादा सावधानी माँगता है।

यह पोस्ट जन्म तिथि और समाप्ति तिथि का नियम देती है, वे एज केस बताती है जो सरल पार्सर को तोड़ देते हैं, और Python व JavaScript में एक फ़ंक्शन देती है, साथ में टेस्ट वेक्टर की एक टेबल जिसे आप अपने टेस्ट सूट में डाल सकते हैं। हर उदाहरण ICAO के काल्पनिक नमूने या सिंथेटिक मानों का इस्तेमाल करता है; कोई असली दस्तावेज़ नहीं है।

यह doc.cheap का ब्लॉग है, जो एक पासपोर्ट और ID OCR API है और आपके लिए ज़ोन पढ़ता है। नीचे की किसी भी चीज़ को इसकी ज़रूरत नहीं; कोड की कोई डिपेंडेंसी नहीं है।

तारीख़ें कहाँ होती हैं

मशीन से पढ़े जा सकने वाले यात्रा दस्तावेज़ों का मानक ICAO Doc 9303 हर फ़ॉर्मैट को दो तारीख़ वाले फ़ील्ड देता है: जन्म तिथि और समाप्ति तिथि, दोनों YYMMDD के रूप में लिखी जाती हैं और दोनों के बाद उनका अपना चेक डिजिट आता है। पोज़िशन 0 से गिनी गई हैं, slice के लिए तैयार:

फ़ॉर्मैट जन्म तिथि समाप्ति तिथि
TD3 (पासपोर्ट), लाइन 2 13–18, चेक डिजिट 19 पर 21–26, चेक डिजिट 27 पर
TD2, लाइन 2 13–18, चेक डिजिट 19 पर 21–26, चेक डिजिट 27 पर
TD1 (ID कार्ड), लाइन 2 0–5, चेक डिजिट 6 पर 8–13, चेक डिजिट 14 पर

TD3 नमूने की लाइन L898902C36UTO7408122F3404159ZE184226B<<<<<16 पर इससे जन्म तिथि के लिए 740812 और समाप्ति के लिए 340415 मिलता है।

तय कट-ऑफ़ साल क्यों टूटता है

आम शॉर्टकट एक पिवट है: किसी संख्या से नीचे के दो अंकों वाले साल 2000 के दशकों में जाते हैं, बाक़ी 1900 के दशकों में। Python का अपना time.strptime भी %y के लिए ऐसे ही काम करता है। उसके दस्तावेज़ में लिखा है: "When 2-digit years are parsed, they are converted according to the POSIX and ISO C standards: values 69–99 are mapped to 1969–1999, and values 0–68 are mapped to 2000–2068."

लॉग फ़ाइल के लिए यह एक ठीक-ठाक डिफ़ॉल्ट है। जन्म तिथि के लिए यह दोनों दिशाओं में गलत है। इस नियम से 12 अप्रैल 1968 (680412) को जन्मा व्यक्ति 2068 में जन्मा माना जाएगा। पिवट खिसकाइए और आप सिर्फ़ बग को खिसकाते हैं: कोई भी तय संख्या किसी न किसी के लिए गलत है, और कोड जितने साल प्रोडक्शन में रहता है, उतनी ज़्यादा गलत होती जाती है। सही जवाब आज की तारीख़ पर निर्भर करता है, इसलिए नियम को आज की तारीख़ इनपुट के रूप में लेनी होगी।

जन्म तिथि: सबसे हाल की वह सदी जो भविष्य में नहीं है

साल को 2000 + YY पढ़िए। अगर इससे बनी पूरी तारीख़ आज के बाद की है, तो 100 साल घटा दीजिए। बस इतना ही।

तीन बातें तय करती हैं कि कोई इम्प्लीमेंटेशन सच में सही है या नहीं:

  • सिर्फ़ साल नहीं, पूरी तारीख़ की तुलना कीजिए। नीचे के उदाहरणों में आज 24 सितंबर 2026 है। 2000 वाली रीडिंग में 261224 24 दिसंबर 2026 है, जो तीन महीने बाद है, इसलिए व्यक्ति 24 दिसंबर 1926 को जन्मा था। सिर्फ़ 26 की 26 से तुलना करने पर वह नवजात शिशु माना जाता।
  • आज को अतीत माना जाता है। आज जन्मा व्यक्ति इसी सदी में जन्मा है, पिछली में नहीं। 260924 24 सितंबर 2026 ही रहता है; 260925, एक दिन बाद, 1926 में चला जाता है। "से बाद" इस्तेमाल कीजिए, "से बाद या बराबर" नहीं।
  • "आज" को UTC में, या अपनी चुनी हुई किसी एक तय टाइम ज़ोन में लीजिए, और फ़ंक्शन में पास कीजिए। जो फ़ंक्शन ख़ुद घड़ी पढ़ता है, उसे टेस्ट नहीं किया जा सकता, और एक ज़ोन का सर्वर और दूसरे ज़ोन का ब्राउज़र हर दिन के कुछ हिस्से में तारीख़ पर असहमत हो सकते हैं।

समाप्ति तिथि: यही सदी

समाप्ति तिथि को 2000 + YY पढ़ा जाता है और वैसे ही छोड़ दिया जाता है। 2012 में एक्सपायर हुआ पासपोर्ट अब भी 2012 की तारीख़ है, 2112 की नहीं, और 2032 में एक्सपायर होने वाला 2032 है। 1900 वाली रीडिंग सिर्फ़ उस दस्तावेज़ के लिए सही होती जो 2000 से पहले एक्सपायर हो गया, और ऐसा कोई दस्तावेज़ अब इस्तेमाल में नहीं है। यह नियम 2090 के दशक तक चलता है, जब उस समय जारी हुआ दस्तावेज़ 2100 के दशक में एक्सपायर हो सकता है; कोड को उससे पहले देखना होगा, अभी नहीं।

टेस्ट करने लायक एज केस

सदी के आर-पार 29 फ़रवरी। पहले सदी चुनिए, फिर जाँचिए कि तारीख़ मौजूद है या नहीं। जन्म तिथि के रूप में 280229 2000 वाली रीडिंग में आज के बाद है, इसलिए यह 29 फ़रवरी 1928 बनती है, जो मौजूद है क्योंकि 1928 लीप वर्ष है। 000229 29 फ़रवरी 2000 है, जो मौजूद है क्योंकि 2000 लीप वर्ष है; 1900 में, जो लीप वर्ष नहीं था, यही छह अंक कोई तारीख़ ही नहीं होते। 00 दो अंकों वाला अकेला साल है जिसमें सदी "क्या यह लीप वर्ष है?" का जवाब बदल देती है। सदी चुनने से पहले तारीख़ की जाँच कीजिए और इनमें से कोई एक गलत हो जाएगा।

इसी साल आगे पड़ने वाली जन्म तिथि। ऊपर बताया जा चुका है: 261224 1926 है, 2026 नहीं, और किसी टेस्ट को एक तय "आज" के साथ इसे पक्का करना चाहिए।

तारीख़ में फ़िलर। जब तारीख़ का कुछ हिस्सा अज्ञात हो, तो ज़ोन जन्म तिथि में < रख सकता है, जैसे 94<<08। लौटाने के लिए कोई कैलेंडर तारीख़ नहीं है। नीचे का फ़ंक्शन छह अंकों से अलग किसी भी मान के लिए कुछ नहीं लौटाता, और कच्चा टेक्स्ट दिखाने का काम कॉल करने वाले पर छोड़ देता है।

असंभव तारीख़ पर पास होता चेक डिजिट। चेक डिजिट कैरेक्टरों की रक्षा करता है, कैलेंडर की नहीं। 7, 3, 1 भार के साथ 741312 का योग 88 है, इसलिए इसका चेक डिजिट 8 है, और 7413128 पास हो जाता है। 13वाँ महीना मौजूद नहीं है। चेक डिजिट और तारीख़ दो अलग जाँचें हैं और दोनों का पास होना ज़रूरी है; पहली जाँच को MRZ चेक डिजिट कैसे काम करते हैं पूरी तरह समझाता है।

100 साल से ज़्यादा उम्र के लोग। 1925 में जन्मे व्यक्ति और 2025 में जन्मे बच्चे के छह अंक एक जैसे हैं। अकेला ज़ोन इन्हें अलग नहीं कर सकता, और ऊपर का नियम कम उम्र वाली रीडिंग चुनता है। अगर आपके यूज़र्स में सौ साल से ज़्यादा उम्र के लोग हैं, तो डेटा पेज पर छपी तारीख़ से तुलना कीजिए, जहाँ चार अंकों वाला साल दिखता है अगर दस्तावेज़ उसे छापता है।

Python में फ़ंक्शन

कोई डिपेंडेंसी नहीं। today एक datetime.date है जिसे कॉल करने वाला पास करता है।

import re
from datetime import date, datetime, timezone

SIX_DIGITS = re.compile(r"[0-9]{6}")

def read_mrz_date(raw, kind, today):
    """Turn an MRZ YYMMDD field into a date, or None if it is not one.

    kind is "birth" or "expiry"; today is a datetime.date.
    """
    if kind not in ("birth", "expiry"):
        raise ValueError(f"unknown kind: {kind!r}")
    if not SIX_DIGITS.fullmatch(raw):
        return None  # fillers, letters or the wrong length
    yy, mm, dd = int(raw[0:2]), int(raw[2:4]), int(raw[4:6])
    year = 2000 + yy
    if kind == "birth" and (year, mm, dd) > (today.year, today.month, today.day):
        year -= 100  # the 2000s reading is in the future
    try:
        return date(year, mm, dd)  # checked after the century is chosen
    except ValueError:
        return None  # 13th month, 31 April, 29 February in a common year

# In production:
# read_mrz_date("740812", "birth", datetime.now(timezone.utc).date())

पैटर्न जान-बूझकर [0-9] है, \d या str.isdigit() नहीं: ये दोनों दूसरी लिपियों के अंक भी स्वीकार करते हैं, जबकि MRZ में हमेशा सिर्फ़ ASCII अंक होते हैं। तुलना एक tuple से होती है, ताकि असंभव महीने की तुलना भी date() के उसे अस्वीकार करने से पहले बिना दिक़्क़त हो जाए।

JavaScript में वही फ़ंक्शन

today एक YYYY-MM-DD स्ट्रिंग है। ऐसी दो स्ट्रिंग की तुलना सामान्य स्ट्रिंग की तरह सही होती है, जिससे सदी का नियम एक ही लाइन में रहता है।

export function readMrzDate(raw, kind, today /* "YYYY-MM-DD", UTC */) {
  if (kind !== "birth" && kind !== "expiry") throw new Error(`unknown kind: ${kind}`);
  if (!/^[0-9]{6}$/.test(raw)) return null; // fillers, letters or the wrong length
  const month = Number(raw.slice(2, 4));
  const day = Number(raw.slice(4, 6));
  let year = 2000 + Number(raw.slice(0, 2));
  const iso = (y) => `${y}-${raw.slice(2, 4)}-${raw.slice(4, 6)}`;
  if (kind === "birth" && iso(year) > today) year -= 100; // the 2000s reading is in the future
  // Date.UTC rolls 31 April over to 1 May; a changed month or day means no such date.
  const d = new Date(Date.UTC(year, month - 1, day));
  if (month < 1 || month > 12 || d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day) {
    return null;
  }
  return iso(year);
}

// In production:
// readMrzDate("740812", "birth", new Date().toISOString().slice(0, 10));

toISOString() हमेशा UTC में होता है, और यही इसे today का सुरक्षित स्रोत बनाता है।

टेस्ट वेक्टर

नीचे की हर पंक्ति नियम से हाथ से निकाली गई है, आज को 24 सितंबर 2026 पर तय रखकर। दोनों फ़ंक्शन को आख़िरी कॉलम का मान लौटाना चाहिए ("कोई तारीख़ नहीं" के लिए Python में None, JavaScript में null)।

इनपुट प्रकार अपेक्षित क्यों
740812 birth 1974-08-12 ICAO नमूना
340415 expiry 2034-04-15 ICAO नमूना
120415 expiry 2012-04-15 एक्सपायर, फिर भी 2012
940308 birth 1994-03-08 2094 भविष्य में है
150101 birth 2015-01-01 2015 अतीत में है
300101 birth 1930-01-01 2030 भविष्य में है
261224 birth 1926-12-24 इसी साल आगे, इसलिए पिछली सदी
260925 birth 1926-09-25 आज से अगला दिन, इसलिए पिछली सदी
260924 birth 2026-09-24 आज को अतीत माना जाता है
260923 birth 2026-09-23 आज से पिछला दिन
261224 expiry 2026-12-24 समाप्ति 2000 के दशकों में रहती है
320310 expiry 2032-03-10 समाप्ति 2000 के दशकों में रहती है
280229 birth 1928-02-29 1928 लीप वर्ष है
000229 birth 2000-02-29 2000 लीप वर्ष है, 1900 नहीं था
270229 birth कुछ नहीं 1927 लीप वर्ष नहीं है
230229 expiry कुछ नहीं 2023 लीप वर्ष नहीं है
240229 expiry 2024-02-29 2024 लीप वर्ष है
941308 birth कुछ नहीं 13वाँ महीना नहीं होता
94<<08 birth कुछ नहीं फ़िलर, तारीख़ नहीं

Python वर्शन का टेस्ट रनर इन पंक्तियों पर today = date(2026, 9, 24) के साथ चलने वाला एक लूप है, जो result.isoformat() (या None) की तुलना अपेक्षित कॉलम से करता है। टेस्ट में "आज" को तय रखिए: असली घड़ी के साथ 261224 जैसी पंक्तियों का जवाब 25 दिसंबर को बदल जाता है।

हमारा MRZ पार्सर क्या करता है

इस साइट का MRZ पार्सर यही नियम मानता है: साल 2000 + YY से शुरू होता है, जिस जन्म तिथि की पूरी तारीख़ आज (UTC में) के बाद की हो वह 100 साल पीछे जाती है, कैलेंडर जाँच सदी चुनने के बाद होती है, और समाप्ति तिथियाँ 2000 के दशकों में रहती हैं। उसके अपने टेस्ट इसी साल आगे पड़ने वाली तारीख़, आज, और सदी के दोनों ओर 29 फ़रवरी को कवर करते हैं। जिस मान को वह तारीख़ में नहीं बदल पाता, उसके लिए वह कच्चा टेक्स्ट दिखाता है, और जहाँ ज़ोन में फ़िलर है वहाँ ? रखता है; और जब छह अंक अपना चेक डिजिट पास कर लेते हैं पर कोई कैलेंडर तारीख़ नहीं बनाते, तो वह चेतावनी में यह बताता है। समाप्ति तिथि को UTC में आज की तारीख़ के हिसाब से एक्सपायर या एक्सपायर नहीं के रूप में चिह्नित किया जाता है। यह ब्राउज़र में चलता है, इसलिए आप कोई सिंथेटिक ज़ोन पेस्ट करके उसकी रीडिंग की तुलना अपने इम्प्लीमेंटेशन से कर सकते हैं।

पाइपलाइन में इसकी जगह

अगर आप कोई होस्टेड रिकग्निशन सेवा इस्तेमाल करते हैं, तो उसके तारीख़ वाले फ़ील्ड में सदी पहले ही आपके लिए चुन ली गई है; सवाल यह है कि क्या उसका नियम वही है जो आप चाहते हैं। doc.cheap का रिस्पॉन्स holder.birth_date और document.expiry_date को ISO YYYY-MM-DD के रूप में लौटाता है, और उनके साथ mrz.lines में ज़ोन ठीक वैसा ही जैसा पढ़ा गया, ताकि जब कोई तारीख़ एक्सेस या पैसे का फ़ैसला करे, तब आप छह अंक ख़ुद निकालकर ऊपर का फ़ंक्शन लागू कर सकें। अगर आप अभी सेवा चुन रहे हैं, तो पासपोर्ट OCR API तुलना प्रकाशित क़ीमतों को साथ-साथ रखती है।

अगर आपको कोई ऐसा इनपुट मिले जिस पर यह नियम गलत जवाब देता है, तो admin@doc.cheap पर लिखिए।