Kenapa ini penting
Hampir setiap aplikasi menghitung sesuatu: total belanja, diskon, pajak, rata-rata nilai, persentase progres, atau jarak. Di JavaScript, semua angka biasa bertipe number yang disimpan dalam format floating point 64-bit (standar IEEE 754). Format ini cepat dan fleksibel, tapi punya keanehan terkenal: 0.1 + 0.2 tidak menghasilkan tepat 0.3.
Kalau kamu membangun aplikasi kasir atau keuangan dan tidak paham keanehan ini, kamu bisa menampilkan total 30000.000000000004 atau membulatkan uang dengan cara yang salah. Modul ini membahas objek Math, cara membulatkan dengan benar, nilai khusus NaN dan Infinity, pengecekan angka, BigInt untuk bilangan sangat besar, dan cara menampilkan angka dalam format Rupiah yang rapi dengan Intl.NumberFormat.
Konsep inti
Masalah presisi floating point
Komputer menyimpan angka dalam basis 2 (biner). Pecahan seperti 0,1 tidak bisa direpresentasikan tepat dalam biner, sama seperti 1/3 tidak bisa ditulis tepat dalam desimal (0,3333...). Akibatnya 0.1 + 0.2 menjadi 0.30000000000000004.
Strategi praktis:
- Untuk uang, simpan dalam satuan terkecil berupa bilangan bulat. Rupiah biasanya sudah bulat; untuk mata uang bersen (dolar), simpan dalam sen (
1999bukan19.99). - Untuk menampilkan, bulatkan di akhir dengan
toFixedatauIntl.NumberFormat. - Untuk membandingkan pecahan, cek selisihnya lebih kecil dari
Number.EPSILON(angka sangat kecil) daripada memakai===.
Objek Math
Math adalah object bawaan berisi konstanta dan function matematika. Kamu tidak perlu membuatnya dengan new.
| Function | Fungsi | 4.6 |
-4.6 |
|---|---|---|---|
Math.round |
bulat terdekat | 5 | -5 |
Math.floor |
bulat ke bawah | 4 | -5 |
Math.ceil |
bulat ke atas | 5 | -4 |
Math.trunc |
buang desimal | 4 | -4 |
Lainnya: Math.max, Math.min, Math.abs (nilai mutlak), Math.pow atau operator ** (pangkat), Math.sqrt (akar), Math.random() (angka acak 0 sampai kurang dari 1), dan konstanta Math.PI. Perhatikan: Math.round(2.5) hasilnya 3, tapi Math.round(-2.5) hasilnya -2, karena nilai tepat setengah dibulatkan ke arah positif tak hingga.
toFixed dan pembulatan ke n desimal
angka.toFixed(n) mengembalikan string dengan n digit desimal. Karena hasilnya string, ubah dengan Number() kalau mau dihitung lagi. Hati-hati, (1.005).toFixed(2) menghasilkan "1.00" bukan "1.01", karena 1.005 sebenarnya tersimpan sebagai 1.00499999... Trik umum untuk membulatkan ke 2 desimal: Math.round((x + Number.EPSILON) * 100) / 100.
NaN dan Infinity
NaN(Not a Number) muncul dari operasi angka yang gagal, misalnyaNumber("abc")atau0 / 0. Ironisnya,typeof NaNadalah"number".Infinitymuncul dari pembagian dengan nol (1 / 0) atau angka yang terlalu besar.Number.isNaN(x)hanyatruejikaxbenar-benarNaN. Function global lamaisNaN(x)mengonversi dulu, sehinggaisNaN("abc")bernilaitrue, yang sering menyesatkan.Number.isFinite(x)mengecek angka valid yang bukanNaNatauInfinity.Number.isInteger(x)mengecek bilangan bulat.Number.isSafeInteger(x)mengecek bilangan bulat yang masih bisa disimpan tepat (sampaiNumber.MAX_SAFE_INTEGER, yaitu 9.007.199.254.740.991).
BigInt
Untuk bilangan bulat yang melewati batas aman, misalnya ID transaksi panjang atau perhitungan kriptografi, pakai BigInt dengan menambahkan n di belakang angka (123n) atau BigInt("123"). Aturan BigInt: tidak bisa dicampur langsung dengan number (1n + 1 melempar TypeError), pembagian membuang sisa (7n / 2n hasilnya 3n), dan tidak bisa dipakai dengan Math. JSON.stringify juga melempar error untuk BigInt, jadi ubah ke string dulu.
Format angka dengan Intl.NumberFormat
Intl.NumberFormat memformat angka sesuai aturan lokal. Untuk Indonesia ("id-ID"), pemisah ribuan adalah titik dan desimal adalah koma. Dengan style: "currency" dan currency: "IDR", hasilnya seperti "Rp 1.500.000,00". Biasanya Rupiah ditampilkan tanpa desimal, jadi tambahkan maximumFractionDigits: 0. Catatan teknis: spasi setelah "Rp" adalah karakter non-breaking space (), bukan spasi biasa. Ini penting kalau kamu membandingkan string hasil format di dalam test.
Buat formatter sekali lalu pakai berulang kali (lebih efisien), daripada membuat new Intl.NumberFormat di setiap pemanggilan. Untuk kebutuhan cepat ada juga angka.toLocaleString("id-ID").
Contoh kode
// 1) Presisi floating point
console.log(0.1 + 0.2); // → 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // → false
const hampirSama = (a, b) => Math.abs(a - b) < Number.EPSILON;
console.log(hampirSama(0.1 + 0.2, 0.3)); // → true
// Simpan uang bersen sebagai bilangan bulat (sen)
const hargaSen = 1999; // $19.99
const totalSen = hargaSen * 3;
console.log(totalSen / 100); // → 59.97
// 2) Pembulatan dengan Math
console.log(Math.round(4.6), Math.floor(4.6), Math.ceil(4.1), Math.trunc(4.9)); // → 5 4 5 4
console.log(Math.round(-4.6), Math.floor(-4.6), Math.ceil(-4.6), Math.trunc(-4.6)); // → -5 -5 -4 -4
console.log(Math.round(2.5), Math.round(-2.5)); // → 3 -2
console.log(Math.max(3, 9, 1), Math.min(...[7, 2, 5]), Math.abs(-12)); // → 9 2 12
console.log(2 ** 10, Math.sqrt(144)); // → 1024 12
// Angka acak bulat antara min dan max (inklusif)
const acak = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const dadu = acak(1, 6);
console.log(dadu >= 1 && dadu <= 6); // → true
// 3) toFixed mengembalikan string
const rata = 85.6666;
console.log(rata.toFixed(2), typeof rata.toFixed(2)); // → 85.67 string
console.log((1.005).toFixed(2)); // → 1.00 (bukan 1.01!)
const bulat2 = (x) => Math.round((x + Number.EPSILON) * 100) / 100;
console.log(bulat2(1.005)); // → 1.01
// 4) NaN dan Infinity
console.log(Number("abc"), 0 / 0, 1 / 0, -1 / 0); // → NaN NaN Infinity -Infinity
console.log(typeof NaN); // → number
console.log(isNaN("abc"), Number.isNaN("abc")); // → true false
console.log(Number.isFinite(10), Number.isFinite(Infinity), Number.isFinite("10")); // → true false false
console.log(Number.isInteger(5), Number.isInteger(5.0), Number.isInteger(5.5)); // → true true false
console.log(Number.MAX_SAFE_INTEGER); // → 9007199254740991
console.log(9007199254740992 === 9007199254740993); // → true (presisi hilang!)
// 5) BigInt
const besar = 9007199254740993n;
console.log(besar + 1n); // → 9007199254740994n
console.log(7n / 2n, 7n % 2n); // → 3n 1n
console.log(typeof besar); // → bigint
try {
console.log(besar + 1);
} catch (err) {
console.log(err.constructor.name); // → TypeError (tidak bisa campur BigInt dan number)
}
console.log(Number(10n) + 1, BigInt(5) * 2n); // → 11 10n
console.log(JSON.stringify({ id: besar.toString() })); // → {"id":"9007199254740993"}
// 6) Format Rupiah dengan Intl.NumberFormat
const rupiah = new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
maximumFractionDigits: 0,
});
console.log(rupiah.format(1500000)); // → Rp 1.500.000
console.log(rupiah.format(2750.6)); // → Rp 2.751
console.log(rupiah.format(1500000).includes(" ")); // → true (spasi non-breaking)
const angkaID = new Intl.NumberFormat("id-ID");
console.log(angkaID.format(1234567.891)); // → 1.234.567,891
console.log(new Intl.NumberFormat("id-ID", { notation: "compact" }).format(1500000)); // → 1,5 jt
console.log(new Intl.NumberFormat("id-ID", { style: "percent", maximumFractionDigits: 1 }).format(0.125)); // → 12,5%
console.log((25000).toLocaleString("id-ID")); // → 25.000
Kesalahan umum
- Membandingkan pecahan dengan
===.0.1 + 0.2 === 0.3bernilaifalse. Bandingkan selisihnya dengan toleransi kecil, atau hitung dalam bilangan bulat. - Lupa bahwa
toFixedmengembalikan string.(1.5).toFixed(1) + 1menghasilkan"1.51". Ubah ke angka dulu kalau masih dihitung. - Memakai
isNaNglobal.isNaN("teks")bernilaitruekarena dikonversi dulu. PakaiNumber.isNaN. - Mencampur BigInt dan number.
10n + 5melemparTypeError. Konversi salah satunya secara eksplisit. - Memformat Rupiah dengan menyambung string manual.
"Rp" + angkatidak memberi pemisah ribuan. PakaiIntl.NumberFormat("id-ID", ...)yang dibuat sekali lalu dipakai ulang.
Latihan
- Mudah, Hitung rata-rata dari
[78, 85, 92, 67]lalu tampilkan dengan dua desimal. - Sedang, Buat function
hitungTotal(harga, diskonPersen, pajakPersen)yang mengembalikan total akhir dalam Rupiah bulat (dibulatkan ke rupiah terdekat), lalu tampilkan dengan format Rupiah tanpa desimal. Uji dengan harga 250000, diskon 15, pajak 11. - Menantang, Buat function
faktorial(n)memakai BigInt yang bisa menghitung25!dengan tepat, lalu bandingkan dengan versinumberuntuk melihat hilangnya presisi.
Lihat solusi
// 1) Mudah
const nilai = [78, 85, 92, 67];
let jumlah = 0;
for (const n of nilai) jumlah += n;
console.log((jumlah / nilai.length).toFixed(2)); // → 80.50
// 2) Sedang
const rupiah = new Intl.NumberFormat("id-ID", { style: "currency", currency: "IDR", maximumFractionDigits: 0 });
function hitungTotal(harga, diskonPersen, pajakPersen) {
const setelahDiskon = harga * (1 - diskonPersen / 100);
const total = setelahDiskon * (1 + pajakPersen / 100);
return Math.round(total);
}
const total = hitungTotal(250000, 15, 11);
console.log(total); // → 235875
console.log(rupiah.format(total)); // → Rp 235.875
// 3) Menantang
function faktorial(n) {
let hasil = 1n;
for (let i = 2n; i <= BigInt(n); i++) hasil *= i;
return hasil;
}
function faktorialNumber(n) {
let hasil = 1;
for (let i = 2; i <= n; i++) hasil *= i;
return hasil;
}
console.log(faktorial(25)); // → 15511210043330985984000000n
console.log(faktorialNumber(25)); // → 1.5511210043330986e+25 (sudah tidak tepat)
Ringkasan
- Semua angka biasa adalah floating point 64-bit, sehingga pecahan bisa tidak presisi; hitung uang dalam bilangan bulat.
Math.round,floor,ceil, dantruncpunya perilaku berbeda terutama pada angka negatif.toFixedmengembalikan string dan bisa meleset pada kasus seperti1.005.- Pakai
Number.isNaN,Number.isFinite, danNumber.isIntegeruntuk validasi angka;BigIntuntuk bilangan bulat di atas batas aman. Intl.NumberFormat("id-ID", { style: "currency", currency: "IDR" })menghasilkan format Rupiah yang benar.