Bekerja dengan tanggal hampir selalu melibatkan perhitungan: kapan kedaluwarsa, berapa lama pengguna berlangganan, kapan pengingat berikutnya, atau berapa hari antara dua momen. java.time menyediakan dua kelompok operasi: aritmatika berbasis kalender (plusDays, plusMonths, Period) yang menghormati panjang bulan, dan aritmatika berbasis waktu absolut (plus(Duration), ChronoUnit.SECONDS.between) yang menghitung secara presisi. Bab ini membedah keduanya dan menyoroti jebakan Period versus Duration.

Penambahan dan Pengurangan #

Setiap tipe temporal di java.time menyediakan method plusXxx(...) dan minusXxx(...). Operasi ini tidak mengubah instance asli (semua tipe java.time bersifat immutable), melainkan mengembalikan instance baru.

OperasiTipeMenghormati Kalender?Contoh
plusDays(7)LocalDateYa15 Jan + 7 hari = 22 Jan
plusMonths(1)LocalDateYa31 Jan + 1 bln = 28 Feb
plus(Duration)LocalDateTimeTidak15 Jan 08:30 + 24 jam
plus(Period)LocalDateYa15 Jan + P1M15D
minusYears(1)LocalDateYa2026 → 2025

Perbedaan Period dan Duration adalah sumber bug tersekat. Duration.ofDays(1) selalu 24 jam absolut, sedangkan Period.ofDays(1) berarti “tanggal besok” yang bisa 23 atau 25 jam saat daylight saving time.

Contoh Lengkap: Penambahan, Period vs Duration, ChronoUnit #

Kode berikut mendemonstrasikan enam skenario aritmatika, termasuk perbedaan Period vs Duration dan perhitungan selisih antar tanggal dengan dua pendekatan:

kotlin
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.Period
import java.time.temporal.ChronoUnit

fun main() {
    val tanggal: LocalDate = LocalDate.of(2026, 1, 15)
    val jadwal: LocalDateTime = LocalDateTime.of(2026, 1, 15, 8, 30)

    // 1. Penambahan berbasis tanggal: plusDays, plusWeeks, plusMonths, plusYears
    println("=== Penambahan Period (kalender) ===")
    println("tanggal                : $tanggal")
    println("plusDays(7)            : ${tanggal.plusDays(7)}")
    println("plusWeeks(2)           : ${tanggal.plusWeeks(2)}")
    println("plusMonths(1)          : ${tanggal.plusMonths(1)}")
    println("plusYears(1)          : ${tanggal.plusYears(1)}")
    println("minusDays(10)          : ${tanggal.minusDays(10)}")

    // 2. Penambahan Period sebagai objek
    val periodeHalal: Period = Period.of(0, 6, 15)
    println()
    println("=== Period sebagai objek ===")
    println("periodeHalal           : $periodeHalal")
    println("tanggal.plus(periode)  : ${tanggal.plus(periodeHalal)}")
    println("tanggal.minus(periode) : ${tanggal.minus(periodeHalal)}")

    // 3. Penambahan berbasis jam: plusHours, plusMinutes via Duration
    println()
    println("=== Penambahan Duration (jam/menit/detik) ===")
    val durasiRapat: Duration = Duration.ofMinutes(90)
    val durasiIstirahat: Duration = Duration.ofHours(2).plusMinutes(15)
    println("jadwal                 : $jadwal")
    println("plus(durasiRapat)      : ${jadwal.plus(durasiRapat)}")
    println("plus(durasiIstirahat)  : ${jadwal.plus(durasiIstirahat)}")
    println("minusNanos(500)        : ${jadwal.minusNanos(500)}")

    // 4. Period vs Duration: mengapa 1 hari bisa berbeda
    println()
    println("=== Period vs Duration ===")
    val periodeSehari: Period = Period.ofDays(1)
    val durasiSehari: Duration = Duration.ofDays(1)
    println("Period.ofDays(1)       : $periodeSehari  (kalender)")
    println("Duration.ofDays(1)     : $durasiSehari  (24 jam absolut)")
    println("tanggal + Period(1 day): ${tanggal.plus(periodeSehari)}")
    println("jadwal + Duration(1 day): ${jadwal.plus(durasiSehari)}")

    // 5. Menghitung selisih antar tanggal
    println()
    println("=== Selisih antar tanggal ===")
    val awal = LocalDate.of(2026, 1, 15)
    val akhir = LocalDate.of(2026, 3, 2)
    val selisihPeriod: Period = Period.between(awal, akhir)
    val selisihHari: Long = ChronoUnit.DAYS.between(awal, akhir)
    val selisihBulan: Long = ChronoUnit.MONTHS.between(awal, akhir)
    println("awal                   : $awal")
    println("akhir                  : $akhir")
    println("Period.between         : $selisihPeriod")
    println("DAYS.between           : $selisihHari")
    println("MONTHS.between         : $selisihBulan")

    // 6. Menghitung selisih antar instan (jam/detik)
    println()
    println("=== Selisih antar instan ===")
    val a = LocalDateTime.of(2026, 1, 15, 8, 0)
    val b = LocalDateTime.of(2026, 1, 15, 10, 45)
    val selisihDurasi: Duration = Duration.between(a, b)
    println("a                      : $a")
    println("b                      : $b")
    println("Duration.between       : $selisihDurasi (${selisihDurasi.toMinutes()} menit)")
    println("HOURS.between          : ${ChronoUnit.HOURS.between(a, b)}")
    println("MINUTES.between        : ${ChronoUnit.MINUTES.between(a, b)}")

    // 7. Iterasi tanggal per minggu
    println()
    println("=== Iterasi per minggu ===")
    var cursor = tanggal
    repeat(4) { i ->
        println("minggu ${i + 1}: $cursor (${cursor.dayOfWeek})")
        cursor = cursor.plusWeeks(1)
    }
}

Output yang Dihasilkan:

text
=== Penambahan Period (kalender) ===
tanggal                : 2026-01-15
plusDays(7)            : 2026-01-22
plusWeeks(2)           : 2026-01-29
plusMonths(1)          : 2026-02-15
plusYears(1)          : 2027-01-15
minusDays(10)          : 2026-01-05

=== Period sebagai objek ===
periodeHalal           : P6M15D
tanggal.plus(periode)  : 2026-07-30
tanggal.minus(periode) : 2025-06-30

=== Penambahan Duration (jam/menit/detik) ===
jadwal                 : 2026-01-15T08:30
plus(durasiRapat)      : 2026-01-15T10:00
plus(durasiIstirahat)  : 2026-01-15T10:45
minusNanos(500)        : 2026-01-15T08:29:59.999999500

=== Period vs Duration ===
Period.ofDays(1)       : P1D  (kalender)
Duration.ofDays(1)     : PT24H  (24 jam absolut)
tanggal + Period(1 day): 2026-01-16
jadwal + Duration(1 day): 2026-01-16T08:30

=== Selisih antar tanggal ===
awal                   : 2026-01-15
akhir                  : 2026-03-02
Period.between         : P1M15D
DAYS.between           : 46
MONTHS.between         : 1

=== Selisih antar instan ===
a                      : 2026-01-15T08:00
b                      : 2026-01-15T10:45
Duration.between       : PT2H45M (165 menit)
HOURS.between          : 2
MINUTES.between        : 165

=== Iterasi per minggu ===
minggu 1: 2026-01-15 (THURSDAY)
minggu 2: 2026-01-22 (THURSDAY)
minggu 3: 2026-01-29 (THURSDAY)
minggu 4: 2026-02-05 (THURSDAY)

Mari kita telaah baris-baris kode di atas:

  1. Baris 14-18 (plusDays(7), plusMonths(1)): method ini mengembalikan instance baru, instance tanggal asli tidak berubah. plusMonths(1) pada 15 Januari 2026 menghasilkan 15 Februari 2026 — ini aman karena 15 ada di bulan Februari. Namun plusMonths(1) pada 31 Januari akan menghasilkan 28 Februari (atau 29 di tahun kabisat), bukan 3 Maret, karena java.time memotong ke akhir bulan.
  2. Baris 23 (Period.of(0, 6, 15)): membuat periode 6 bulan 15 hari. Period.of(years, months, days) menerima tiga komponen. Menambahkannya ke 15 Januari 2026 menghasilkan 30 Juli 2026 (15 Jan + 6 bulan = 15 Jul; + 15 hari = 30 Jul).
  3. Baris 32-33 (Duration.ofHours(2).plusMinutes(15)): durasi dapat dirantai. Hasilnya PT2H15M setara dengan Duration.ofMinutes(135). LocalDateTime.plus(Duration) menambahkan durasi absolut ke jam.
  4. Baris 41-46 (Period vs Duration): Period.ofDays(1) ditambahkan ke LocalDate menghasilkan tanggal besok. Duration.ofDays(1) ditambahkan ke LocalDateTime menghasilkan jam 24 jam kemudian. Pada tanggal yang sama tanpa DST, hasilnya tampak sama; perbedaan muncul saat ada perubahan DST.
  5. Baris 52 (Period.between(awal, akhir)): menghasilkan P1M15D — 1 bulan 15 hari. ChronoUnit.DAYS.between menghasilkan 46 hari total. Keduanya benar namun melaporkan satuan berbeda — pilih sesuai kebutuhan tampilan.
  6. Baris 64 (Duration.between(a, b)): untuk LocalDateTime bekerja seperti Instant. Hasilnya PT2H45M = 165 menit. ChronoUnit.MINUTES.between langsung memberi angka menit (165), sedangkan HOURS.between membulatkan ke bawah menjadi 2 (kehilangan 45 menit).

Strategi Iterasi dan Akumulasi #

Iterasi tanggal per minggu, per bulan, atau per tahun dapat dilakukan dengan loop sederhana seperti pada blok 7. Untuk iterasi panjang (mis. 5 tahun per minggu = 260 iterasi), pendekatan ini tetap efisien karena LocalDate ringan. Alternatif idiomatik Kotlin menggunakan sequence:

kotlin Pola: sequence untuk rentang tanggal
fun rentangMingguan(awal: LocalDate, jumlah: Int): Sequence<LocalDate> = sequence {
    var cursor = awal
    repeat(jumlah) {
        yield(cursor)
        cursor = cursor.plusWeeks(1)
    }
}

rentangMingguan(LocalDate.of(2026, 1, 15), 4).forEach { println(it) }

Pola ini memproses tanggal secara lazy, cocok bila jumlah iterasi besar atau bila kita ingin memutus lebih awal dengan takeWhile.

Referensi Lanjutan #

  • java.time package — dokumentasi java.time untuk Period, Duration, dan operasi aritmetika tanggal-jam

Selanjutnya #

Konsep individual sudah lengkap — saatnya menyatukannya menjadi praktik terbaik. Bab Date-Time Best Practices akan merangkum pedoman memilih tipe yang benar, strategi penyimpanan, dan jebakan umum yang harus dihindari.