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) }
LocalDateTimetidak 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 gunakanZonedDateTimeatauInstant.
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 #
| Simbol | Arti | Contoh |
|---|---|---|
yyyy | Tahun 4 digit | 2024 |
yy | Tahun 2 digit | 24 |
MM | Bulan 2 digit | 08 |
MMM | Nama bulan singkat | Aug |
MMMM | Nama bulan penuh | August |
dd | Hari 2 digit | 17 |
EEE | Nama hari singkat | Sat |
EEEE | Nama hari penuh | Saturday |
HH | Jam (00–23) | 14 |
hh | Jam (01–12) | 02 |
mm | Menit | 30 |
ss | Detik | 45 |
a | AM/PM | PM |
z | Zona waktu singkat | WIB |
Z | Offset 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 tepat —
LocalDateuntuk tanggal saja,LocalTimeuntuk waktu saja,LocalDateTimeuntuk keduanya tanpa zona,ZonedDateTimeuntuk waktu dengan zona,Instantuntuk timestamp universal.Instantuntuk database — selalu simpan timestamp sebagaiInstant(UTC) di database. Konversi ke zona waktu lokal hanya saat menampilkan ke pengguna.- Hindari
java.util.DatedanCalendar— API lama ini mutable, tidak thread-safe, dan memiliki API yang membingungkan. Selalu gunakanjava.time.LocalDateTimetidak punya zona waktu — tanpa zona, “14:30” bisa berarti WIB atau UTC. GunakanZonedDateTimeatauInstantuntuk waktu yang perlu diinterpretasikan lintas zona.- Semua objek
java.timeimmutable — operasi sepertiplusDays()danminusMonths()selalu menghasilkan objek baru. Tidak perlu khawatir satu objek diubah di tempat lain.Durationuntuk waktu,Perioduntuk tanggal —Duration.between(t1, t2)untuk selisih jam/menit/detik;Period.between(d1, d2)untuk selisih tahun/bulan/hari. GunakanChronoUnituntuk total dalam satu satuan.DateTimeFormatterdenganLocale— sertakanLocale("id", "ID")saat memformat nama bulan atau hari agar muncul dalam Bahasa Indonesia, bukan English.- Parsing yang aman dengan
runCatching—LocalDate.parse()melempar exception untuk format yang salah. Bungkus denganrunCatching { }.getOrNull()untuk parsing yang aman dari input pengguna.