Duration #
Bekerja dengan rentang waktu — berapa lama suatu operasi berjalan, berapa lama menunggu sebelum retry, berapa sisa waktu sebuah session — adalah kebutuhan yang sangat umum. Sebelum Kotlin 1.6, developer harus bekerja langsung dengan millisecond atau second sebagai Long, yang rentan terhadap bug unit (lupa kalikan 1000, atau salah satuan). Kotlin memperkenalkan kotlin.time.Duration sebagai tipe nilai yang merepresentasikan durasi waktu secara type-safe: 5.seconds, 30.minutes, 2.hours — semua dengan tipe yang sama dan bisa dioperasikan satu sama lain. Dikombinasikan dengan measureTime dan measureTimedValue untuk benchmarking, Duration menjadi fondasi yang bersih untuk semua kebutuhan pengukuran dan manajemen waktu. Artikel ini membahas seluruh API Duration Kotlin dari pembuatan hingga penggunaan di coroutine dan profiling performa.
Membuat Duration #
Duration bisa dibuat dari angka menggunakan extension properties yang tersedia untuk Int, Long, dan Double.
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.microseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds
// Extension properties untuk membuat Duration
val limaDetik = 5.seconds
val tigaPuluhMenit = 30.minutes
val duaJam = 2.hours
val satuHari = 1.days
val seratusMs = 100.milliseconds
val seribuUs = 1000.microseconds
val sejutaNs = 1_000_000.nanoseconds
// Double juga didukung
val setengahDetik = 0.5.seconds // 500ms
val satuSetengahJam = 1.5.hours // 90 menit
val duaSetengahHari = 2.5.days // 60 jam
// Dari Long
val timeout: Duration = 30L.seconds
val ttl: Duration = 24L.hours
// Duration.ZERO dan INFINITE
val nol = Duration.ZERO
val tak_terbatas = Duration.INFINITE
println(limaDetik) // 5s
println(trigaPuluhMenit) // 30m
println(duaJam) // 2h
println(seratusMs) // 100ms
println(setengahDetik) // 500ms
flowchart LR
A["Angka"] --> B["Extension Properties"]
B --> C["Int / Long / Double"]
C --> D[".nanoseconds"]
C --> E[".microseconds"]
C --> F[".milliseconds"]
C --> G[".seconds"]
C --> H[".minutes"]
C --> I[".hours"]
C --> J[".days"]
D & E & F & G & H & I & J --> K["Duration\n(tipe tunggal)"]Konversi Antar Satuan #
Duration menyimpan nilai secara internal dalam nanosecond dengan presisi tinggi. Konversi ke satuan lain mudah dan ekspresif.
val durasi = 90.minutes
// Konversi ke Double
println(durasi.inWholeSeconds) // 5400 (Long)
println(durasi.inWholeMinutes) // 90 (Long)
println(durasi.inWholeHours) // 1 (Long — dibulatkan ke bawah)
println(durasi.inWholeDays) // 0 (Long)
println(durasi.inWholeMilliseconds) // 5_400_000 (Long)
println(durasi.inWholeNanoseconds) // 5_400_000_000_000 (Long)
// Konversi ke Double (mempertahankan pecahan)
println(durasi.inSeconds) // 5400.0 (Double)
println(durasi.inMinutes) // 90.0 (Double)
println(durasi.inHours) // 1.5 (Double)
println(durasi.inDays) // 0.0625 (Double)
// toComponents — pecah jadi komponen
durasi.toComponents { jam, menit, detik, nanosecond ->
println("$jam jam $menit menit $detik detik")
// "1 jam 30 menit 0 detik"
}
// Contoh: SLA 99.9% uptime dalam setahun
val setahun = 365.days
val sla999 = setahun * 0.001 // 0.1% downtime yang diizinkan
sla999.toComponents { jam, menit, _, _ ->
println("Downtime maksimal: $jam jam $menit menit per tahun")
// "8 jam 45 menit"
}
Parsing dari String #
// Duration.parse — membaca dari string ISO 8601 atau format Kotlin
val d1 = Duration.parse("PT5M") // 5 menit (ISO 8601)
val d2 = Duration.parse("PT1H30M") // 1 jam 30 menit
val d3 = Duration.parse("5m") // 5 menit (format Kotlin)
val d4 = Duration.parse("1h 30m") // 1 jam 30 menit
val d5 = Duration.parse("2d 3h 15m 30s") // 2 hari 3 jam 15 menit 30 detik
// parseOrNull — aman untuk input tidak terpercaya
val valid = Duration.parseOrNull("30s") // 30s
val invalid = Duration.parseOrNull("xyz") // null
// Parsing dari millisecond (dari database atau API)
val fromMs = 5_400_000L.milliseconds // dari long millisecond
val fromSeconds = 5400.seconds // dari long second (Unix timestamp diff)
Operasi Aritmatika #
Duration mendukung operasi matematika yang intuitif.
val satuJam = 1.hours
val tigaPuluhMenit = 30.minutes
val limaDetik = 5.seconds
// Penjumlahan dan pengurangan
val satuSetengahJam = satuJam + tigaPuluhMenit // 1h 30m
val setengahJam = satuJam - tigaPuluhMenit // 30m
val selisih = satuJam - 90.minutes // Duration.ZERO? — bisa negatif!
// Duration bisa negatif
val negatif = 30.minutes - 1.hours // -30m
println(negatif.isNegative()) // true
println(negatif.absoluteValue) // 30m
// Perkalian dan pembagian dengan scalar
val tigaJam = satuJam * 3 // 3h
val duapuluhMenit = satuJam / 3 // 20m
val duaKali = limaDetik * 2.0 // 10s
val setengah = satuJam * 0.5 // 30m
// Pembagian Duration dengan Duration — menghasilkan Double
val rasio = satuSetengahJam / satuJam // 1.5
// Perbandingan
println(satuJam > tigaPuluhMenit) // true
println(limaDetik < trigaPuluhMenit) // true
println(satuJam == 60.minutes) // true
println(satuJam.compareTo(90.minutes)) // negatif (kurang dari)
// Operasi berguna lainnya
println(satuJam.coerceAtMost(30.minutes)) // 30m
println(limaDetik.coerceAtLeast(1.minutes)) // 1m
// Pengecekan
println(Duration.ZERO.isPositive()) // false
println(5.seconds.isPositive()) // true
println((-5).seconds.isNegative()) // true
println(Duration.INFINITE.isInfinite()) // true
measureTime — Mengukur Waktu Eksekusi #
measureTime mengeksekusi blok kode dan mengembalikan Duration yang merepresentasikan waktu yang dibutuhkan.
import kotlin.time.measureTime
// Penggunaan dasar
val waktu: Duration = measureTime {
Thread.sleep(100) // simulasi operasi yang memakan waktu
}
println("Waktu eksekusi: $waktu") // "Waktu eksekusi: 100ms" (kira-kira)
// Benchmarking fungsi
fun hitungFibonacci(n: Int): Long = when (n) {
0, 1 -> n.toLong()
else -> hitungFibonacci(n - 1) + hitungFibonacci(n - 2)
}
val waktuFib = measureTime {
hitungFibonacci(40)
}
println("Fibonacci(40) membutuhkan: $waktuFib")
// Membandingkan dua implementasi
val waktuLambat = measureTime {
repeat(1_000_000) { i -> i * i }
}
val hasilCached = (0..999_999).map { it * it }
val waktuCepat = measureTime {
hasilCached.forEach { it }
}
println("Tanpa cache: $waktuLambat")
println("Dengan cache: $waktuCepat")
// Benchmark sederhana dengan multiple runs
fun benchmark(label: String, ulang: Int = 10, blok: () -> Unit): Duration {
// Warmup
repeat(3) { blok() }
val total = measureTime {
repeat(ulang) { blok() }
}
val rataRata = total / ulang
println("[$label] Total: $total | Rata-rata: $rataRata per iterasi")
return rataRata
}
benchmark("String concat", 100) {
var s = ""
repeat(1000) { s += "x" }
}
benchmark("StringBuilder", 100) {
val sb = StringBuilder()
repeat(1000) { sb.append("x") }
sb.toString()
}
measureTimedValue — Nilai dan Waktu Sekaligus #
measureTimedValue seperti measureTime tapi juga mengembalikan nilai yang dihasilkan oleh blok.
import kotlin.time.measureTimedValue
// Mengembalikan TimedValue<T> yang berisi value dan duration
val (hasil, waktu) = measureTimedValue {
hitungFibonacci(35)
}
println("Fibonacci(35) = $hasil, dihitung dalam $waktu")
// Tipe eksplisit
val timedResult: kotlin.time.TimedValue<List<Int>> = measureTimedValue {
(1..1000).filter { it % 2 == 0 }.map { it * it }
}
println("${timedResult.value.size} elemen, membutuhkan ${timedResult.duration}")
// Berguna untuk logging performa di produksi
fun <T> withTiming(label: String, threshold: Duration = 100.milliseconds, blok: () -> T): T {
val (nilai, durasi) = measureTimedValue(blok)
if (durasi > threshold) {
println("LAMBAT [$label]: $durasi (threshold: $threshold)")
} else {
println("OK [$label]: $durasi")
}
return nilai
}
val data = withTiming("ambil data", threshold = 200.milliseconds) {
ambilDataDariDatabase()
}
// Profiling middleware di Ktor
fun Application.configureMonitoring() {
intercept(ApplicationCallPipeline.Monitoring) {
val (_, durasi) = measureTimedValue {
proceed()
}
val path = call.request.path()
if (durasi > 500.milliseconds) {
log.warn("Slow request: $path took $durasi")
}
}
}
Duration di Coroutine #
Duration terintegrasi secara alami dengan coroutine Kotlin, terutama untuk delay dan withTimeout.
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.minutes
// delay dengan Duration — lebih ekspresif dari delay(milliseconds)
suspend fun prosesAsync() {
// ANTI-PATTERN: delay dengan angka magic dalam millisecond
delay(5000) // 5 detik? 5000 ms? tidak jelas
// BENAR: delay dengan Duration
delay(5.seconds)
delay(30.minutes)
delay(1.5.seconds) // 1500ms
}
// withTimeout dengan Duration
suspend fun ambilDataDenganTimeout(): String = withTimeout(10.seconds) {
// Jika operasi ini lebih dari 10 detik, TimeoutCancellationException dilempar
ambilDataDariApi()
}
// withTimeoutOrNull — mengembalikan null jika timeout
suspend fun ambilDataAman(): String? = withTimeoutOrNull(5.seconds) {
ambilDataDariApi()
}
// Implementasi retry dengan Duration
suspend fun <T> retryDenganDelay(
maxPercobaan: Int = 3,
jedaAwal: Duration = 1.seconds,
faktorBackoff: Double = 2.0,
blok: suspend () -> T
): T {
var percobaan = 0
var jeda = jedaAwal
while (true) {
try {
return blok()
} catch (e: Exception) {
percobaan++
if (percobaan >= maxPercobaan) throw e
println("Percobaan $percobaan gagal, menunggu $jeda...")
delay(jeda)
jeda = (jeda * faktorBackoff).coerceAtMost(30.seconds)
}
}
}
// Penggunaan
suspend fun main() {
val data = retryDenganDelay(maxPercobaan = 5, jedaAwal = 500.milliseconds) {
ambilDataDariApi()
}
}
Formatting Duration #
val durasi = 1.hours + 23.minutes + 45.seconds + 500.milliseconds
// toString bawaan — format singkat
println(durasi) // "1h 23m 45.5s"
println(5.seconds) // "5s"
println(1500.milliseconds) // "1.5s"
println(90.minutes) // "1h 30m"
println(0.5.seconds) // "500ms"
println(100.nanoseconds) // "100ns"
// Format kustom untuk tampilan pengguna
fun Duration.formatKustom(): String {
return toComponents { jam, menit, detik, _ ->
buildString {
if (jam > 0) append("${jam}j ")
if (menit > 0) append("${menit}m ")
if (detik > 0 || (jam == 0L && menit == 0)) append("${detik}d")
}.trim()
}
}
println(1.hours.formatKustom()) // "1j"
println((1.hours + 30.minutes).formatKustom()) // "1j 30m"
println((2.hours + 5.minutes + 30.seconds).formatKustom()) // "2j 5m 30d"
// Format ISO 8601
fun Duration.toISO8601(): String {
return toComponents { jam, menit, detik, nanosecond ->
buildString {
append("PT")
if (jam > 0) append("${jam}H")
if (menit > 0) append("${menit}M")
val detikDesimal = detik + nanosecond / 1_000_000_000.0
if (detikDesimal > 0) append("${detikDesimal}S")
}
}
}
// Format countdowntimer — HH:MM:SS
fun Duration.formatCountdown(): String {
return toComponents { jam, menit, detik, _ ->
"%02d:%02d:%02d".format(jam, menit, detik)
}
}
println((2.hours + 5.minutes + 7.seconds).formatCountdown()) // "02:05:07"
println(45.minutes.formatCountdown()) // "00:45:00"
// Format human-readable relatif
fun Duration.formatRelatif(): String = when {
this < 1.seconds -> "baru saja"
this < 1.minutes -> "${inWholeSeconds} detik lalu"
this < 1.hours -> "${inWholeMinutes} menit lalu"
this < 1.days -> "${inWholeHours} jam lalu"
else -> "${inWholeDays} hari lalu"
}
println(30.seconds.formatRelatif()) // "30 detik lalu"
println(45.minutes.formatRelatif()) // "45 menit lalu"
println(3.hours.formatRelatif()) // "3 jam lalu"
println(2.days.formatRelatif()) // "2 hari lalu"
Pola Idiomatik dalam Kode Produksi #
Cache dengan TTL (Time-To-Live) #
import kotlin.time.TimeSource
class Cache<K, V>(private val ttl: Duration) {
private val sumber = TimeSource.Monotonic
private val data = mutableMapOf<K, Pair<V, kotlin.time.TimeMark>>()
fun simpan(kunci: K, nilai: V) {
data[kunci] = nilai to sumber.markNow()
}
fun ambil(kunci: K): V? {
val (nilai, waktuSimpan) = data[kunci] ?: return null
return if (waktuSimpan.elapsedNow() < ttl) nilai else {
data.remove(kunci)
null
}
}
fun bersihkan() {
val sekarang = sumber.markNow()
data.entries.removeIf { (_, v) -> v.second.elapsedNow() >= ttl }
}
}
// Penggunaan
val cache = Cache<String, String>(ttl = 5.minutes)
cache.simpan("user:1", "Andi")
val user = cache.ambil("user:1") // "Andi" jika belum 5 menit
Rate Limiter Sederhana #
class RateLimiter(
private val maksPermintaan: Int,
private val jendela: Duration
) {
private val sumber = TimeSource.Monotonic
private val histori = ArrayDeque<kotlin.time.TimeMark>()
@Synchronized
fun izinkan(): Boolean {
val sekarang = sumber.markNow()
// Hapus histori di luar jendela waktu
while (histori.isNotEmpty() && histori.first().elapsedNow() > jendela) {
histori.removeFirst()
}
return if (histori.size < maksPermintaan) {
histori.addLast(sekarang)
true
} else false
}
}
// Maksimal 10 permintaan per menit
val limiter = RateLimiter(maksPermintaan = 10, jendela = 1.minutes)
fun tanganiPermintaan() {
if (!limiter.izinkan()) {
throw TooManyRequestsException("Rate limit tercapai")
}
// proses permintaan
}
TimeSource — Monotonic Clock #
import kotlin.time.TimeSource
// TimeSource.Monotonic — lebih andal dari System.currentTimeMillis untuk mengukur durasi
// Tidak terpengaruh perubahan jam sistem atau daylight saving time
val sumber = TimeSource.Monotonic
val mulai = sumber.markNow()
// ... lakukan sesuatu ...
Thread.sleep(250)
val elapsed: Duration = mulai.elapsedNow()
println("Berlalu: $elapsed") // "Berlalu: 250ms" (kira-kira)
// Perbandingan dua TimeMark
val mark1 = sumber.markNow()
Thread.sleep(100)
val mark2 = sumber.markNow()
println(mark1 < mark2) // true — mark1 lebih awal
println(mark2 - mark1) // ~100ms — selisih antar mark
// Menggunakan TimeMark untuk deadline
val deadline = sumber.markNow() + 5.seconds
while (deadline.hasNotPassedNow()) {
// Lakukan sesuatu dalam batas waktu 5 detik
Thread.sleep(100)
}
println("Waktu habis!")
Duration vs Long Millisecond #
// ANTI-PATTERN: millisecond sebagai Long — rentan bug satuan
fun buatKoneksi(timeoutMs: Long) { /* ... */ }
fun cacheData(dataTtlMs: Long) { /* ... */ }
// Mudah salah:
buatKoneksi(30) // 30 ms? atau lupa kalikan 1000?
buatKoneksi(30_000) // 30 detik — tapi tidak jelas dari code
cacheData(5 * 60 * 1000) // 5 menit — verbose dan error-prone
// BENAR: gunakan Duration
fun buatKoneksi(timeout: Duration) { /* ..., gunakan timeout.inWholeMilliseconds */ }
fun cacheData(ttl: Duration) { /* ..., gunakan ttl.inWholeSeconds */ }
buatKoneksi(30.seconds) // jelas: 30 detik
buatKoneksi(30.minutes) // jelas: 30 menit
cacheData(5.minutes) // jelas: 5 menit
// Interoperabilitas dengan API Java yang pakai Long
fun koneksiBawaan(url: String, timeout: Duration) {
val koneksi = java.net.URL(url).openConnection()
koneksi.connectTimeout = timeout.inWholeMilliseconds.toInt()
koneksi.readTimeout = timeout.inWholeMilliseconds.toInt()
}
Ringkasan #
- Extension properties seperti
5.seconds,30.minutes,2.hoursadalah cara idiomatik membuat Duration — jauh lebih jelas dari angka millisecond mentah yang rentan bug satuan.inWholeSeconds,inWholeMinutes,inWholeHours(Long) untuk nilai bulat;inSeconds,inMinutes(Double) untuk mempertahankan pecahan. Gunakan yang sesuai kebutuhan.toComponents { jam, menit, detik, ns -> }untuk memecah Duration menjadi komponen individual — berguna untuk formatting dan tampilan timer.measureTime { }mengukur waktu eksekusi dan mengembalikanDuration.measureTimedValue { }mengembalikanPairberisi nilai dan durasi — gunakan keduanya untuk profiling performa.delay(duration)danwithTimeout(duration)di coroutine menerimaDurationlangsung — gunakan ini daripadadelay(milliseconds)yang tidak jelas satuannya.TimeSource.Monotoniclebih andal dariSystem.currentTimeMillis()untuk mengukur durasi — tidak terpengaruh perubahan jam sistem. GunakanmarkNow()danelapsedNow()untuk pengukuran presisi.- Duration mendukung operasi matematika lengkap: penjumlahan, pengurangan, perkalian, pembagian, perbandingan,
coerceAtMost,coerceAtLeast, danabsoluteValue.- Duration bisa negatif —
30.minutes - 1.hoursmenghasilkan-30m. GunakanisNegative(),isPositive(), danabsoluteValueuntuk penanganan kasus edge.- Parsing dari String:
Duration.parse("PT1H30M")untuk format ISO 8601,Duration.parse("1h 30m")untuk format Kotlin, danDuration.parseOrNull()untuk penanganan aman input tidak valid.- Gunakan Duration sebagai parameter fungsi alih-alih
Longmillisecond — ini membuat API lebih ekspresif, aman, dan tidak ambigu tentang satuan waktu yang diharapkan.