Angka yang sama dapat ditampilkan dengan sangat berbeda tergantung negara. 1,234,567.89 di Amerika Serikat sama maknanya dengan 1.234.567,89 di Indonesia atau Jerman, dan dengan 1 234 567,89 di Prancis. Perbedaannya bukan sekadar estetika — pemisah grup dan simbol desimal yang salah dapat menyebabkan parsing gagal atau salah interpretasi. Kotlin/JVM mewarisi kelas java.util.Locale dan java.text.NumberFormat dari Java untuk menangani perbedaan ini. Chapter ini membongkar bagaimana Locale bekerja dan bagaimana NumberFormat menggunakannya untuk memformat angka, mata uang, dan persentase.

Apa itu Locale #

Locale adalah identifier yang merepresentasikan kombinasi bahasa dan negara (atau wilayah). Konstruksi paling umum:

kotlin Contoh: konstruksi Locale
import java.util.Locale

val us = Locale.US                  // Locale konstan
val de = Locale.GERMANY             // Locale konstan
val id = Locale("id", "ID")         // Bahasa Indonesia, negara Indonesia
val fr = Locale.FRANCE              // Locale konstan
val jp = Locale.JAPAN               // Locale konstan

Locale memengaruhi banyak aspek: pemisah grup, simbol desimal, nama hari/bulan, simbol mata uang, bahkan urutan penulisan tanggal. Method String.format() menerima Locale sebagai argumen pertama untuk memengaruhi outputnya.

Format Angka dengan Locale #

kotlin
import java.util.Locale
import java.text.NumberFormat

fun main() {
    val angka = 1234567.89

    println("Locale default: ${"%,.2f".format(angka)}")
    println("Locale US:     ${"%,.2f".format(Locale.US, angka)}")
    println("Locale German: ${"%,.2f".format(Locale.GERMANY, angka)}")
    println("Locale ID:     ${"%,.2f".format(Locale("id", "ID"), angka)}")
    println("Locale France: ${"%,.2f".format(Locale.FRANCE, angka)}")
    println("Locale Japan:  ${"%,.2f".format(Locale.JAPAN, angka)}")

    println("--- NumberFormat.getInstance ---")
    val nfUS = NumberFormat.getInstance(Locale.US)
    val nfDE = NumberFormat.getInstance(Locale.GERMANY)
    val nfID = NumberFormat.getInstance(Locale("id", "ID"))
    println("US:  ${nfUS.format(angka)}")
    println("DE:  ${nfDE.format(angka)}")
    println("ID:  ${nfID.format(angka)}")

    println("--- Currency ---")
    val cfUS = NumberFormat.getCurrencyInstance(Locale.US)
    val cfID = NumberFormat.getCurrencyInstance(Locale("id", "ID"))
    val cfJP = NumberFormat.getCurrencyInstance(Locale.JAPAN)
    println("US:  ${cfUS.format(angka)}")
    println("ID:  ${cfID.format(angka)}")
    println("JP:  ${cfJP.format(angka)}")

    println("--- Integer ---")
    val intUS = NumberFormat.getIntegerInstance(Locale.US)
    val intDE = NumberFormat.getIntegerInstance(Locale.GERMANY)
    println("US: ${intUS.format(angka)}")
    println("DE: ${intDE.format(angka)}")

    println("--- Persen ---")
    val persen = 0.875
    val pfUS = NumberFormat.getPercentInstance(Locale.US)
    val pfDE = NumberFormat.getPercentInstance(Locale.GERMANY)
    println("US: ${pfUS.format(persen)}")
    println("DE: ${pfDE.format(persen)}")
}

Output yang Dihasilkan:

text
Locale default: 1,234,567.89
Locale US:     1,234,567.89
Locale German: 1.234.567,89
Locale ID:     1.234.567,89
Locale France: 1 234 567,89
Locale Japan:  1,234,567.89
--- NumberFormat.getInstance ---
US:  1,234,567.89
DE:  1.234.567,89
ID:  1.234.567,89
--- Currency ---
US:  $1,234,567.89
ID:  Rp1.234.567,89
JP:  ¥1,234,568
--- Integer ---
US: 1,234,568
DE: 1.234.568
--- Persen ---
US: 88%
DE: 88 %

Mari kita telaah baris-baris kode di atas:

  1. Baris 8 ("%,.2f".format(angka) tanpa Locale): pemanggilan extension .format() tanpa argumen Locale menggunakan Locale default JVM. Di sebagian besar environment server, default adalah Locale.US, sehingga output 1,234,567.89. Namun default ini bisa berubah — jangan andalkan untuk output yang harus konsisten lintas mesin.
  2. Baris 9 ("%,.2f".format(Locale.US, angka)): pemanggilan dengan Locale eksplisit. Output dijamin 1,234,567.89 di mesin mana pun. Specifier , memicu pemisah grup sesuai Locale.
  3. Baris 10 (Locale.GERMANY): Jerman menggunakan titik sebagai pemisah grup dan koma sebagai simbol desimal. Output: 1.234.567,89.
  4. Baris 11 (Locale("id", "ID")): Indonesia juga menggunakan titik untuk pemisah grup dan koma untuk desimal, sama seperti Jerman. Konstruktor Locale(language, country) menerima kode bahasa ISO 639 (id) dan kode negara ISO 3166 (ID).
  5. Baris 12 (Locale.FRANCE): Prancis menggunakan spasi non-breaking (U+00A0) sebagai pemisah grup dan koma untuk desimal. Output: 1 234 567,89 — di terminal, spasi ini terlihat seperti spasi biasa.
  6. Baris 13 (Locale.JAPAN): Jepang menggunakan format yang sama dengan US (koma untuk grup, titik untuk desimal) untuk angka biasa, tetapi menggunakan simbol mata uang ¥ yang berbeda.
  7. Baris 16–20 (NumberFormat.getInstance(locale)): NumberFormat adalah kelas abstrak yang memformat angka sesuai aturan Locale. Method getInstance mengembalikan formatter default untuk angka umum. Kita panggil method .format(angka) untuk mendapatkan string.
  8. Baris 23–28 (getCurrencyInstance): khusus untuk mata uang. Method ini menambahkan simbol mata uang di depan dan menggunakan aturan penulisan mata uang negara tersebut. Untuk Jepang, ¥1,234,568 — perhatikan tidak ada desimal karena Yen secara tradisional tidak punya pecahan sen.
  9. Baris 31–34 (getIntegerInstance): memformat sebagai bilangan bulat — desimal dibulatkan ke bilangan bulat terdekat. Pemisah grup tetap berlaku sesuai Locale.
  10. Baris 38–40 (getPercentInstance): memformat nilai desimal sebagai persentase. Nilai 0.875 dikalikan 100 secara otomatis menjadi 88%. Perhatikan di Jerman ada spasi sebelum tanda persen (88 %) — konvensi yang diikuti oleh beberapa Locale Eropa.

Perbandingan Format Lintas Locale #

Tabel berikut merangkum bagaimana angka 1234567.89 ditampilkan di berbagai Locale:

LocalePemisah grupDesimalContoh
Locale.USKoma ,Titik .1,234,567.89
Locale.GERMANYTitik .Koma ,1.234.567,89
Locale("id", "ID")Titik .Koma ,1.234.567,89
Locale.FRANCESpasi NBSPKoma ,1 234 567,89
Locale.JAPANKoma ,Titik .1,234,567.89
Locale.ITALYTitik .Koma ,1.234.567,89

Format Mata Uang #

NumberFormat.getCurrencyInstance(locale) adalah cara standar memformat nilai uang. Method ini menambahkan simbol mata uang dan menggunakan jumlah desimal yang sesuai untuk mata uang tersebut (Yen = 0, Dolar = 2, Dinar Bahrain = 3):

kotlin Contoh: format mata uang
val cfID = NumberFormat.getCurrencyInstance(Locale("id", "ID"))
println(cfID.format(1500000.0))  // Rp1.500.000,00

Untuk format mata uang yang lebih kompleks (misalnya menampilkan kode ISO IDR alih-alih simbol Rp), gunakan DecimalFormat dengan pattern kustom — topik yang lebih lanjut dan berada di luar cakupan chapter ini.

Locale Default JVM #

Locale default JVM ditentukan oleh environment variabel sistem operasi (LANG, LC_ALL di Linux; pengaturan Region di Windows). Kita dapat memeriksa dan mengubah Locale default secara terprogram:

kotlin Contoh: inspeksi Locale default
println("Default: ${Locale.getDefault()}")
Locale.setDefault(Locale.US)
println("Setelah set: ${Locale.getDefault()}")

Referensi Lanjutan #

  • Java SE 21 Locale — dokumentasi java.util.Locale Java SE 21 untuk pemisah grup, simbol desimal, dan mata uang

Dengan pemahaman tentang Locale dan NumberFormat, kita siap beranjak ke chapter terakhir section ini: console application fundamentals — bagaimana stdin/stdout/stderr bekerja, exit code, ANSI color, dan perilaku program ketika di-pipe.