Kenapa hitungan float kadang nggak sesuai ekspektasi kamu?
Pernah nggak sih kamu lagi coding, terus ngejalanin operasi matematika pakai angka yang ada koma-nya alias float, eh hasilnya malah agak "aneh"? Misalnya, kamu expect 0.1 + 0.2 itu persis 0.3, tapi di program malah munculnya 0.30000000000000004? Atau mungkin 1.0 - 0.9 nggak pas 0.1, tapi jadi 0.09999999999999998? Kalau iya, tenang, kamu nggak sendirian. Ini salah satu "misteri" yang sering bikin developer pemula (bahkan yang udah lama juga kadang lupa) garuk-garuk kepala.
Jadi, kenapa sih float kadang nggak sesuai ekspektasi? Jawabannya itu ada hubungannya sama cara komputer "menyimpan" dan "menghitung" angka yang ada koma-nya.
Bayangin gini deh. Kita manusia sehari-hari pakai sistem bilangan basis 10, alias desimal. Angka 123.45 itu artinya (1 10^2) + (2 10^1) + (3 10^0) + (4 10^-1) + (5 10^-2). Gampang kan? Nah, komputer itu pakainya sistem bilangan basis 2, alias biner. Cuma ada angka 0 dan 1. Angka 10 (desimal) itu di biner jadi 1010 (1 2^3 + 0 2^2 + 1 2^1 + 0 * 2^0). Gitu deh seterusnya.
Masalah mulai muncul waktu kita mau representasiin angka desimal yang ada koma-nya ke dalam biner. Kayak angka 0.5 itu gampang, di biner jadi 0.1 (1 2^-1). Angka 0.25 itu 0.01 (0 2^-1 + 1 2^-2). Angka 0.75 itu 0.11 (1 2^-1 + 1 * 2^-2). Angka yang bisa dipecah jadi penjumlahan 1/2, 1/4, 1/8, 1/16, dan seterusnya (alias pangkat negatif dari 2) itu bisa direpresentasiin dengan pas di biner.
Tapi gimana kalau angka desimalnya itu kayak 0.1? Di desimal, 0.1 itu kan 1/10. Coba deh kamu ubah 1/10 ke biner. Kita mulai nyari pangkat negatif 2 yang paling gede tapi kurang dari 0.1. 2^-1 = 0.5 (kegedean) 2^-2 = 0.25 (kegedean) 2^-3 = 0.125 (kegedean) 2^-4 = 0.0625 (nah, ini bisa) Sisa: 0.1 - 0.0625 = 0.0375. Lanjut cari lagi: 2^-5 = 0.03125 (bisa) Sisa: 0.0375 - 0.03125 = 0.00625. Lanjut lagi: 2^-6 = 0.015625 (kegedean) 2^-7 = 0.0078125 (kegedean) 2^-8 = 0.00390625 (bisa) Sisa: 0.00625 - 0.00390625 = 0.00234375. Terus aja gitu...
Kamu bakal sadar kalau proses ini nggak akan pernah berhenti dengan sisa 0. Sama kayak kamu nyoba nyari representasi desimal dari 1/3 (itu kan 0.333333... nggak beres-beres), angka 0.1 (dan banyak angka desimal lainnya kayak 0.2, 0.3, dll.) itu nggak bisa direpresentasiin secara persis di biner dalam jumlah digit yang terbatas.
Komputer punya "jatah" bit (digit biner) yang terbatas buat nyimpen angka float. Ada standar internasional namanya IEEE 754 yang ngatur gimana nyimpen angka float ini. Yang paling umum dipakai itu:
- Single-precision float (biasanya pakai tipe
floatdi banyak bahasa pemrograman): Pakai 32 bit. - Double-precision float (biasanya pakai tipe
double): Pakai 64 bit.
Nah, karena keterbatasan bit ini, angka kayak 0.1, 0.2, 0.3 itu nggak bisa disimpen dengan akurat. Mereka disimpen sebagai angka biner yang paling mendekati. Jadi, 0.1 di komputer itu sebenarnya bukan persis 0.1, tapi angka yang sedikit lebih kecil atau sedikit lebih besar tapi sangat dekat. Sama kayak 1/3 yang kamu tulis 0.3333 itu kan bukan persis 1/3, cuma mendekati.
Waktu kamu ngelakuin operasi kayak 0.1 + 0.2, yang dijumlahin itu bukan persis 0.1 dan persis 0.2. Yang dijumlahin adalah representasi biner yang mendekati 0.1 dan representasi biner yang mendekati 0.2. Hasil penjumlahannya pun nanti diubah lagi ke representasi biner yang paling mendekati hasil seharusnya. Dan karena representasi awalnya udah nggak persis, wajar kalau hasilnya jadi nggak persis juga. Makanya muncul angka-angka "ekor panjang" kayak 0.30000000000000004 atau 0.09999999999999998. Ini bukan error dalam perhitungan, tapi konsekuensi dari cara komputer merepresentasikan angka float.
Oke, sekarang udah ngerti kan akar masalahnya? Terus gimana cara ngatasin atau setidaknya ngelola masalah ini biar nggak bikin pusing dan program kita tetep jalan sesuai harapan?
Ini dia beberapa tips dan trik yang bisa kamu pakai:
1. Gunakan double kalau memungkinkan
Kalau kamu cuma butuh angka yang "lebih akurat", pakai double aja. Tipe double pakai 64 bit, yang artinya dia punya lebih banyak "jatah" bit buat nyimpen bagian pecahan dari angka. Ini bikin representasinya lebih dekat ke angka desimal yang sebenarnya dibanding float yang cuma 32 bit. Jadi, ketidakakuratan atau "error" pembulatannya jauh lebih kecil.
Contoh: float a = 0.1f; float b = 0.2f; float c = a + b; // c bisa jadi 0.300000004
vs
double x = 0.1; double y = 0.2; double z = x + y; // z mungkin 0.30000000000000004, tapi errornya jauh lebih kecil daripada float
Untuk kebanyakan kasus sains, rekayasa, atau perhitungan yang butuh akurasi tinggi tapi bukan soal uang, double udah cukup banget. Perlu diingat, double juga tetap nggak bisa representasiin semua angka desimal secara persis lho ya. Masih pakai basis 2 soalnya. Tapi setidaknya "lebih mending" dibanding float.
2. Hindari Perhitungan yang Sensitif dengan Angka Float (Terutama Uang!)
Nah, ini yang paling penting. Jangan sekali-kali (kalau bisa!) pakai float atau double buat perhitungan yang butuh akurasi mutlak, apalagi soal uang. Bayangin kalau aplikasi bank kamu pakai float buat nyimpen saldo nasabah atau ngitung bunga. Bisa-bisa ada nasabah yang saldonya jadi Rp 99.999,9999999998 alih-alih Rp 100.000,00 gara-gara pembulatan float. Kan berabe!
Jadi, buat perhitungan finansial atau apa pun yang butuh presisi decimal yang eksak (bukan presisi biner yang mendekati), kamu punya opsi lain:
3. Gunakan Tipe Integer untuk Perhitungan Eksak (Khususnya Uang)
Cara paling klasik dan aman buat ngitung uang adalah jangan pakai koma sama sekali. Gimana caranya? Simpan nilai uang dalam satuan terkecilnya sebagai integer. Misalnya, kalau mata uangnya Rupiah, simpan dalam bentuk "sen". Rp 100.000,00 itu disimpen sebagai 10.000.000 sen. Rp 5.500,50 disimpen sebagai 550.050 sen.
Waktu mau nambahin atau ngurangin, ya tinggal operasikan aja integer-nya. saldo_sen = 10000000; // Rp 100.000 uangmasuksen = 50000; // Rp 500 saldosen = saldosen + uangmasuksen; // Hasilnya 10050000 sen
Hasilnya kan integer tuh, dijamin akurat, nggak ada cerita 0.000000000000001. Nanti waktu mau nampilin ke user, baru deh kamu ubah lagi dari sen ke Rupiah (atau mata uang lainnya) dengan membagi 100 (atau 1000 kalau mata uangnya punya 3 digit desimal) dan format sesuai standar mata uang.
Cara ini ampuh banget buat menghindari masalah presisi float dalam perhitungan uang.
4. Gunakan Tipe Data/Library Khusus untuk Decimal Presisi Arbitrer (BigDecimal, Decimal, dll.)
Beberapa bahasa pemrograman punya tipe data atau library khusus yang didesain buat nyimpen dan ngitung angka desimal dengan presisi yang kamu tentuin sendiri, atau bahkan presisi "arbitrer" (seakurat mungkin sampai digit terakhir). Contoh yang paling terkenal adalah BigDecimal di Java atau Decimal di Python.
Tipe-tipe ini nggak nyimpen angka pakai representasi biner standar IEEE 754. Mereka biasanya nyimpen angka desimal sebagai dua bagian integer: satu buat nilai angkanya (misalnya 12345) dan satu lagi buat nyimpen posisi koma-nya (misalnya 2 digit di belakang koma). Dengan cara ini, angka 123.45 bisa disimpen persis.
Perhitungan pakai BigDecimal atau Decimal itu berbeda dari operasi aritmatika standar float. Mereka melakukan perhitungan secara "decimal", kayak kita ngitung di kertas. Ini bikin perhitungannya jauh lebih lambat dibanding float/double standar, tapi hasilnya dijamin akurat secara desimal.
Contoh pakai BigDecimal di Java:
java
import java.math.BigDecimal;BigDecimal a = new BigDecimal("0.1"); // Penting: pakai String biar nggak kena masalah float pas inisialisasi
BigDecimal b = new BigDecimal("0.2");
BigDecimal c = a.add(b); // Hasilnya persis BigDecimal("0.3")Lihat kan, hasilnya persis kayak yang kita expect. Kalau pakai float/double, perhitungan bunga itu bisa jadi angka aneh di belakang koma.
Jadi, kalau kamu butuh akurasi tinggi dan kecepatan bukan jadi masalah utama, atau memang berurusan sama keuangan, wajib banget pakai BigDecimal atau Decimal ini.
5. Jangan Bandingkan Angka Float Langsung Pakai ==
Karena angka float itu seringnya cuma representasi yang mendekati dan ada "ekor" angka yang nggak kita harapkan, membandingkan dua angka float langsung pakai operator == itu sering kali salah.
Contoh: double a = 0.1 + 0.2; double b = 0.3; if (a == b) { ... } // Ini KEMUNGKINAN BESAR FALSE!
Kenapa? Karena a itu nilainya 0.30000000000000004 (atau semacamnya), sedangkan b itu ya 0.3 (atau representasi biner paling dekat dengan 0.3). Angka 0.30000000000000004 itu tidak sama persis dengan 0.3.
Terus gimana cara bandingin angka float? Caranya adalah dengan memeriksa apakah selisih absolut (nilai mutlak dari selisih) antara dua angka itu sangat kecil, di bawah batas toleransi yang kita tentukan sendiri. Batas toleransi ini biasa disebut epsilon. Epsilon ini angka float yang sangat kecil, misalnya 0.000001 atau 0.000000001.
Rumusnya kira-kira gini: abs(a - b) < epsilon
Di mana abs() itu fungsi nilai mutlak. Kalau selisihnya kurang dari epsilon, kita anggap dua angka itu "sama" dalam konteks komputasi float.
Contoh pseudo-code:
epsilon = 0.000001
double a = 0.1 + 0.2;
double b = 0.3;Besar epsilon-nya tergantung kebutuhan dan tingkat presisi yang kamu inginkan. Semakin kecil epsilon, semakin ketat perbandingannya.
Ini tips krusial banget kalau kamu mainan sama float dan perlu bikin logika berdasarkan perbandingan.
6. Pikirkan Kapan dan Bagaimana Melakukan Pembulatan (Rounding)
Pembulatan itu penting, tapi harus dilakuin di waktu yang tepat. Kalau kamu bulatkan angka float di tengah-tengah perhitungan, kamu bisa menambah "error" pembulatan. Sebaiknya, lakukan pembulatan hanya saat:
- Menampilkan hasil ke pengguna: Pengguna nggak perlu lihat angka 0.30000000000000004. Bulatkan ke dua desimal jadi 0.30 atau 0.3.
- Hasil akhir dari serangkaian perhitungan: Kalau kamu udah selesai ngitung semuanya, baru bulatkan hasilnya sesuai kebutuhan.
- Ada aturan bisnis yang mengharuskan pembulatan di tahap tertentu: Misalnya, aturan bank ngitung bunga harian terus dibulatkan ke sen terdekat setiap akhir hari.
Ada banyak cara pembulatan (round half up, round down, round ceiling, dll.), pastikan kamu pakai metode yang sesuai dengan kebutuhan atau aturan yang berlaku. Di banyak bahasa pemrograman, ada fungsi atau method khusus buat ini (misalnya Math.round(), BigDecimal.setScale() dengan berbagai mode pembulatan).
Ingat, pembulatan ini tujuannya bikin angka float "terlihat" rapi dan sesuai ekspektasi manusia, tapi dia nggak menghilangkan masalah representasi biner di baliknya. Makanya, jangan pakai pembulatan buat "memperbaiki" presisi di tengah perhitungan kritis kayak keuangan, lebih baik pakai BigDecimal atau integer seperti tips sebelumnya.
7. Pahami Batasan dan Jangkauan Angka Float
Selain masalah presisi, angka float juga punya batasan jangkauan (range) nilai yang bisa disimpan dan representasi angka yang terlalu besar atau terlalu kecil.
- Overflow: Kalau hasil perhitungan float terlalu besar melebihi nilai maksimum yang bisa disimpan, hasilnya bisa jadi "Infinity".
- Underflow: Kalau hasilnya terlalu dekat dengan nol tapi nggak persis nol, bisa jadi "Negative Infinity" atau bahkan "0" kalau angkanya saking kecilnya sampe nggak bisa direpresentasiin beda dari nol.
- NaN (Not-a-Number): Hasil dari operasi yang nggak valid, misalnya akar kuadrat dari angka negatif, atau membagi 0.0 dengan 0.0.
Ini jarang terjadi di perhitungan sehari-hari, tapi kalau kamu kerja sama angka-angka ekstrem, penting buat tahu batasan ini.
Kesimpulan
Jadi, kenapa hitungan float kadang nggak sesuai ekspektasi kamu? Intinya karena komputer nyimpen angka desimal pakai sistem biner, dan banyak angka desimal yang nggak bisa diterjemahin persis ke biner dalam jumlah bit yang terbatas. Akhirnya, yang disimpen itu cuma representasi yang mendekati, dan waktu dioperasikan, error pembulatan kecil ini bisa akumulasi atau bikin hasil akhir jadi sedikit meleset dari yang kita kira.
Ini bukan kelemahan fundamental komputer atau bahasa pemrograman, tapi memang sifat matematika dan cara kerja representasi angka. Kayak kamu nggak bisa nulis 1/3 dengan persis pakai angka desimal yang jumlah digitnya terbatas.
Dengan memahami cara kerja float dan menerapkan tips-tips di atas (pakai double untuk presisi lebih baik, pakai integer/BigDecimal buat presisi eksak kayak uang, hindari == buat perbandingan, dan lakukan pembulatan dengan bijak), kamu bisa menghindari banyak kejutan dan bug yang disebabkan oleh perilaku float ini.
Mungkin awalnya terasa ribet, tapi percaya deh, ngerti soal float dan cara ngelolanya itu skill dasar yang penting banget buat developer. Jadi, jangan pusing lagi kalau ketemu 0.30000000000000004, anggap aja itu "kode rahasia" dari komputer yang ngasih tahu kamu buat lebih hati-hati sama angka yang ada koma-nya!
Semoga penjelasan ini gampang dipahami ya. Selamat coding!