Nilai yang sama tampil berbeda di tiap negara: 1234567.89 dibaca orang Indonesia sebagai 1.234.567,89, orang Amerika sebagai 1,234,567.89, dan orang Prancis sebagai 1 234 567,89. Kesalahan format bukan sekadar soal kosmetik — pembaca bisa salah menafsirkan posisi desimal, dan uang yang dihitung dengan tipe data yang salah bisa bergeser sen demi sen tanpa terlihat. Chapter ini membahas format angka per locale, simbol sekaligus posisi mata uang, presisi desimal per mata uang, dan praktik terpenting dari semuanya: menyimpan uang sebagai integer lalu memformatnya hanya di lapisan tampilan. Ekstensi intl tidak tersedia di lingkungan buku ini, jadi kita membangun formatter PHP murni yang teruji dan menampilkan NumberFormatter sebagai pembanding.

Format Angka per Locale #

Tiga keputusan membentuk tampilan sebuah angka: pemisah desimal, pemisah ribuan, dan jumlah digit desimal. Locale berbeda menjawab ketiganya secara berbeda:

LocalePemisah desimalPemisah ribuan1234567.89 tampil sebagai
id_IDkoma (,)titik (.)1.234.567,89
en_UStitik (.)koma (,)1,234,567.89
fr_FRkoma (,)spasi1 234 567,89

Fungsi number_format() menerima keempat keputusan itu sebagai argumen, sehingga kita bisa menghasilkan gaya locale mana pun tanpa ekstensi tambahan:

php
<?php
$angka = 1234567.89;

echo number_format($angka, 2, ",", "."), "\n"; // gaya id_ID
echo number_format($angka, 2, ".", ","), "\n"; // gaya en_US
echo number_format($angka, 2, ",", " "), "\n"; // gaya fr_FR

Output yang Dihasilkan:

text
1.234.567,89
1,234,567.89
1 234 567,89

Argumen ketiga menentukan pemisah desimal dan argumen keempat menentukan pemisah ribuan. Perhatikan bahwa format Indonesia dan Amerika adalah cermin satu sama lain — keduanya menukar titik dan koma sekaligus, bukan hanya salah satunya. Menukar satu pemisah saja menghasilkan angka yang salah dibaca, misalnya 1,234.567,89 yang tidak valid di locale mana pun.

Simbol, Posisi, dan Presisi Mata Uang #

Mata uang menambah tiga keputusan di atas format angka: simbol apa yang dipakai, di posisi mana simbol ditempatkan, dan berapa digit desimal yang lazim. Rupiah ditulis Rp di depan tanpa spasi dan tanpa desimal — sen sudah lama tidak beredar. Dolar Amerika menempatkan $ di depan dengan dua desimal. Banyak locale Eropa justru menaruh simbol € di belakang angka. Yen Jepang memakai simbol ¥ di depan tanpa desimal.

Mata uangSimbolPosisi simbolJumlah desimalContoh tampilan
IDRRpdepan, tanpa spasi0Rp150.000
USD$depan2$1,234.57
EUR€belakang (banyak locale Eropa)29 876,50 €
JPY¥depan0¥7,500

Tabel aturan tersebut bisa kita terjemahkan langsung menjadi fungsi formatter yang memakai match untuk memilih aturan per mata uang:

php
<?php
function formatUang(int|float $nilai, string $mataUang): string
{
    $aturan = match ($mataUang) {
        "IDR" => ["simbol" => "Rp", "posisi" => "depan",    "desimal" => 0, "des" => ",", "ribu" => "."],
        "USD" => ["simbol" => "$",  "posisi" => "depan",    "desimal" => 2, "des" => ".", "ribu" => ","],
        "EUR" => ["simbol" => "€", "posisi" => "belakang", "desimal" => 2, "des" => ",", "ribu" => " "],
        "JPY" => ["simbol" => "¥", "posisi" => "depan",    "desimal" => 0, "des" => ".", "ribu" => ","],
    };

    $angkaTerformat = number_format($nilai, $aturan["desimal"], $aturan["des"], $aturan["ribu"]);

    return $aturan["posisi"] === "depan"
        ? $aturan["simbol"] . $angkaTerformat
        : $angkaTerformat . " " . $aturan["simbol"];
}

foreach ([["IDR", 150000], ["USD", 1234.567], ["EUR", 9876.5], ["JPY", 7500]] as [$mataUang, $nilai]) {
    echo formatUang($nilai, $mataUang), "\n";
}

Output yang Dihasilkan:

text
Rp150.000
$1,234.57
9 876,50 €
¥7,500

Mari kita bedah baris-baris pentingnya. Ekspresi match ($mataUang) mengembalikan satu set aturan lengkap per mata uang — simbol, posisi, jumlah desimal, dan kedua pemisah — sehingga menambah mata uang baru berarti menambah satu baris tabel, bukan menambah percabangan if yang menyebar. Pemanggilan number_format($nilai, $aturan["desimal"], ...) sekaligus membulatkan nilai: 1234.567 dolar menjadi $1,234.57, dan 9876.5 euro menjadi 9 876,50 € dengan pemisah gaya Eropa. Rupiah dan yen dicetak tanpa desimal, sehingga 7500 yen tampil sebagai ¥7,500.

Menyimpan Uang sebagai Integer #

Formatter di atas menerima int|float, dan di sinilah jebakan menunggu. Bilangan pecahan pada komputer disimpan dalam basis dua, yang tidak mampu merepresentasikan 0.1 secara persis — sama seperti 1/3 tidak bisa ditulis persis dalam desimal:

php
<?php
var_dump(0.1 + 0.2 === 0.3);

Output yang Dihasilkan:

text
bool(false)

Perbandingan menghasilkan false karena 0.1 + 0.2 sesungguhnya sedikit lebih besar dari 0.3. Selisihnya mikroskopis pada satu operasi, tetapi aplikasi nyata menjumlahkan ribuan transaksi; kesalahan pembulatan menumpuk dan saldo akhir bergeser dari yang seharusnya. Karena itu aturannya tegas: simpan uang dalam satuan terkecil sebagai integer, bukan sebagai float.

php
<?php
// Semua nilai dalam cent (1/100 dollar) — integer, tanpa pembulatan
$hargaSatuanCent = 19999;   // $199.99
$jumlah = 3;
$ongkosKirimCent = 500;     // $5.00

$totalCent = $hargaSatuanCent * $jumlah + $ongkosKirimCent;

$dollar = intdiv($totalCent, 100);
$cent = $totalCent % 100;

printf("Total: $%d.%02d\n", $dollar, $cent);

Output yang Dihasilkan:

text
Total: $604.97

Perhitungan 19999 × 3 + 500 menghasilkan 60497 cent secara eksak — tidak ada satu bit pun yang hilang. Fungsi intdiv() mengembalikan bagian dolar dan operator % mengembalikan sisa cent, lalu printf merakit keduanya tanpa pernah menyentuh float. Untuk rupiah, satuan terkecil praktisnya adalah rupiah itu sendiri, jadi seluruh nilai sudah berupa integer sejak awal. Di database, kolom INTEGER atau BIGINT menjadi rumah yang tepat untuk nilai-nilai ini — perancangan kolomnya kita bahas pada Chapter Merancang Database Schema .

IntlNumberFormatter sebagai Pembanding #

Ekstensi intl menyediakan NumberFormatter yang membaca data CLDR: simbol, posisi, pemisah, dan presisi untuk ribuan kombinasi locale dan mata uang, tanpa satu tabel manual pun:

php
<?php
// Membutuhkan ekstensi intl — tidak tersedia di lingkungan buku ini
$formatter = new NumberFormatter("id_ID", NumberFormatter::CURRENCY);

echo $formatter->formatCurrency(1234567.89, "IDR");

Untuk locale id_ID dan mata uang IDR, formatter ini menghasilkan tampilan setara Rp1.234.567,89 menurut data locale resmi — persis hasil tabel manual kita, tetapi tanpa pemeliharaan aturan sendiri. Ekstensi intl memang tidak tersedia di lingkungan buku ini, sehingga formatter PHP murni di atas menjadi alat kerja kita. Perlu dicatat bahwa intl hanya menggantikan lapisan tampilan; keputusan arsitektur menyimpan integer tidak berubah di mana pun formatter berasal.

Ringkasan #

Format angka dan mata uang adalah tampilan, bukan penyimpanan: number_format() mewakili keputusan locale mana pun lewat empat argumennya, tabel aturan per mata uang menangani simbol, posisi, dan presisi, dan NumberFormatter dari intl menawarkan jalur resmi berbasis CLDR saat ekstensi itu tersedia. Di lapisan data, uang selalu berupa integer satuan terkecil karena float kehilangan presisi sedetik setelah nilai pecahan masuk. Angka dan uang kini rapi; satu dimensi data yang masih polos adalah waktu — Chapter Date/Time Localization berikutnya menyeting nama bulan, urutan tanggal, dan zona waktu untuk pembaca Anda.

Referensi #