Makine tarafından okunabilir bölgedeki (machine-readable zone, MRZ) her tarih altı rakamdan oluşur: ikisi yıl, ikisi ay, ikisi gün için. Yüzyıl yoktur. ICAO'nun yayımladığı örnek pasaportta doğum tarihi olarak 740812 yazar ve bölgedeki hiçbir şey bunun 1974 mü yoksa 2074 mü olduğunu söylemez. 1974 olduğunu bilirsiniz, çünkü kimse doğmadan önce pasaport taşımaz; kuralın tamamı bu tek akıl yürütmeden ibarettir. Bunu tam olarak doğru uygulamak, önemli olduğu gün de dahil, sabit bir eşik yılından biraz daha fazla özen ister.
Bu yazı doğum tarihleri ve son geçerlilik tarihleri için kuralı, basit ayrıştırıcıları bozan uç durumları ve kendi test paketinize ekleyebileceğiniz bir test vektörü tablosuyla birlikte Python ve JavaScript'te bir fonksiyon sunuyor. Tüm örneklerde ICAO'nun kurgusal örneği ya da sentetik değerler kullanılır; gerçek bir belge yer almaz.
Burası doc.cheap'in blogu; doc.cheap, bölgeyi sizin için okuyan bir Pasaport ve Kimlik OCR API'si. Aşağıdakilerin hiçbiri ona ihtiyaç duymaz; kodun bağımlılığı yoktur.
Tarihler nerede durur
Makine tarafından okunabilir seyahat belgelerinin standardı olan ICAO Doc 9303, her biçime iki tarih alanı verir: doğum tarihi ve son geçerlilik tarihi; her biri YYMMDD olarak yazılır ve ardından kendi kontrol rakamı gelir. Konumlar 0'dan başlar, slice için hazırdır:
| Biçim | Doğum tarihi | Son geçerlilik tarihi |
|---|---|---|
| TD3 (pasaportlar), 2. satır | 13–18, kontrol rakamı 19'da | 21–26, kontrol rakamı 27'de |
| TD2, 2. satır | 13–18, kontrol rakamı 19'da | 21–26, kontrol rakamı 27'de |
| TD1 (kimlik kartları), 2. satır | 0–5, kontrol rakamı 6'da | 8–13, kontrol rakamı 14'te |
TD3 örnek satırı L898902C36UTO7408122F3404159ZE184226B<<<<<16 üzerinde bu, doğum tarihi için 740812, son geçerlilik için 340415 verir.
Sabit bir eşik yılı neden bozulur
Olağan kestirme bir pivot noktasıdır: belirli bir sayının altındaki iki haneli yıllar 2000'lere, geri kalanlar 1900'lere gider. Python'un kendi time.strptime fonksiyonu %y için böyle çalışır. Belgelerinde şöyle yazar: "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."
Bu, bir log dosyası için makul bir varsayılandır. Doğum tarihi içinse iki yönde de yanlıştır. Bu kurala göre 12 Nisan 1968'de (680412) doğan biri 2068'de doğmuş olur. Pivotu kaydırırsanız yalnızca hatayı kaydırırsınız: herhangi bir sabit sayı birileri için yanlıştır ve kod üretimde kaldığı her yıl daha da yanlış hale gelir. Doğru cevap bugünün tarihine bağlıdır, dolayısıyla kural bugünün tarihini girdi olarak almalıdır.
Doğum tarihleri: gelecekte olmayan en son yüzyıl
Yılı 2000 + YY olarak okuyun. Ortaya çıkan tam tarih bugünden sonraysa 100 yıl çıkarın. Hepsi bu.
Bir uygulamanın gerçekten doğru olup olmadığını üç ayrıntı belirler:
- Yalnızca yılı değil, tarihin tamamını karşılaştırın. Aşağıdaki örneklerde bugün 24 Eylül 2026'dır.
261224, 2000'ler okumasında 24 Aralık 2026'dır, yani üç ay sonrasıdır; dolayısıyla kişi 24 Aralık 1926'da doğmuştur. Yalnızca26ile26'yı karşılaştırmak onu yeni doğmuş bir bebek sayardı. - Bugün geçmiş sayılır. Bugün doğan biri önceki yüzyılda değil, bu yüzyılda doğmuştur.
26092424 Eylül 2026 olarak kalır; bir gün sonrası olan260925ise 1926'ya döner. "Büyük veya eşit" değil, "büyük" karşılaştırması kullanın. - "Bugün"ü UTC'de ya da seçtiğiniz sabit bir saat diliminde alın ve parametre olarak geçirin. Saati kendisi okuyan bir fonksiyon test edilemez; bir saat dilimindeki sunucu ile başka bir dilimdeki tarayıcı her günün bir kısmında tarih konusunda anlaşamayabilir.
Son geçerlilik tarihleri: bu yüzyıl
Son geçerlilik tarihi 2000 + YY olarak okunur ve öyle bırakılır. 2012'de süresi dolmuş bir pasaport 2112 değil, hâlâ 2012 tarihlidir; 2032'de süresi dolacak olan da 2032'dir. 1900'ler okuması yalnızca 2000'den önce süresi dolmuş bir belge için doğru olurdu ve böyle bir belge artık kullanımda değildir. Bu kural 2090'lara kadar geçerlidir; o yıllarda düzenlenen bir belgenin süresi 2100'lerde dolabilir. Kodun buna ondan önce bakılması gerekir, şimdi değil.
Test etmeye değer uç durumlar
Yüzyıl sınırında 29 Şubat. Önce yüzyılı seçin, ardından tarihin var olup olmadığını kontrol edin. 280229 doğum tarihi olarak 2000'ler okumasında bugünden sonradır, bu yüzden 29 Şubat 1928 olur; 1928 artık yıl olduğu için bu tarih vardır. 000229, 29 Şubat 2000'dir ve 2000 artık yıl olduğu için vardır; artık yıl olmayan 1900'de aynı altı rakam hiç tarih olmazdı. 00, yüzyılın "bu artık yıl mı?" sorusunun cevabını değiştirdiği tek iki haneli yıldır. Yüzyılı seçmeden önce tarihi doğrularsanız bunlardan biri yanlış çıkar.
Bu yılın ilerleyen günlerine düşen bir doğum tarihi. Yukarıda ele alındı: 261224 2026 değil 1926'dır ve bir test bunu sabit bir "bugün" ile sağlama almalıdır.
Tarihteki dolgu karakterleri. Tarihin bir kısmı bilinmiyorsa bölge doğum tarihinde < taşıyabilir, örneğin 94<<08. Döndürülecek bir takvim tarihi yoktur. Aşağıdaki fonksiyon altı rakam olmayan her değer için hiçbir şey döndürmez ve ham metni göstermeyi çağırana bırakır.
İmkânsız bir tarihte geçen kontrol rakamı. Kontrol rakamı takvimi değil, karakterleri korur. 7, 3, 1 ağırlıklarıyla 741312 toplamı 88'dir, dolayısıyla kontrol rakamı 8'dir ve 7413128 geçer. 13. ay yoktur. Kontrol rakamı ve tarih iki ayrı kontroldür ve ikisinin de geçmesi gerekir; ilkini MRZ kontrol rakamları nasıl çalışır ayrıntılı olarak anlatır.
100 yaşın üzerindeki kişiler. 1925'te doğan biri ile 2025'te doğan bir bebek aynı altı rakama sahiptir. Bölge tek başına onları ayıramaz ve yukarıdaki kural daha genç okumayı seçer. Kullanıcılarınız arasında yüz yaşını aşmış kişiler varsa, veri sayfasında basılı tarihle karşılaştırın; belge dört haneli yılı basıyorsa o yıl orada yer alır.
Python'da fonksiyon
Bağımlılık yok. today, çağıranın geçirdiği bir datetime.date'tir.
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())
Desen bilerek \d ya da str.isdigit() değil, [0-9]'dur: bu ikisi başka yazı sistemlerinin rakamlarını da kabul eder, MRZ ise yalnızca ASCII rakamları içerir. Karşılaştırma bir tuple kullanır; böylece imkânsız bir ay bile date() onu reddetmeden önce sorunsuz karşılaştırılır.
Aynı fonksiyon JavaScript'te
today, YYYY-MM-DD biçiminde bir string'dir. Bu tür iki string düz string olarak doğru karşılaştırılır; bu da yüzyıl kuralını tek satırda tutar.
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() her zaman UTC'dedir; onu today için güvenli bir kaynak yapan da budur.
Test vektörleri
Aşağıdaki her satır, bugün 24 Eylül 2026 olarak sabitlenerek kuraldan elle hesaplanmıştır. Her iki fonksiyon da son sütundaki değeri döndürmelidir ("tarih yok" için Python'da None, JavaScript'te null).
| Girdi | Tür | Beklenen | Neden |
|---|---|---|---|
740812 |
birth | 1974-08-12 | ICAO örneği |
340415 |
expiry | 2034-04-15 | ICAO örneği |
120415 |
expiry | 2012-04-15 | süresi dolmuş, yine de 2012 |
940308 |
birth | 1994-03-08 | 2094 gelecekte |
150101 |
birth | 2015-01-01 | 2015 geçmişte |
300101 |
birth | 1930-01-01 | 2030 gelecekte |
261224 |
birth | 1926-12-24 | bu yılın ilerisinde, dolayısıyla önceki yüzyıl |
260925 |
birth | 1926-09-25 | yarın, dolayısıyla önceki yüzyıl |
260924 |
birth | 2026-09-24 | bugün geçmiş sayılır |
260923 |
birth | 2026-09-23 | dün |
261224 |
expiry | 2026-12-24 | son geçerlilik 2000'lerde kalır |
320310 |
expiry | 2032-03-10 | son geçerlilik 2000'lerde kalır |
280229 |
birth | 1928-02-29 | 1928 artık yıl |
000229 |
birth | 2000-02-29 | 2000 artık yıl, 1900 değildi |
270229 |
birth | yok | 1927 artık yıl değil |
230229 |
expiry | yok | 2023 artık yıl değil |
240229 |
expiry | 2024-02-29 | 2024 artık yıl |
941308 |
birth | yok | 13. ay yoktur |
94<<08 |
birth | yok | dolgu karakterleri, tarih değil |
Python sürümü için bir test çalıştırıcısı, today = date(2026, 9, 24) ile bu satırlar üzerinde dönen ve result.isoformat() değerini (ya da None) beklenen sütunla karşılaştıran bir döngüdür. Testlerde "bugün"ü sabit tutun: gerçek saatle, 261224 gibi satırların cevabı 25 Aralık'ta değişir.
MRZ ayrıştırıcımız ne yapar
Bu sitedeki MRZ ayrıştırıcı aynı kuralı izler: yıl 2000 + YY olarak başlar, tam tarihi bugünden (UTC'de) sonra olan bir doğum tarihi 100 yıl geriye gider, takvim kontrolü yüzyıl seçildikten sonra yapılır ve son geçerlilik tarihleri 2000'lerde kalır. Kendi testleri bu yılın ilerisine düşen bir tarihi, bugünü ve yüzyılın her iki tarafında 29 Şubat'ı kapsar. Tarihe çeviremediği bir değer için bunun yerine ham metni gösterir; bölgede dolgu karakteri olan yerlerde ? yer alır. Altı rakam kontrol rakamını geçtiği halde hiçbir takvim tarihine karşılık gelmediğinde bunu bir uyarıyla belirtir. Son geçerlilik tarihi, UTC'deki bugünün tarihine göre süresi dolmuş ya da dolmamış olarak işaretlenir. Tarayıcıda çalışır; böylece sentetik bir bölge yapıştırıp okumasını kendi uygulamanızla karşılaştırabilirsiniz.
Bir iş akışında nereye oturur
Barındırılan bir tanıma hizmeti kullanıyorsanız, tarih alanlarındaki yüzyıl sizin için zaten seçilmiştir; soru, onun kuralının sizin istediğiniz kural olup olmadığıdır. doc.cheap yanıtı holder.birth_date ve document.expiry_date alanlarını ISO YYYY-MM-DD olarak döndürür ve yanlarında bölgeyi tam okunduğu haliyle mrz.lines içinde verir; böylece bir tarih erişimi ya da parayı belirlediğinde altı rakamı kendiniz ayırıp yukarıdaki fonksiyonu uygulayabilirsiniz. Hâlâ bir hizmet seçiyorsanız, pasaport OCR API karşılaştırması yayımlanmış fiyatları yan yana koyar.
Kuralın yanlış işlediği bir girdi bulursanız admin@doc.cheap adresine yazın.