Tampilan tanggal ke pengguna jarang memakai format ISO 2026-01-15. Pengguna Indonesia lebih akrab dengan 15 Januari 2026, sedangkan pengguna Amerika membaca January 15, 2026. Untuk kasus ini, java.time menyediakan DateTimeFormatter dengan tiga sumber pola: predefined formatters ISO, localized formatters berbasis FormatStyle, dan custom pattern via ofPattern. Bab ini membedah ketiganya, termasuk cara mengaitkan Locale agar nama hari dan bulan tampil dalam bahasa yang benar.

Tiga Sumber Pola Formatter #

Sumber PolaKapan DipakaiContoh
Predefined ISO (ISO_LOCAL_DATE, dll.)Pertukaran antar sistem, log2026-01-15
FormatStyle + LocaleTampilan pengguna dengan bahasa lokal15 Januari 2026
ofPattern("dd MMM yyyy")Kontrol presisi karakter per komponen15 Jan 2026

Pilihan strategi bergantung pada kebutuhan: ISO untuk pertukaran data, FormatStyle untuk UI standar per regional, dan custom pattern untuk tampilan terstruktur khusus (mis. faktur, laporan PDF).

Contoh Lengkap: Predefined, Style, dan Custom Pattern #

Kode berikut mendemonstrasikan ketiga sumber pola secara berurutan, termasuk format locale-aware untuk Bahasa Indonesia, English (US), dan Jepang:

kotlin
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale

fun main() {
    val tanggal: LocalDate = LocalDate.of(2026, 1, 15)
    val jadwal: LocalDateTime = LocalDateTime.of(2026, 1, 15, 8, 30)
    val instan = ZonedDateTime.of(jadwal, ZoneId.of("Asia/Jakarta"))

    // 1. Predefined formatters (ISO)
    println("=== Predefined ISO ===")
    println("ISO_DATE        : ${DateTimeFormatter.ISO_DATE.format(tanggal)}")
    println("ISO_LOCAL_DATE  : ${DateTimeFormatter.ISO_LOCAL_DATE.format(tanggal)}")
    println("ISO_DATE_TIME   : ${DateTimeFormatter.ISO_DATE_TIME.format(instan)}")
    println("ISO_INSTANT     : ${DateTimeFormatter.ISO_INSTANT.format(instan.toInstant())}")

    // 2. Predefined formatters (FormatStyle + Locale)
    println()
    println("=== FormatStyle ===")
    val fmtTanggalID = DateTimeFormatter
        .ofLocalizedDate(FormatStyle.LONG)
        .withLocale(Locale.of("id", "ID"))
    val fmtTanggalUS = DateTimeFormatter
        .ofLocalizedDate(FormatStyle.LONG)
        .withLocale(Locale.US)
    println("LONG (id-ID)    : ${fmtTanggalID.format(tanggal)}")
    println("LONG (en-US)    : ${fmtTanggalUS.format(tanggal)}")

    val fmtMediumID = DateTimeFormatter
        .ofLocalizedDate(FormatStyle.MEDIUM)
        .withLocale(Locale.of("id", "ID"))
    println("MEDIUM (id-ID)  : ${fmtMediumID.format(tanggal)}")

    // 3. Custom pattern via ofPattern
    println()
    println("=== Custom pattern ===")
    val fmt1 = DateTimeFormatter.ofPattern("dd MMM yyyy")
    val fmt2 = DateTimeFormatter.ofPattern("EEEE, dd MMMM yyyy", Locale.of("id", "ID"))
    val fmt3 = DateTimeFormatter.ofPattern("HH:mm:ss | dd-MM-yyyy")
    val fmt4 = DateTimeFormatter.ofPattern("hh:mm a, MMM d yyyy", Locale.US)

    println("dd MMM yyyy     : ${fmt1.format(tanggal)}")
    println("EEEE (id-ID)    : ${fmt2.format(tanggal)}")
    println("HH:mm:ss | ...  : ${fmt3.format(jadwal)}")
    println("hh:mm a (US)    : ${fmt4.format(jadwal)}")

    // 4. Format dengan ZoneId (RFC-1123)
    val fmtRfc1123 = DateTimeFormatter.RFC_1123_DATE_TIME
    println()
    println("=== RFC-1123 (HTTP headers) ===")
    println("RFC_1123         : ${fmtRfc1123.format(instan)}")

    // 5. Locale-aware bulan dan hari
    println()
    println("=== Locale-aware month/day ===")
    val zonaJakarta = ZoneId.of("Asia/Jakarta")
    val hariJakarta = jadwal.atZone(zonaJakarta)
    val fmtFullID = DateTimeFormatter
        .ofPattern("EEEE, dd MMMM yyyy 'pukul' HH:mm")
        .withLocale(Locale.of("id", "ID"))
    println("Full (id-ID)     : ${fmtFullID.format(hariJakarta)}")

    val fmtFullJP = DateTimeFormatter
        .ofPattern("EEEE, dd MMMM yyyy 'at' HH:mm")
        .withLocale(Locale.JAPAN)
    println("Full (ja-JP)     : ${fmtFullJP.format(hariJakarta)}")

    // 6. Format Instant langsung (perlu zona)
    val instanUtc = hariJakarta.toInstant()
    val fmtInstant = DateTimeFormatter
        .ofPattern("yyyy-MM-dd HH:mm:ss z")
        .withZone(ZoneId.of("UTC"))
    println()
    println("=== Format Instant (UTC) ===")
    println("format UTC       : ${fmtInstant.format(instanUtc)}")
}

Output yang Dihasilkan:

text
=== Predefined ISO ===
ISO_DATE        : 2026-01-15
ISO_LOCAL_DATE  : 2026-01-15
ISO_DATE_TIME   : 2026-01-15T08:30:00+07:00[Asia/Jakarta]
ISO_INSTANT     : 2026-01-15T01:30:00Z

=== FormatStyle ===
LONG (id-ID)    : 15 Januari 2026
LONG (en-US)    : January 15, 2026
MEDIUM (id-ID)  : 15 Jan 2026

=== Custom pattern ===
dd MMM yyyy     : 15 Jan 2026
EEEE (id-ID)    : Kamis, 15 Januari 2026
HH:mm:ss | ...  : 08:30:00 | 15-01-2026
hh:mm a (US)    : 08:30 AM, Jan 15 2026

=== RFC-1123 (HTTP headers) ===
RFC_1123         : Thu, 15 Jan 2026 08:30:00 +0700

=== Locale-aware month/day ===
Full (id-ID)     : Kamis, 15 Januari 2026 pukul 08:30
Full (ja-JP)     : 木曜日, 15 1月 2026 at 08:30

=== Format Instant (UTC) ===
format UTC       : 2026-01-15 01:30:00 UTC

Mari kita telaah baris-baris kode di atas:

  1. Baris 17-20 (predefined ISO): ISO_DATE dan ISO_LOCAL_DATE menghasilkan format yang sama untuk LocalDate — 2026-01-15. Perbedaannya muncul saat memformat ZonedDateTime: ISO_DATE menambahkan offset (+07:00), ISO_LOCAL_DATE tidak. ISO_INSTANT selalu diakhiri Z untuk UTC.
  2. Baris 26-31 (FormatStyle.LONG): dengan Locale.of("id","ID"), hasilnya 15 Januari 2026. Dengan Locale.US, hasilnya January 15, 2026. Locale mengatur urutan komponen dan nama bulan; FormatStyle mengatur panjang (SHORT, MEDIUM, LONG, FULL).
  3. Baris 40-44 (custom pattern): ofPattern("dd MMM yyyy") memberi kontrol penuh. dd = hari 2 digit, MMM = singkatan bulan (Jan), MMMM = nama bulan penuh (Januari), EEEE = nama hari penuh (Kamis). HH = jam 24 jam, hh = jam 12 jam, a = AM/PM.
  4. Baris 53 (RFC_1123_DATE_TIME): format standar HTTP header Date:. Hasilnya Thu, 15 Jan 2026 08:30:00 +0700. Cocok untuk response HTTP dan email RFC 2822.
  5. Baris 61-66 (literal string dalam pattern): tanda kutip tunggal 'pukul' menandai literal yang akan dicetak apa adanya. Tanpa tanda kutip, pukul akan dianggap sebagai pola huruf — p misalnya tidak dikenali dan akan melempar exception.
  6. Baris 75-78 (format Instant dengan withZone): untuk memformat Instant langsung, formatter harus tahu zona target lewat withZone(...). Tanpa ini, format(Instant) melempar UnsupportedTemporalTypeException karena Instant tidak memiliki komponen tanggal lokal.

Pattern Symbol yang Sering Dipakai #

Tabel berikut merangkum simbol pattern yang paling sering muncul di aplikasi nyata:

SimbolArtiContoh
yyyyTahun 4 digit2026
MMBulan 2 digit01
MMMBulan singkatanJan
MMMMBulan penuhJanuari
ddHari 2 digit15
EEEENama hari penuhKamis
HHJam 24 jam08
hhJam 12 jam08
mmMenit30
ssDetik00
aAM/PM markerAM
zNama zonaWIB
ZOffset signed+0700

Referensi Lanjutan #

Selanjutnya #

Formatting adalah satu arah: objek tanggal → string. Operasi kebalikannya adalah parsing string → objek tanggal. Bab Parsing Date-Time akan membahas LocalDate.parse(...) dengan formatter kustom dan cara menangani DateTimeParseException dengan baik.