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 Input | Pattern | Contoh |
|---|---|---|
| API JSON modern | ISO-8601 default | 2026-01-15 |
| Form pengguna Indonesia | dd/MM/yyyy | 15/01/2026 |
| Sistem lawas Amerika | MM/dd/yyyy | 01/15/2026 |
| Berkas batch | yyyyMMdd HHmmss | 20260115 083000 |
| Tampilan lokal | EEEE, dd MMMM yyyy | Kamis, 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:
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:
=== 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? : trueMari kita telaah baris-baris kode di atas:
- Baris 14-16 (
LocalDate.parse("2026-01-15")): parser defaultISO_LOCAL_DATEhanya menerima format ini. Input seperti"2026-1-15"tanpa leading zero akan ditolak denganDateTimeParseException. - Baris 22-25 (
ofPattern("dd/MM/yyyy", Locale.of("id","ID"))): pattern kustom dengan locale. Locale menentukan bagaimana simbolMMM(singkatan bulan) danMMMM(nama bulan penuh) diterjemahkan. Tanpa locale yang tepat,"15 Jan 2026"tidak akan dapat diparse karena parser mencari"15 Jan."dalam locale yang berbeda. - 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. - Baris 56 (SMART parsing
31-02-2026): mode SMART (default) memperbaiki tanggal invalid dengan menggulir ke tanggal terdekat yang valid.31 Februaridi tahun 2026 (bukan kabisat) dianggap sebagai hari ke-3 setelah akhir Februari yang punya 28 hari, sehingga hasilnya2026-02-28(batas akhir bulan). - Baris 61-66 (STRICT parsing): mode
STRICTmenolak tanggal yang tidak ada di kalender. Ini cocok untuk validasi input pengguna ketika kita tidak ingin menerima tanggal seperti31-02-2026secara diam-diam. Mode STRICT juga memerlukan field yang lengkap (era, tahun, bulan, hari). - Baris 73-79 (round-trip): membuktikan bahwa
formatdanparsedengan formatter yang sama menghasilkan objek yang setara. PolayyyyMMddsering 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:
| Pola | Implementasi | Cocok Untuk |
|---|---|---|
| Try-catch ke nullable | fun parse(s): T? | Input opsional, fallback ke default |
runCatching | runCatching { LocalDate.parse(s) } | Pipeline fungsional, akses .getOrNull() |
| Eksepsi diteruskan | lempar DateTimeParseException | Validasi wajib (form dengan error message) |
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
DateTimeFormatteruntuk parsing teks menjadi tipejava.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.