Setiap tanggal di zona baca mesin (machine-readable zone, MRZ) terdiri dari enam digit: dua untuk tahun, dua untuk bulan, dua untuk hari. Tidak ada abad. Paspor spesimen yang diterbitkan ICAO mencantumkan 740812 sebagai tanggal lahir, dan tidak ada apa pun di zona itu yang memberi tahu apakah itu 1974 atau 2074. Anda tahu itu 1974 karena tidak ada orang yang memegang paspor sebelum ia lahir, dan satu penalaran itulah seluruh aturannya. Menerapkannya dengan tepat, termasuk pada hari ketika hal itu menentukan, butuh sedikit lebih banyak kehati-hatian daripada satu tahun batas yang tetap.

Artikel ini memberikan aturan untuk tanggal lahir dan tanggal kedaluwarsa, kasus-kasus tepi yang merusak parser sederhana, dan sebuah fungsi di Python dan JavaScript beserta tabel data uji yang bisa Anda masukkan ke suite pengujian Anda sendiri. Semua contoh memakai spesimen fiktif ICAO atau nilai sintetis; tidak ada dokumen asli yang muncul.

Ini adalah blog doc.cheap, API OCR Paspor dan Kartu Identitas yang membaca zona ini untuk Anda. Tidak ada bagian di bawah yang membutuhkannya; kodenya tanpa dependensi.

Letak tanggal-tanggalnya

ICAO Doc 9303, standar untuk dokumen perjalanan yang bisa dibaca mesin, memberi setiap format dua field tanggal: tanggal lahir dan tanggal kedaluwarsa, masing-masing ditulis sebagai YYMMDD dan masing-masing diikuti digit pemeriksanya sendiri. Posisi dihitung dari 0, siap dipakai di slice:

Format Tanggal lahir Tanggal kedaluwarsa
TD3 (paspor), baris 2 13–18, digit pemeriksa di 19 21–26, digit pemeriksa di 27
TD2, baris 2 13–18, digit pemeriksa di 19 21–26, digit pemeriksa di 27
TD1 (kartu identitas), baris 2 0–5, digit pemeriksa di 6 8–13, digit pemeriksa di 14

Pada baris spesimen TD3 L898902C36UTO7408122F3404159ZE184226B<<<<<16, hasilnya 740812 untuk tanggal lahir dan 340415 untuk tanggal kedaluwarsa.

Mengapa tahun batas yang tetap gagal

Jalan pintas yang biasa adalah titik pivot: tahun dua digit di bawah angka tertentu masuk ke tahun 2000-an, sisanya ke tahun 1900-an. time.strptime milik Python sendiri bekerja dengan cara itu untuk %y. Dokumentasinya menyatakan: "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."

Itu bawaan yang masuk akal untuk file log. Untuk tanggal lahir, itu salah ke dua arah. Menurut aturan itu, seseorang yang lahir pada 12 April 1968 (680412) lahir pada 2068. Geser pivotnya dan Anda hanya memindahkan bug-nya: angka tetap mana pun akan salah bagi seseorang, dan makin salah setiap tahun kode itu tetap berjalan di produksi. Jawaban yang benar bergantung pada tanggal hari ini, jadi aturannya harus menerima tanggal hari ini sebagai masukan.

Tanggal lahir: abad terakhir yang tidak berada di masa depan

Baca tahunnya sebagai 2000 + YY. Jika tanggal lengkap yang dihasilkan lebih lambat dari hari ini, kurangi 100 tahun. Itu saja.

Tiga detail menentukan apakah sebuah implementasi benar-benar tepat:

  • Bandingkan tanggal lengkapnya, bukan hanya tahunnya. Dalam contoh di bawah, hari ini adalah 24 September 2026. 261224 dalam pembacaan 2000-an adalah 24 Desember 2026, tiga bulan lagi, jadi orang itu lahir pada 24 Desember 1926. Membandingkan 26 dengan 26 saja akan menganggapnya bayi baru lahir.
  • Hari ini dihitung sebagai masa lalu. Orang yang lahir hari ini lahir di abad ini, bukan abad lalu. 260924 tetap 24 September 2026; 260925, sehari kemudian, mundur ke 1926. Gunakan "lebih lambat dari", bukan "lebih lambat dari atau sama dengan".
  • Ambil "hari ini" dalam UTC, atau dalam satu zona waktu tetap pilihan Anda, dan teruskan sebagai parameter. Fungsi yang membaca jam sendiri tidak bisa diuji, dan server di satu zona serta browser di zona lain bisa berbeda pendapat tentang tanggal selama sebagian waktu setiap hari.

Tanggal kedaluwarsa: abad ini

Tanggal kedaluwarsa dibaca sebagai 2000 + YY dan dibiarkan begitu. Paspor yang kedaluwarsa pada 2012 tetap bertanggal 2012, bukan 2112, dan yang kedaluwarsa pada 2032 adalah 2032. Pembacaan 1900-an hanya benar untuk dokumen yang kedaluwarsa sebelum 2000, dan tidak ada dokumen seperti itu yang masih dipakai. Aturan ini berlaku sampai tahun 2090-an, ketika dokumen yang diterbitkan saat itu bisa kedaluwarsa di tahun 2100-an; kodenya perlu ditinjau sebelum itu, bukan sekarang.

Kasus tepi yang layak diuji

29 Februari melintasi abad. Pilih abadnya dulu, baru periksa apakah tanggalnya ada. 280229 sebagai tanggal lahir lebih lambat dari hari ini dalam pembacaan 2000-an, jadi menjadi 29 Februari 1928, yang ada karena 1928 adalah tahun kabisat. 000229 adalah 29 Februari 2000, yang ada karena 2000 adalah tahun kabisat; pada 1900, yang bukan tahun kabisat, enam digit yang sama sama sekali bukan tanggal. 00 adalah satu-satunya tahun dua digit di mana abad mengubah jawaban atas "apakah ini tahun kabisat?". Validasi tanggal sebelum memilih abad, dan salah satu dari kasus ini akan keliru.

Tanggal lahir yang jatuh lebih lambat tahun ini. Sudah dibahas di atas: 261224 adalah 1926, bukan 2026, dan sebuah tes harus memastikannya dengan "hari ini" yang tetap.

Karakter pengisi di dalam tanggal. Zona bisa memuat < di tanggal lahir jika sebagian tanggalnya tidak diketahui, misalnya 94<<08. Tidak ada tanggal kalender yang bisa dikembalikan. Fungsi di bawah tidak mengembalikan apa pun untuk nilai yang bukan enam digit, dan menyerahkan kepada pemanggil untuk menampilkan teks mentahnya.

Digit pemeriksa yang lolos pada tanggal yang mustahil. Digit pemeriksa melindungi karakter, bukan kalender. Dengan bobot 7, 3, 1, 741312 berjumlah 88, jadi digit pemeriksanya 8, dan 7413128 lolos. Bulan ke-13 tidak ada. Digit pemeriksa dan tanggal adalah dua pemeriksaan terpisah dan keduanya harus lolos; cara kerja digit pemeriksa MRZ membahas yang pertama secara lengkap.

Orang berusia di atas 100 tahun. Orang yang lahir pada 1925 dan bayi yang lahir pada 2025 memiliki enam digit yang sama. Zona saja tidak bisa membedakan keduanya, dan aturan di atas memilih pembacaan yang lebih muda. Jika pengguna Anda termasuk orang berusia seratus tahun, bandingkan dengan tanggal yang tercetak di halaman data, tempat tahun empat digit muncul jika dokumen mencetaknya.

Fungsinya di Python

Tanpa dependensi. today adalah datetime.date yang diteruskan oleh pemanggil.

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())

Polanya sengaja [0-9], bukan \d atau str.isdigit(): keduanya menerima digit dari aksara lain, sedangkan MRZ hanya pernah berisi digit ASCII. Perbandingannya memakai tuple agar bulan yang mustahil tetap bisa dibandingkan dengan rapi sebelum date() menolaknya.

Fungsi yang sama di JavaScript

today adalah string YYYY-MM-DD. Dua string seperti itu dibandingkan dengan benar sebagai string biasa, sehingga aturan abad cukup satu baris.

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() selalu dalam UTC, dan itulah yang membuatnya menjadi sumber yang aman untuk today.

Data uji

Setiap baris di bawah dihitung manual dari aturannya, dengan hari ini ditetapkan pada 24 September 2026. Kedua fungsi harus mengembalikan nilai di kolom terakhir (None di Python, null di JavaScript untuk "tidak ada tanggal").

Masukan Jenis Diharapkan Alasan
740812 birth 1974-08-12 spesimen ICAO
340415 expiry 2034-04-15 spesimen ICAO
120415 expiry 2012-04-15 sudah kedaluwarsa, tetap 2012
940308 birth 1994-03-08 2094 ada di masa depan
150101 birth 2015-01-01 2015 ada di masa lalu
300101 birth 1930-01-01 2030 ada di masa depan
261224 birth 1926-12-24 lebih lambat tahun ini, jadi abad lalu
260925 birth 1926-09-25 besok, jadi abad lalu
260924 birth 2026-09-24 hari ini dihitung sebagai masa lalu
260923 birth 2026-09-23 kemarin
261224 expiry 2026-12-24 kedaluwarsa tetap di 2000-an
320310 expiry 2032-03-10 kedaluwarsa tetap di 2000-an
280229 birth 1928-02-29 1928 tahun kabisat
000229 birth 2000-02-29 2000 tahun kabisat, 1900 bukan
270229 birth tidak ada 1927 bukan tahun kabisat
230229 expiry tidak ada 2023 bukan tahun kabisat
240229 expiry 2024-02-29 2024 tahun kabisat
941308 birth tidak ada tidak ada bulan ke-13
94<<08 birth tidak ada karakter pengisi, bukan tanggal

Penguji untuk versi Python cukup berupa perulangan atas baris-baris ini dengan today = date(2026, 9, 24), yang membandingkan result.isoformat() (atau None) dengan kolom yang diharapkan. Tetapkan "hari ini" dalam tes: dengan jam sungguhan, baris seperti 261224 akan berubah jawabannya pada 25 Desember.

Apa yang dilakukan parser MRZ kami

Parser MRZ di situs ini mengikuti aturan yang sama: tahun dimulai sebagai 2000 + YY, tanggal lahir yang tanggal lengkapnya lebih lambat dari hari ini (dalam UTC) mundur 100 tahun, pemeriksaan kalender dilakukan setelah abad dipilih, dan tanggal kedaluwarsa tetap di tahun 2000-an. Tes-tesnya sendiri mencakup tanggal yang jatuh lebih lambat tahun ini, hari ini, dan 29 Februari di kedua sisi abad. Untuk nilai yang tidak bisa diubah menjadi tanggal, parser menampilkan teks mentahnya, dengan ? di tempat zona memuat karakter pengisi, dan ketika enam digit lolos digit pemeriksanya tetapi tidak menunjuk tanggal kalender, parser menyatakannya dalam peringatan. Tanggal kedaluwarsa ditandai sudah kedaluwarsa atau belum terhadap tanggal hari ini dalam UTC. Parser berjalan di browser, jadi Anda bisa menempelkan zona sintetis dan membandingkan pembacaannya dengan implementasi Anda sendiri.

Tempatnya dalam pipeline

Jika Anda memakai layanan pengenalan yang di-hosting, abad pada field tanggalnya sudah dipilihkan untuk Anda; pertanyaannya adalah apakah aturannya sesuai dengan yang Anda inginkan. Respons doc.cheap mengembalikan holder.birth_date dan document.expiry_date sebagai ISO YYYY-MM-DD, dan di sampingnya zona persis seperti terbaca di mrz.lines, sehingga Anda bisa mengambil sendiri enam digitnya dan menerapkan fungsi di atas ketika sebuah tanggal menentukan akses atau uang. Jika Anda masih memilih layanan, perbandingan API OCR paspor menyandingkan harga yang dipublikasikan.

Jika Anda menemukan masukan yang salah ditangani aturan ini, tulis ke admin@doc.cheap.