Aplikasi nyata menerima tanggal dari berbagai sumber: input form pengguna, berkas konfigurasi, body HTTP, kolom database. Format yang diterima bervariasi, dan tidak semua string valid. Operasi parsing yang baik harus menerima format yang diharapkan, menolak format lain, dan menangani kegagalan dengan rapi tanpa crash. Bab ini membahas LocalDate.parse(...), custom pattern via DateTimeFormatter.ofPattern(...), penanganan DateTimeParseException, dan strategi strict versus smart parsing.

Parsing ISO-8601 dan Custom Pattern #

LocalDate.parse(string) tanpa argumen formatter memakai DateTimeFormatter.ISO_LOCAL_DATE. Format ini ketat — hanya menerima yyyy-MM-dd dengan leading zero. Untuk format lain, sediakan DateTimeFormatter eksplisit.

Sumber InputPatternContoh
API JSON modernISO-8601 default2026-01-15
Form pengguna Indonesiadd/MM/yyyy15/01/2026
Sistem lawas AmerikaMM/dd/yyyy01/15/2026
Berkas batchyyyyMMdd HHmmss20260115 083000
Tampilan lokalEEEE, dd MMMM yyyyKamis, 15 Januari 2026

Pattern MM/dd/yyyy versus dd/MM/yyyy adalah sumber bug klasik: 01/02/2026 bisa berarti 1 Februari atau 2 Januari. Selalu tentukan pattern secara eksplisit; jangan mengandalkan default kecuali yakin formatnya ISO-8601.

Contoh Lengkap: ISO, Custom, Error, dan Strict Mode #

Kode berikut memparis berbagai format, menangani input yang tidak valid dengan try-catch, dan membandingkan mode SMART (default) versus STRICT:

kotlin
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.util.Locale

fun main() {
    // 1. Parsing ISO-8601 default (tanpa formatter eksplisit)
    val iso1: LocalDate = LocalDate.parse("2026-01-15")
    val iso2: LocalTime = LocalTime.parse("08:30:00")
    val iso3: LocalDateTime = LocalDateTime.parse("2026-01-15T08:30:00")
    println("=== Parsing ISO-8601 ===")
    println("LocalDate       : $iso1")
    println("LocalTime       : $iso2")
    println("LocalDateTime   : $iso3")

    // 2. Parsing dengan custom pattern
    val fmt1 = DateTimeFormatter.ofPattern("dd/MM/yyyy")
    val fmt2 = DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.of("id", "ID"))
    val fmt3 = DateTimeFormatter.ofPattern("EEEE, dd MMMM yyyy", Locale.of("id", "ID"))
    val fmt4 = DateTimeFormatter.ofPattern("yyyyMMdd HHmmss")

    val d1 = LocalDate.parse("15/01/2026", fmt1)
    val d2 = LocalDate.parse("15 Jan 2026", fmt2)
    val d3 = LocalDate.parse("Kamis, 15 Januari 2026", fmt3)
    val d4 = LocalDateTime.parse("20260115 083000", fmt4)
    println()
    println("=== Parsing custom pattern ===")
    println("15/01/2026      -> $d1")
    println("15 Jan 2026     -> $d2")
    println("Kamis, 15 Januari 2026 -> $d3")
    println("20260115 083000 -> $d4")

    // 3. Penanganan DateTimeParseException dengan aman
    println()
    println("=== Penanganan error ===")
    val inputBerisiko: List<String> = listOf(
        "2026-01-15",      // valid ISO
        "2026-13-40",      // bulan/hari tidak valid
        "15/01/2026",      // bukan ISO
        "bukan-tanggal",   // sampah
    )
    for (input in inputBerisiko) {
        val hasil: LocalDate? = tryParse(input)
        println("input: ${input.padEnd(20)} -> ${hasil ?: "null (parse gagal)"}")
    }

    // 4. Strict vs Lenient parsing
    println()
    println("=== Strict vs Smart ===")
    val fmtLenient = DateTimeFormatter.ofPattern("dd-MM-yyyy")
    val fmtStrict = DateTimeFormatter.ofPattern("dd-MM-yyyy")
        .withResolverStyle(java.time.format.ResolverStyle.STRICT)

    // Lenient: 31 Februari dipaksa menjadi 3 Maret
    val lenientHasil = LocalDate.parse("31-02-2026", fmtLenient)
    println("31-02-2026 (SMART)  -> $lenientHasil")

    // Strict: 31 Februari melempar exception
    val strictHasil: String = try {
        LocalDate.parse("31-02-2026", fmtStrict).toString()
    } catch (e: DateTimeParseException) {
        "exception: ${e.message}"
    }
    println("31-02-2026 (STRICT) -> $strictHasil")

    // 5. Round-trip: parse -> format -> parse
    println()
    println("=== Round-trip ===")
    val asli = LocalDate.of(2026, 1, 15)
    val formatBerkas = DateTimeFormatter.ofPattern("yyyyMMdd")
    val stringBerkas = asli.format(formatBerkas)
    val hasilRoundTrip = LocalDate.parse(stringBerkas, formatBerkas)
    println("asli            : $asli")
    println("format          : $stringBerkas")
    println("parsed          : $hasilRoundTrip")
    println("sama?           : ${asli == hasilRoundTrip}")
}

fun tryParse(input: String): LocalDate? {
    return try {
        LocalDate.parse(input)
    } catch (e: DateTimeParseException) {
        null
    }
}

Output yang Dihasilkan:

text
=== Parsing ISO-8601 ===
LocalDate       : 2026-01-15
LocalTime       : 08:30
LocalDateTime   : 2026-01-15T08:30

=== Parsing custom pattern ===
15/01/2026      -> 2026-01-15
15 Jan 2026     -> 2026-01-15
Kamis, 15 Januari 2026 -> 2026-01-15
20260115 083000 -> 2026-01-15T08:30

=== Penanganan error ===
input: 2026-01-15           -> 2026-01-15
input: 2026-13-40           -> null (parse gagal)
input: 15/01/2026           -> null (parse gagal)
input: bukan-tanggal        -> null (parse gagal)

=== Strict vs Smart ===
31-02-2026 (SMART)  -> 2026-02-28
31-02-2026 (STRICT) -> exception: Text '31-02-2026' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {YearOfEra=2026, MonthOfYear=2, DayOfMonth=31},ISO of type java.time.format.Parsed

=== Round-trip ===
asli            : 2026-01-15
format          : 20260115
parsed          : 2026-01-15
sama?           : true

Mari kita telaah baris-baris kode di atas:

  1. Baris 14-16 (LocalDate.parse("2026-01-15")): parser default ISO_LOCAL_DATE hanya menerima format ini. Input seperti "2026-1-15" tanpa leading zero akan ditolak dengan DateTimeParseException.
  2. Baris 22-25 (ofPattern("dd/MM/yyyy", Locale.of("id","ID"))): pattern kustom dengan locale. Locale menentukan bagaimana simbol MMM (singkatan bulan) dan MMMM (nama bulan penuh) diterjemahkan. Tanpa locale yang tepat, "15 Jan 2026" tidak akan dapat diparse karena parser mencari "15 Jan." dalam locale yang berbeda.
  3. Baris 36-48 (tryParse): pattern idiomatik Kotlin. Fungsi mengembalikan LocalDate? (nullable). Pemanggil dapat memakai elvis operator ?: untuk memberi nilai default. Pendekatan ini lebih bersih daripada melempar exception ke pemanggil.
  4. Baris 56 (SMART parsing 31-02-2026): mode SMART (default) memperbaiki tanggal invalid dengan menggulir ke tanggal terdekat yang valid. 31 Februari di tahun 2026 (bukan kabisat) dianggap sebagai hari ke-3 setelah akhir Februari yang punya 28 hari, sehingga hasilnya 2026-02-28 (batas akhir bulan).
  5. Baris 61-66 (STRICT parsing): mode STRICT menolak tanggal yang tidak ada di kalender. Ini cocok untuk validasi input pengguna ketika kita tidak ingin menerima tanggal seperti 31-02-2026 secara diam-diam. Mode STRICT juga memerlukan field yang lengkap (era, tahun, bulan, hari).
  6. Baris 73-79 (round-trip): membuktikan bahwa format dan parse dengan formatter yang sama menghasilkan objek yang setara. Pola yyyyMMdd sering dipakai untuk penamaan berkas backup karena terurut leksikal sama dengan urutan kronologis.

Strategi Penanganan Error #

Parsing selalu berisiko gagal karena input dapat berasal dari sumber yang tidak tepercaya. Tiga pola umum yang dipakai di Kotlin:

PolaImplementasiCocok Untuk
Try-catch ke nullablefun parse(s): T?Input opsional, fallback ke default
runCatchingrunCatching { LocalDate.parse(s) }Pipeline fungsional, akses .getOrNull()
Eksepsi diteruskanlempar DateTimeParseExceptionValidasi wajib (form dengan error message)
kotlin Pola: runCatching untuk pipeline fungsional
fun parseTanggal(input: String): Result<LocalDate> =
    runCatching { LocalDate.parse(input, fmt) }

val hasil = parseTanggal("15/01/2026")
    .map { it.plusDays(7) }
    .getOrElse { LocalDate.of(1970, 1, 1) }

Pola runCatching lebih idiomatik di Kotlin karena menghindari boilerplate try-catch dan mengembalikan tipe Result<T> yang dapat dirantai dengan .map, .recover, dan .getOrElse.

Referensi Lanjutan #

  • DateTimeFormatter — dokumentasi DateTimeFormatter untuk parsing teks menjadi tipe java.time

Selanjutnya #

Setelah objek tanggal berhasil dibuat, langkah berikutnya adalah manipulasi: menambah hari, minggu, atau bulan; menghitung selisih antar tanggal; dan memilih antara Duration versus Period untuk operasi yang benar. Bab Date-Time Arithmetic akan membahasnya tuntas.