Date & Time

Date & Time #

Bekerja dengan tanggal dan waktu adalah salah satu topik yang terlihat mudah tapi penuh jebakan — zona waktu yang berbeda, DST (Daylight Saving Time), kalender lokal, dan format yang tidak konsisten. Kotlin menggunakan API java.time yang diperkenalkan di Java 8, yang jauh lebih baik dari java.util.Date dan Calendar yang lama. API ini dirancang dengan prinsip immutability: semua operasi menghasilkan objek baru, tidak memodifikasi yang lama. Artikel ini membahas kelas-kelas utama java.time, cara membuat, memanipulasi, memformat, dan membandingkan tanggal dan waktu, serta pola-pola terbaik untuk penggunaan di aplikasi nyata.

Kenapa Bukan java.util.Date? #

Sebelum masuk ke java.time, penting dipahami mengapa API lama sebaiknya dihindari:

// ANTI-PATTERN: java.util.Date — penuh masalah
val tanggalLama = java.util.Date()
// - Mutable — bisa diubah setelah dibuat, rawan bug
// - Nama yang menyesatkan (Date menyimpan waktu juga, dalam milidetik sejak epoch)
// - API yang tidak intuitif (bulan dimulai dari 0, tahun = tahun - 1900)
// - Tidak thread-safe

// BENAR: java.time — immutable, ekspresif, thread-safe
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.Instant

val sekarang = LocalDateTime.now()

Peta Kelas java.time #

Memilih kelas yang tepat adalah langkah pertama yang krusial:

flowchart TD
    A{Apa yang perlu\ndisimpan?} --> B{Perlu\nzona waktu?}
    B -- Ya --> C["ZonedDateTime\nContoh: 2024-08-24T10:30+07:00[Asia/Jakarta]"]
    B -- Tidak --> D{Perlu\nwaktu?}
    D -- Tidak --> E["LocalDate\nContoh: 2024-08-24"]
    D -- Hanya waktu --> F["LocalTime\nContoh: 10:30:45"]
    D -- Keduanya --> G["LocalDateTime\nContoh: 2024-08-24T10:30:45"]
    A --> H{Titik waktu\nuniversal?}
    H -- Ya --> I["Instant\nContoh: 2024-08-24T03:30:45Z (UTC)"]
    A --> J{Selisih antara\ndua waktu?}
    J -- Berbasis waktu --> K["Duration\nContoh: 8 jam 30 menit"]
    J -- Berbasis tanggal --> L["Period\nContoh: 1 tahun 2 bulan 3 hari"]

LocalDate — Tanggal Tanpa Waktu #

LocalDate mewakili tanggal kalender (tahun, bulan, hari) tanpa informasi waktu dan zona waktu. Cocok untuk tanggal lahir, deadline, jadwal kalender.

import java.time.LocalDate
import java.time.Month
import java.time.DayOfWeek

// Membuat LocalDate
val hari ini = LocalDate.now()
val tanggalSpesifik = LocalDate.of(2024, 8, 17)
val dariMonth = LocalDate.of(2024, Month.AUGUST, 17)
val dariString = LocalDate.parse("2024-08-17")  // format ISO default: yyyy-MM-dd

println(hari ini)           // 2024-08-17 (misalnya)
println(tanggalSpesifik)    // 2024-08-17

// Mengakses komponen
println(tanggalSpesifik.year)        // 2024
println(tanggalSpesifik.monthValue)  // 8
println(tanggalSpesifik.month)       // AUGUST
println(tanggalSpesifik.dayOfMonth)  // 17
println(tanggalSpesifik.dayOfWeek)   // SATURDAY
println(tanggalSpesifik.dayOfYear)   // 230

// Manipulasi — selalu menghasilkan objek baru (immutable)
val besok       = hari ini.plusDays(1)
val mingguLalu  = hari ini.minusWeeks(1)
val bulanDepan  = hari ini.plusMonths(1)
val tahunLalu   = hari ini.minusYears(1)

// Info berguna
println(tanggalSpesifik.isLeapYear)       // false
println(tanggalSpesifik.lengthOfMonth())  // 31 (Agustus)
println(tanggalSpesifik.lengthOfYear())   // 366 jika kabisat, 365 jika tidak

// Awal dan akhir bulan/tahun
val awalBulan = tanggalSpesifik.withDayOfMonth(1)
val akhirBulan = tanggalSpesifik.withDayOfMonth(tanggalSpesifik.lengthOfMonth())
val awalTahun  = tanggalSpesifik.withDayOfYear(1)

LocalTime — Waktu Tanpa Tanggal #

LocalTime mewakili waktu dalam sehari (jam, menit, detik, nanodetik) tanpa informasi tanggal atau zona waktu.

import java.time.LocalTime

val sekarang = LocalTime.now()
val waktuSpesifik = LocalTime.of(14, 30, 0)        // 14:30:00
val denganDetik   = LocalTime.of(9, 0, 45, 500_000_000) // 09:00:45.500
val dariString    = LocalTime.parse("14:30:00")

println(waktuSpesifik.hour)        // 14
println(waktuSpesifik.minute)      // 30
println(waktuSpesifik.second)      // 0
println(waktuSpesifik.nano)        // 0

// Manipulasi
val satuJamKemudian = waktuSpesifik.plusHours(1)    // 15:30
val limaMenitSebelum = waktuSpesifik.minusMinutes(5) // 14:25

// Konstanta berguna
println(LocalTime.MIN)      // 00:00
println(LocalTime.MAX)      // 23:59:59.999999999
println(LocalTime.NOON)     // 12:00
println(LocalTime.MIDNIGHT) // 00:00

// Cek jam kerja
fun isJamKerja(waktu: LocalTime): Boolean {
    val mulai = LocalTime.of(9, 0)
    val selesai = LocalTime.of(17, 0)
    return !waktu.isBefore(mulai) && waktu.isBefore(selesai)
}

println(isJamKerja(LocalTime.of(10, 30)))  // true
println(isJamKerja(LocalTime.of(18, 0)))   // false

LocalDateTime — Tanggal dan Waktu #

LocalDateTime menggabungkan LocalDate dan LocalTime tanpa informasi zona waktu. Cocok untuk jadwal lokal seperti “rapat pada 24 Agustus 2024 pukul 10:30”.

import java.time.LocalDateTime

val sekarang = LocalDateTime.now()
val spesifik  = LocalDateTime.of(2024, 8, 24, 10, 30, 0)
val dariDate  = LocalDate.of(2024, 8, 24).atTime(10, 30)
val dariTime  = LocalDate.of(2024, 8, 24).atTime(LocalTime.of(10, 30))

// Ekstrak komponen
println(spesifik.toLocalDate())   // 2024-08-24
println(spesifik.toLocalTime())   // 10:30

// Manipulasi
val duaJamKemudian  = spesifik.plusHours(2)       // 2024-08-24T12:30
val tigarHariLagi   = spesifik.plusDays(3)         // 2024-08-27T10:30
val awalHari        = spesifik.toLocalDate().atStartOfDay()  // 2024-08-24T00:00

// Ubah komponen tertentu
val gantJam = spesifik.withHour(15)               // 2024-08-24T15:30
val gantTanggal = spesifik.withDayOfMonth(1)      // 2024-08-01T10:30

ZonedDateTime — Tanggal Waktu dengan Zona #

ZonedDateTime adalah LocalDateTime lengkap dengan zona waktu. Gunakan ini ketika berurusan dengan pengguna dari zona waktu yang berbeda, penjadwalan lintas zona, atau sistem terdistribusi.

import java.time.ZonedDateTime
import java.time.ZoneId

// Daftar zona waktu Indonesia
val wib  = ZoneId.of("Asia/Jakarta")       // UTC+7
val wita = ZoneId.of("Asia/Makassar")      // UTC+8
val wit  = ZoneId.of("Asia/Jayapura")      // UTC+9

val sekarangWib = ZonedDateTime.now(wib)
println(sekarangWib)  // 2024-08-24T10:30:00+07:00[Asia/Jakarta]

// Konversi antar zona waktu
val waktuJakarta = ZonedDateTime.of(2024, 8, 24, 10, 30, 0, 0, wib)
val waktuTokyo   = waktuJakarta.withZoneSameInstant(ZoneId.of("Asia/Tokyo"))
val waktuLondon  = waktuJakarta.withZoneSameInstant(ZoneId.of("Europe/London"))

println("Jakarta: ${waktuJakarta.toLocalTime()}")  // 10:30
println("Tokyo:   ${waktuTokyo.toLocalTime()}")    // 12:30 (+2 jam dari Jakarta)
println("London:  ${waktuLondon.toLocalTime()}")   // 03:30 (-7 jam dari Jakarta)

// Daftar semua zona waktu yang tersedia
ZoneId.getAvailableZoneIds()
    .filter { it.startsWith("Asia/") }
    .sorted()
    .forEach { println(it) }
LocalDateTime tidak mengandung informasi zona waktu. Ini berarti “2024-08-24T10:30” bisa berarti 10:30 WIB atau 10:30 UTC — ambigu. Saat menyimpan waktu yang perlu diinterpretasikan lintas zona (misalnya event kalender, jadwal penerbangan), selalu gunakan ZonedDateTime atau Instant.

Instant — Titik Waktu Universal #

Instant merepresentasikan satu titik waktu absolut di garis waktu universal — jumlah milidetik (atau nanodetik) sejak Unix epoch (1 Januari 1970 00:00:00 UTC). Inilah yang sebaiknya kamu simpan di database untuk timestamp.

import java.time.Instant
import java.time.ZoneId

val sekarang = Instant.now()
println(sekarang)  // 2024-08-24T03:30:00Z (selalu dalam UTC, ditandai 'Z')

// Dari epoch milis (misalnya dari System.currentTimeMillis())
val dariMilis = Instant.ofEpochMilli(System.currentTimeMillis())
val dariDetik = Instant.ofEpochSecond(1_724_464_200L)

// Ke epoch milis
println(sekarang.toEpochMilli())  // 1724464200000 (misalnya)

// Konversi Instant ke ZonedDateTime untuk tampilan lokal
val tampilWib = sekarang.atZone(ZoneId.of("Asia/Jakarta"))
println(tampilWib)  // 2024-08-24T10:30:00+07:00[Asia/Jakarta]

// Perbandingan
val t1 = Instant.now()
Thread.sleep(100)
val t2 = Instant.now()
println(t1.isBefore(t2))   // true
println(t2.isAfter(t1))    // true

Format dan Parsing #

DateTimeFormatter digunakan untuk mengubah objek tanggal/waktu ke String dan sebaliknya.

import java.time.format.DateTimeFormatter
import java.util.Locale

// Format bawaan
val isoDate = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)
println(isoDate)  // 2024-08-24

// Format kustom
val formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale("id", "ID"))
val tanggal = LocalDate.of(2024, 8, 17)
println(tanggal.format(formatter))  // 17 Agustus 2024

// Format waktu
val waktuFormatter = DateTimeFormatter.ofPattern("HH:mm:ss")
println(LocalTime.now().format(waktuFormatter))  // 10:30:45

// Format lengkap
val dtFormatter = DateTimeFormatter.ofPattern("EEEE, dd MMMM yyyy 'pukul' HH:mm", Locale("id", "ID"))
println(LocalDateTime.now().format(dtFormatter))
// Sabtu, 24 Agustus 2024 pukul 10:30

// Parsing String ke objek tanggal
val formatInput = DateTimeFormatter.ofPattern("dd/MM/yyyy")
val tglParsed = LocalDate.parse("17/08/2024", formatInput)
println(tglParsed)  // 2024-08-17

// Parsing yang aman (tidak throw exception)
fun parseLocalDateAman(input: String, pattern: String): LocalDate? {
    return runCatching {
        LocalDate.parse(input, DateTimeFormatter.ofPattern(pattern))
    }.getOrNull()
}

println(parseLocalDateAman("17/08/2024", "dd/MM/yyyy"))   // 2024-08-17
println(parseLocalDateAman("bukan tanggal", "dd/MM/yyyy")) // null

Pola Format Umum #

SimbolArtiContoh
yyyyTahun 4 digit2024
yyTahun 2 digit24
MMBulan 2 digit08
MMMNama bulan singkatAug
MMMMNama bulan penuhAugust
ddHari 2 digit17
EEENama hari singkatSat
EEEENama hari penuhSaturday
HHJam (00–23)14
hhJam (01–12)02
mmMenit30
ssDetik45
aAM/PMPM
zZona waktu singkatWIB
ZOffset UTC+0700

Duration dan Period #

Duration mengukur selisih berbasis waktu (jam, menit, detik), sementara Period mengukur selisih berbasis kalender (tahun, bulan, hari).

import java.time.Duration
import java.time.Period

// Duration — untuk selisih waktu dalam jam/menit/detik
val mulaiKerja = LocalTime.of(9, 0)
val selesaiKerja = LocalTime.of(17, 30)
val durKerja = Duration.between(mulaiKerja, selesaiKerja)

println(durKerja.toHours())   // 8
println(durKerja.toMinutes()) // 510
println(durKerja.toSeconds()) // 30600

// Duration antara dua Instant
val t1 = Instant.now()
Thread.sleep(1500)
val t2 = Instant.now()
val selisih = Duration.between(t1, t2)
println("Waktu berlalu: ${selisih.toMillis()} ms")  // sekitar 1500

// Buat Duration secara langsung
val durSatuJam = Duration.ofHours(1)
val durTigaPuluhMenit = Duration.ofMinutes(30)
val durGabung = durSatuJam.plus(durTigaPuluhMenit)
println(durGabung.toMinutes())  // 90

// Period — untuk selisih tanggal dalam tahun/bulan/hari
val lahir = LocalDate.of(1995, 3, 15)
val hari ini = LocalDate.now()
val umur = Period.between(lahir, hari ini)

println("Umur: ${umur.years} tahun ${umur.months} bulan ${umur.days} hari")

// Buat Period secara langsung
val tigaBulan = Period.ofMonths(3)
val deadline = LocalDate.now().plus(tigaBulan)
println("Deadline: $deadline")

// ChronoUnit untuk perhitungan total dalam satu satuan
import java.time.temporal.ChronoUnit

val hariHingga = ChronoUnit.DAYS.between(hari ini, LocalDate.of(2025, 1, 1))
println("Hari menuju tahun baru: $hariHingga")

val bulanHingga = ChronoUnit.MONTHS.between(hari ini, LocalDate.of(2025, 1, 1))
println("Bulan menuju tahun baru: $bulanHingga")

Perbandingan Tanggal dan Waktu #

val t1 = LocalDate.of(2024, 1, 1)
val t2 = LocalDate.of(2024, 8, 17)
val t3 = LocalDate.of(2024, 1, 1)

// Metode perbandingan
println(t1.isBefore(t2))   // true
println(t2.isAfter(t1))    // true
println(t1.isEqual(t3))    // true

// compareTo — berguna untuk sorting
println(t1.compareTo(t2))  // negatif (t1 lebih awal)
println(t2.compareTo(t1))  // positif (t2 lebih lambat)
println(t1.compareTo(t3))  // 0 (sama)

// Cek apakah tanggal dalam rentang tertentu
fun LocalDate.dalamRentang(awal: LocalDate, akhir: LocalDate): Boolean {
    return !this.isBefore(awal) && !this.isAfter(akhir)
}

val tanggal = LocalDate.of(2024, 4, 15)
val awalQ2 = LocalDate.of(2024, 4, 1)
val akhirQ2 = LocalDate.of(2024, 6, 30)
println(tanggal.dalamRentang(awalQ2, akhirQ2))  // true

// Sorting list of LocalDate
val tanggalAcak = listOf(
    LocalDate.of(2024, 8, 1),
    LocalDate.of(2024, 3, 15),
    LocalDate.of(2024, 6, 30)
)
println(tanggalAcak.sorted())  // [2024-03-15, 2024-06-30, 2024-08-01]

Pola Penggunaan di Aplikasi Nyata #

Menyimpan Waktu di Database #

// Rekomendasi: simpan sebagai Instant (timestamp UTC) di database
// Tampilkan ke pengguna sesuai zona waktu mereka

data class Transaksi(
    val id: Long,
    val jumlah: Double,
    val dibuatPada: Instant = Instant.now()  // simpan UTC
)

fun tampilkanTransaksi(t: Transaksi, zonaUser: ZoneId) {
    val waktuLokal = t.dibuatPada.atZone(zonaUser)
    val formatter = DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm", Locale("id", "ID"))
    println("Transaksi #${t.id}: Rp${t.jumlah} pada ${waktuLokal.format(formatter)}")
}

val trx = Transaksi(1, 150_000.0)
tampilkanTransaksi(trx, ZoneId.of("Asia/Jakarta"))   // WIB
tampilkanTransaksi(trx, ZoneId.of("Asia/Makassar"))  // WITA

Menghitung Umur #

fun hitungUmur(tanggalLahir: LocalDate): String {
    val sekarang = LocalDate.now()
    require(!tanggalLahir.isAfter(sekarang)) { "Tanggal lahir tidak boleh di masa depan" }

    val umur = Period.between(tanggalLahir, sekarang)
    return "${umur.years} tahun ${umur.months} bulan ${umur.days} hari"
}

println(hitungUmur(LocalDate.of(1995, 3, 15)))

Validasi Jam Operasional #

data class JamOperasional(val buka: LocalTime, val tutup: LocalTime) {
    fun isOpen(waktu: LocalTime = LocalTime.now()): Boolean {
        return !waktu.isBefore(buka) && waktu.isBefore(tutup)
    }

    fun sisaWaktuBuka(waktu: LocalTime = LocalTime.now()): Duration? {
        if (!isOpen(waktu)) return null
        return Duration.between(waktu, tutup)
    }
}

val tokoSupermarket = JamOperasional(LocalTime.of(8, 0), LocalTime.of(22, 0))
println(tokoSupermarket.isOpen(LocalTime.of(14, 0)))   // true
println(tokoSupermarket.isOpen(LocalTime.of(23, 0)))   // false
tokoSupermarket.sisaWaktuBuka(LocalTime.of(21, 30))?.let {
    println("Toko tutup dalam ${it.toMinutes()} menit")  // 30 menit
}

Ringkasan #

  • Pilih kelas yang tepatLocalDate untuk tanggal saja, LocalTime untuk waktu saja, LocalDateTime untuk keduanya tanpa zona, ZonedDateTime untuk waktu dengan zona, Instant untuk timestamp universal.
  • Instant untuk database — selalu simpan timestamp sebagai Instant (UTC) di database. Konversi ke zona waktu lokal hanya saat menampilkan ke pengguna.
  • Hindari java.util.Date dan Calendar — API lama ini mutable, tidak thread-safe, dan memiliki API yang membingungkan. Selalu gunakan java.time.
  • LocalDateTime tidak punya zona waktu — tanpa zona, “14:30” bisa berarti WIB atau UTC. Gunakan ZonedDateTime atau Instant untuk waktu yang perlu diinterpretasikan lintas zona.
  • Semua objek java.time immutable — operasi seperti plusDays() dan minusMonths() selalu menghasilkan objek baru. Tidak perlu khawatir satu objek diubah di tempat lain.
  • Duration untuk waktu, Period untuk tanggalDuration.between(t1, t2) untuk selisih jam/menit/detik; Period.between(d1, d2) untuk selisih tahun/bulan/hari. Gunakan ChronoUnit untuk total dalam satu satuan.
  • DateTimeFormatter dengan Locale — sertakan Locale("id", "ID") saat memformat nama bulan atau hari agar muncul dalam Bahasa Indonesia, bukan English.
  • Parsing yang aman dengan runCatchingLocalDate.parse() melempar exception untuk format yang salah. Bungkus dengan runCatching { }.getOrNull() untuk parsing yang aman dari input pengguna.

← Sebelumnya: Map   Berikutnya: Regex →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact