Bekerja dengan tanggal dan jam adalah salah satu area yang paling sering memunculkan bug produksi. Kesalahan klasik seperti menyimpan waktu lokal padahal server berada di zona berbeda, atau menambahkan satu bulan ke tanggal 31 Januari, kerap terjadi bila kita memakai API lama java.util.Date. Kotlin di atas JVM mewarisi paket java.time yang memisahkan lima konsep inti: Instant, LocalDateTime, ZonedDateTime, Duration, dan Period. Bab ini memperkenalkan kelima konsep tersebut dan format ISO-8601 yang menjadi standar pertukaran antar sistem.

Lima Konsep Inti java.time #

Setiap tipe di java.time dirancang untuk satu pekerjaan spesifik. Mencampur-adukkan tipe adalah sumber bug paling umum. Tabel berikut memetakan tipe, peran, dan kapan harus dipakai:

TipeMengandung Zona?Cocok Untuk
InstantSelalu UTCPenyimpanan timestamp database, log audit
LocalDateTidakTanggal lahir, tanggal faktur, hari libur
LocalTimeTidakJam alarm, jam buka toko
LocalDateTimeTidakPenjadwalan abstrak tanpa konteks zona
ZonedDateTimeYaTampilan layar pengguna, perhitungan zona spesifik
Duration—Panjang waktu berbasis jam/menit/detik
Period—Panjang waktu berbasis tahun/bulan/hari

Perbedaan paling penting ada pada Duration dan Period. Duration.ofDays(1) selalu 24 jam. Period.ofDays(1) berarti “tanggal besok” yang bisa jadi 23 atau 25 jam pada hari pergantian daylight saving time. Memilih salah satu secara keliru menjadi sumber bug tersekat (subtle bug) yang sulit dideteksi.

Contoh Lengkap: Lima Tipe Secara Bersamaan #

Kode berikut membangun sebuah instan acara tetap (15 Januari 2026, 08:30 UTC), lalu memproyeksikannya menjadi LocalDateTime, ZonedDateTime, ditambah Duration, dan Period:

kotlin
import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.Period
import java.time.ZoneId
import java.time.ZonedDateTime

fun main() {
    // 1. Instant: titik waktu absolut di UTC, bebas zona waktu
    val instanSekarang: Instant = Instant.parse("2026-01-15T08:30:00Z")
    println("=== Instant (UTC) ===")
    println("instant       : $instanSekarang")
    println("epochSecond   : ${instanSekarang.epochSecond}")
    println("nano          : ${instanSekarang.nano}")

    // 2. LocalDateTime: tanggal + jam TANPA zona waktu
    val jadwalRapat: LocalDateTime = LocalDateTime.of(2026, 1, 15, 8, 30)
    println()
    println("=== LocalDateTime (tanpa zona) ===")
    println("jadwalRapat   : $jadwalRapat")

    // 3. ZonedDateTime: tanggal + jam + zona waktu
    val zonaJakarta: ZoneId = ZoneId.of("Asia/Jakarta")
    val jadwalJakarta: ZonedDateTime = ZonedDateTime.of(jadwalRapat, zonaJakarta)
    println()
    println("=== ZonedDateTime (dengan zona) ===")
    println("jadwalJakarta : $jadwalJakarta")
    println("zona          : ${jadwalJakarta.zone}")

    // 4. Duration: panjang waktu berbasis jam/menit/detik
    val durasiRapat: Duration = Duration.ofMinutes(90)
    val selesai: LocalDateTime = jadwalRapat.plus(durasiRapat)
    println()
    println("=== Duration ===")
    println("durasiRapat   : $durasiRapat (${durasiRapat.toMinutes()} menit)")
    println("selesai       : $selesai")

    // 5. Period: panjang waktu berbasis tanggal (tahun/bulan/hari)
    val periodeHalal: Period = Period.of(0, 1, 15)
    val tanggalKedaluwarsa: LocalDate = LocalDate.of(2026, 1, 15).plus(periodeHalal)
    println()
    println("=== Period ===")
    println("periodeHalal  : $periodeHalal")
    println("tanggalKedaluwarsa: $tanggalKedaluwarsa")

    // 6. ISO-8601 format default
    println()
    println("=== ISO-8601 ===")
    println("LocalDate ISO : ${LocalDate.of(2026, 1, 15)}")
    println("LocalTime ISO : ${LocalTime.of(8, 30)}")
    println("Instant  ISO : ${Instant.parse("2026-01-15T08:30:00Z")}")
}

Output yang Dihasilkan:

text
=== Instant (UTC) ===
instant       : 2026-01-15T08:30:00Z
epochSecond   : 1768465800
nano          : 0

=== LocalDateTime (tanpa zona) ===
jadwalRapat   : 2026-01-15T08:30

=== ZonedDateTime (dengan zona) ===
jadwalJakarta : 2026-01-15T08:30+07:00[Asia/Jakarta]
zona          : Asia/Jakarta

=== Duration ===
durasiRapat   : PT1H30M (90 menit)
selesai       : 2026-01-15T10:00

=== Period ===
periodeHalal  : P1M15D
tanggalKedaluwarsa: 2026-03-02

=== ISO-8601 ===
LocalDate ISO : 2026-01-15
LocalTime ISO : 08:30
Instant  ISO : 2026-01-15T08:30:00Z

Mari kita telaah baris-baris kode di atas:

  1. Baris 11 (Instant.parse("2026-01-15T08:30:00Z")): mengurai string ISO-8601 dengan akhiran Z yang berarti Zulu/UTC. Hasilnya adalah Instant murni yang dapat disimpan di kolom TIMESTAMP database tanpa ambiguitas zona.
  2. Baris 19 (LocalDateTime.of(2026, 1, 15, 8, 30)): membentuk tanggal+jam tanpa zona. Cocok untuk input pengguna yang belum diikat ke zona tertentu, misalnya formulir “jam berapa rapat dimulai” sebelum pengguna memilih zona.
  3. Baris 26 (ZonedDateTime.of(jadwalRapat, zonaJakarta)): menggabungkan LocalDateTime dengan ZoneId menjadi momen yang sepenuhnya terdefinisi. Offset +07:00 muncul otomatis dari data zona.
  4. Baris 33 (Duration.ofMinutes(90)): menghasilkan PT1H30M. Kita menambahkannya ke LocalDateTime lewat plus(...). Operasi ini bekerja untuk jam karena Duration berbasis detik.
  5. Baris 41 (Period.of(0, 1, 15)): menambahkan 1 bulan 15 hari ke tanggal 15 Januari 2026 menghasilkan 2 Maret 2026 (bukan 30 Februari, karena Februari 2026 hanya 28 hari). Inilah kekuatan Period yang menghormati panjang kalender.
  6. Baris 50-52 (cetak ISO): setiap tipe java.time memakai toString() yang menghasilkan format ISO-8601 bawaan. Format ini dapat di-parse kembali tanpa formatter khusus.

Format ISO-8601: Bahasa Antar Sistem #

ISO-8601 adalah standar internasional representasi tanggal dan jam. Setiap tipe java.time memakai format ISO secara default di toString(). Pola dasarnya ringkas dan tidak ambigu:

text
Date            : yyyy-MM-dd         -> 2026-01-15
Time            : HH:mm:ss           -> 08:30:00
DateTime        : yyyy-MM-ddTHH:mm   -> 2026-01-15T08:30
Instant (UTC)   : yyyy-MM-ddTHH:mm:ssZ -> 2026-01-15T08:30:00Z
Period          : PnYnMnD            -> P1M15D
Duration        : PTnHnMnS           -> PT1H30M

Karakter T memisahkan tanggal dan jam. Karakter Z menandai UTC. Karakter P memulai period dan PT memulai duration. Format ini dapat dipertukarkan antara backend, frontend, API, dan log tanpa perlu formatter tambahan.

Referensi Lanjutan #

Selanjutnya #

Lima tipe di atas berasal dari paket java.time bawaan JDK. Pada chapter java.time Interoperability kita akan melihat bagaimana Kotlin mengonsumsi API ini secara langsung tanpa wrapper, termasuk idiom extension function yang membuat pemakaian java.time terasa lebih Kotlin-idiomatic.