Mengapa tidak menggunakan Double atau Float untuk mewakili mata uang?

939

Saya selalu diberitahu untuk tidak mewakili uang dengan doubleatau floatjenis, dan kali ini saya mengajukan pertanyaan kepada Anda: mengapa?

Saya yakin ada alasan yang sangat bagus, saya hanya tidak tahu apa itu.

Fran Fitzpatrick
sumber
4
Lihat pertanyaan SO ini: Kesalahan Pembulatan?
Jeff Ogata
80
Untuk lebih jelasnya, mereka tidak boleh digunakan untuk apa pun yang membutuhkan akurasi - bukan hanya mata uang.
Jeff
152
Mereka tidak boleh digunakan untuk apa pun yang membutuhkan ketelitian . Namun, 53 bit signifikan ganda (~ 16 angka desimal) biasanya cukup baik untuk hal-hal yang hanya membutuhkan akurasi .
dan04
21
@jeff Komentar Anda sepenuhnya keliru menggambarkan apa nilai biner floating-point baik dan untuk apa itu tidak baik. Baca jawabannya oleh zneak di bawah ini, dan harap hapus komentar Anda yang menyesatkan.
Pascal Cuoq

Jawaban:

985

Karena pelampung dan ganda tidak dapat secara akurat mewakili basis 10 kelipatan yang kita gunakan untuk uang. Masalah ini bukan hanya untuk Java, tetapi untuk bahasa pemrograman apa pun yang menggunakan tipe basis-mengambang 2.

Dalam basis 10, Anda dapat menulis 10.25 sebagai 1025 * 10 -2 (bilangan bulat kali kekuatan 10). Angka - angka floating-point IEEE-754 berbeda, tetapi cara yang sangat sederhana untuk memikirkannya adalah dengan mengalikannya dengan kekuatan dua. Misalnya, Anda bisa melihat 164 * 2 -4 (bilangan bulat kali kekuatan dua), yang juga sama dengan 10,25. Itu bukan bagaimana angka-angka direpresentasikan dalam memori, tetapi implikasi matematikanya sama.

Bahkan dalam basis 10, notasi ini tidak dapat secara akurat mewakili fraksi paling sederhana. Misalnya, Anda tidak bisa mewakili 1/3: representasi desimal berulang (0,3333 ...), jadi tidak ada bilangan bulat terbatas yang bisa Anda kalikan dengan kekuatan 10 untuk mendapatkan 1/3. Anda dapat memilih urutan 3 dan eksponen kecil, seperti 333333333 * 10 -10 , tetapi itu tidak akurat: jika Anda mengalikannya dengan 3, Anda tidak akan mendapatkan 1.

Namun, untuk tujuan penghitungan uang, setidaknya untuk negara yang uangnya dinilai dalam urutan besarnya dolar AS, biasanya yang Anda butuhkan hanyalah menyimpan kelipatan 10 -2 , sehingga tidak masalah 1/3 itu tidak bisa diwakili.

Masalah dengan float dan double adalah bahwa sebagian besar angka seperti uang tidak memiliki representasi yang tepat sebagai bilangan bulat kali kekuatan 2. Bahkan, satu-satunya kelipatan 0,01 antara 0 dan 1 (yang signifikan ketika berhadapan dengan uang karena mereka bilangan bulat bilangan bulat) yang dapat direpresentasikan secara tepat sebagai angka floating-point biner IEEE-754 adalah 0, 0,25, 0,5, 0,75 dan 1. Yang lainnya dimatikan dengan jumlah kecil. Sebagai analogi dengan contoh 0,333333, jika Anda mengambil nilai floating-point untuk 0,1 dan Anda mengalikannya dengan 10, Anda tidak akan mendapatkan 1.

Merepresentasikan uang sebagai doubleatau floatmungkin akan terlihat bagus pada awalnya ketika perangkat lunak menyelesaikan kesalahan kecil, tetapi ketika Anda melakukan lebih banyak penambahan, pengurangan, perkalian dan pembagian pada angka yang tidak tepat, kesalahan akan bertambah dan Anda akan berakhir dengan nilai yang terlihat. tidak akurat. Hal ini membuat pelampung dan ganda tidak memadai untuk berurusan dengan uang, di mana dibutuhkan akurasi sempurna untuk kelipatan basis 10 kekuatan.

Solusi yang berfungsi dalam hampir semua bahasa adalah menggunakan bilangan bulat, dan menghitung sen. Misalnya, 1025 akan menjadi $ 10,25. Beberapa bahasa juga memiliki tipe bawaan untuk menangani uang. Antara lain, Java memiliki BigDecimalkelas, dan C # memiliki decimaltipe.

zneak
sumber
3
@ Fran Anda akan mendapatkan kesalahan pembulatan dan dalam beberapa kasus di mana sejumlah besar mata uang digunakan, perhitungan suku bunga bisa sangat tidak aktif
linuxuser27
5
... sebagian besar basis 10 fraksi, yaitu. Sebagai contoh, 0,1 tidak memiliki representasi titik-mengambang biner yang tepat. Jadi, 1.0 / 10 * 10mungkin tidak sama dengan 1.0.
Chris Jester-Young
6
@ linuxuser27 Saya pikir Fran berusaha menjadi lucu. Bagaimanapun, jawaban zneak adalah yang terbaik yang pernah saya lihat, bahkan lebih baik daripada versi klasik dari Bloch.
Isaac Rabinovitch
5
Tentu saja jika Anda tahu ketepatannya, Anda selalu dapat membulatkan hasil dan dengan demikian menghindari seluruh masalah. Ini jauh lebih cepat dan sederhana daripada menggunakan BigDecimal. Alternatif lain adalah menggunakan presisi tetap atau panjang.
Peter Lawrey
2
@ zneak Anda tahu bahwa bilangan rasional adalah himpunan bagian dari bilangan real, bukan? Bilangan real IEEE-754 adalah bilangan real. Mereka kebetulan juga rasional.
Tim Seguine
314

Dari Bloch, J., Java Efektif, edisi ke-2, Item 48:

Tipe floatdan doubletipe ini sangat tidak cocok untuk perhitungan moneter karena tidak mungkin untuk mewakili 0,1 (atau kekuatan negatif sepuluh lainnya) sebagai a floatatau doubletepat.

Sebagai contoh, misalkan Anda memiliki $ 1,03 dan Anda menghabiskan 42c. Berapa banyak uang yang tersisa?

System.out.println(1.03 - .42);

mencetak 0.6100000000000001.

Cara yang tepat untuk menyelesaikan masalah ini adalah menggunakan BigDecimal, intatau long untuk perhitungan moneter.

Meskipun BigDecimalmemiliki beberapa peringatan (silakan lihat jawaban yang diterima saat ini).

dogbane
sumber
6
Saya sedikit bingung dengan rekomendasi untuk menggunakan int atau panjang untuk perhitungan moneter. Bagaimana Anda mewakili 1,03 sebagai int atau panjang? Saya sudah mencoba "long a = 1.04;" dan "long a = 104/100;" tidak berhasil.
Peter
49
@ Peter, Anda menggunakan long a = 104dan menghitung dalam sen, bukan dalam dolar.
zneak
@zneak Bagaimana bila persentase perlu diterapkan seperti bunga majemuk atau serupa?
trusktr
3
@trusktr, saya akan menggunakan tipe desimal platform Anda. Di Jawa, itu BigDecimal.
zneak
13
@maaartinus ... dan Anda tidak berpikir menggunakan double untuk hal-hal seperti itu rawan kesalahan? Saya telah melihat masalah pembulatan float memukul sistem nyata dengan keras . Bahkan di perbankan. Tolong jangan rekomendasikan itu, atau jika Anda lakukan, berikan itu sebagai jawaban yang terpisah (sehingga kami dapat membatalkannya: P)
eis
75

Ini bukan masalah keakuratan, juga bukan masalah ketepatan. Ini adalah masalah memenuhi harapan manusia yang menggunakan basis 10 untuk perhitungan, bukan basis 2. Sebagai contoh, menggunakan ganda untuk perhitungan keuangan tidak menghasilkan jawaban yang "salah" dalam arti matematika, tetapi dapat menghasilkan jawaban yang bukan apa yang diharapkan dalam arti finansial.

Bahkan jika Anda membulatkan hasil pada menit terakhir sebelum output, Anda kadang-kadang masih bisa mendapatkan hasil menggunakan ganda yang tidak sesuai dengan harapan.

Menggunakan kalkulator, atau menghitung hasil dengan tangan, 1,40 * 165 = 231 tepat. Namun, secara internal menggunakan ganda, pada lingkungan sistem kompiler / sistem operasi saya, ini disimpan sebagai angka biner mendekati 230,99999 ... jadi jika Anda memotong angka, Anda mendapatkan 230 bukannya 231. Anda dapat beralasan bahwa pembulatan alih-alih pemotongan akan telah memberikan hasil yang diinginkan dari 231. Itu benar, tetapi pembulatan selalu melibatkan pemotongan. Apapun teknik pembulatan yang Anda gunakan, masih ada kondisi batas seperti ini yang akan membulatkan ketika Anda berharap untuk mengumpulkan. Mereka cukup langka sehingga mereka sering tidak akan ditemukan melalui pengujian atau pengamatan biasa. Anda mungkin harus menulis beberapa kode untuk mencari contoh yang menggambarkan hasil yang tidak berperilaku seperti yang diharapkan.

Asumsikan Anda ingin membulatkan sesuatu ke sen terdekat. Jadi, Anda mengambil hasil akhir Anda, kalikan dengan 100, tambahkan 0,5, potong, kemudian bagi hasil dengan 100 untuk kembali ke uang. Jika nomor internal yang Anda simpan adalah 3.46499999 .... bukannya 3.465, Anda akan mendapatkan 3.46 sebagai gantinya 3.47 ketika Anda membulatkan angka ke sen terdekat. Tetapi perhitungan dasar 10 Anda mungkin telah mengindikasikan bahwa jawabannya seharusnya 3.465 persis, yang jelas harus dibulatkan hingga 3,47, tidak turun ke 3,46. Hal-hal semacam ini terjadi sesekali dalam kehidupan nyata ketika Anda menggunakan ganda untuk perhitungan keuangan. Ini jarang terjadi, jadi sering kali tanpa disadari sebagai masalah, tetapi itu terjadi.

Jika Anda menggunakan basis 10 untuk perhitungan internal Anda alih-alih ganda, jawabannya selalu persis seperti yang diharapkan oleh manusia, dengan asumsi tidak ada bug lain dalam kode Anda.

Randy D Oxentenko
sumber
2
Terkait, menarik: Di konsol js chrome saya: Math.round (.4999999999999999): 0 Math.round (.49999999999999999): 1
Curtis Yallop
16
Jawaban ini menyesatkan. 1.40 * 165 = 231. Angka apa pun selain tepat 231 adalah salah dalam pengertian matematis (dan semua pengertian lainnya).
Karu
2
@ Karu Saya pikir itu sebabnya Randy mengatakan floats buruk ... Konsol Chrome JS saya menunjukkan 230,99999999999997 sebagai hasilnya. Itu adalah salah, yang merupakan titik yang dibuat dalam jawaban.
trusktr
6
@ Karu: Imho jawabannya tidak salah secara matematis. Hanya saja ada 2 pertanyaan yang dijawab yang bukan pertanyaan yang diajukan. Pertanyaan yang dikompilasi oleh kompiler Anda adalah 1.39999999 * 164.99999999 dan seterusnya yang benar secara matematis sama dengan 230.99999 .... Jelas bukan pertanyaan yang ditanyakan sejak awal ....
markus
2
@CurtisYallop karena nilai dobel ditutup menjadi 0,49999999999999999 adalah 0,5 Mengapa Math.round(0.49999999999999994)kembali 1?
phuclv
53

Saya bermasalah dengan beberapa respons ini. Saya pikir ganda dan pelampung memiliki tempat dalam perhitungan keuangan. Tentu saja, ketika menambah dan mengurangi jumlah moneter non-fraksional, tidak akan ada kehilangan presisi saat menggunakan kelas integer atau kelas BigDecimal. Tetapi ketika melakukan operasi yang lebih kompleks, Anda sering berakhir dengan hasil yang keluar beberapa atau banyak tempat desimal, tidak peduli bagaimana Anda menyimpan angka. Masalahnya adalah bagaimana Anda menyajikan hasilnya.

Jika hasil Anda berada di batas antara dibulatkan ke atas dan dibulatkan ke bawah, dan sen terakhir itu benar-benar penting, Anda mungkin harus memberi tahu pemirsa bahwa jawabannya hampir di tengah - dengan menampilkan lebih banyak tempat desimal.

Masalah dengan ganda, dan lebih lagi dengan mengapung, adalah ketika mereka digunakan untuk menggabungkan angka besar dan kecil. Di jawa

System.out.println(1000000.0f + 1.2f - 1000000.0f);

hasil dalam

1.1875
Rob Scala
sumber
16
INI!!!! Saya sedang mencari semua jawaban untuk menemukan FAKTA RELEVAN ini !!! Dalam perhitungan normal, tidak ada yang peduli jika Anda berada dalam sepersekian sen, tetapi di sini dengan angka tinggi dengan mudah beberapa dolar hilang per transaksi!
Falco
22
Dan sekarang bayangkan seseorang mendapatkan pendapatan harian sebesar 0,01% dari 1 Juta dolar - dia tidak akan mendapatkan apa-apa setiap hari - dan setelah satu tahun dia tidak mendapatkan 1000 Dolar, INI AKAN MASALAH
Falco
6
Masalahnya bukan keakuratannya tetapi float itu tidak memberi tahu Anda bahwa itu menjadi tidak akurat. Bilangan bulat hanya dapat menampung hingga 10 digit, float dapat menyimpan hingga 6 tanpa menjadi tidak akurat (saat Anda memotongnya sesuai). Itu memungkinkan ini sementara bilangan bulat mendapat luapan dan bahasa seperti java akan memperingatkan Anda atau tidak akan mengizinkannya. Saat Anda menggunakan ganda, Anda bisa naik hingga 16 digit yang cukup untuk banyak kasus penggunaan.
sigi
39

Mengapung dan ganda merupakan perkiraan. Jika Anda membuat BigDecimal dan mengirimkan float ke konstruktor, Anda akan melihat apa yang sebenarnya sama dengan float:

groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375

ini mungkin bukan bagaimana Anda ingin mewakili $ 1,01.

Masalahnya adalah bahwa spesifikasi IEEE tidak memiliki cara untuk secara tepat mewakili semua fraksi, beberapa di antaranya berakhir sebagai fraksi berulang sehingga Anda berakhir dengan kesalahan perkiraan. Karena akuntan menyukai hal-hal yang keluar tepat ke sen, dan pelanggan akan terganggu jika mereka membayar tagihan mereka dan setelah pembayaran diproses mereka berutang 0,01 dan mereka dikenakan biaya atau tidak dapat menutup akun mereka, lebih baik menggunakan jenis persis seperti desimal (dalam C #) atau java.math.BigDecimal di Jawa.

Bukan karena kesalahannya tidak dapat dikontrol jika Anda melakukannya: lihat artikel ini oleh Peter Lawrey . Lebih mudah untuk tidak membulatkannya sejak awal. Sebagian besar aplikasi yang menangani uang tidak memerlukan banyak matematika, operasinya terdiri dari menambahkan hal-hal atau mengalokasikan jumlah ke ember yang berbeda. Memperkenalkan floating point dan pembulatan hanya mempersulit hal-hal.

Nathan Hughes
sumber
5
float, doubledan BigDecimalmerupakan nilai yang tepat . Konversi kode ke objek tidak eksak serta operasi lainnya. Jenisnya sendiri tidak eksak.
chux - Reinstate Monica
1
@ chux: membaca ulang ini, saya pikir Anda memiliki titik bahwa kata-kata saya dapat ditingkatkan. Saya akan mengedit ini dan menulis ulang.
Nathan Hughes
28

Saya akan berisiko diturunkan, tetapi saya pikir ketidakcocokan angka floating point untuk perhitungan mata uang terlalu tinggi. Selama Anda memastikan Anda melakukan cent-rounding dengan benar dan memiliki angka yang cukup signifikan untuk bekerja dalam rangka melawan ketidakcocokan representasi biner-desimal yang dijelaskan oleh zneak, tidak akan ada masalah.

Orang yang menghitung dengan mata uang di Excel selalu menggunakan pelampung presisi ganda (tidak ada jenis mata uang di Excel) dan saya belum melihat ada orang yang mengeluh tentang kesalahan pembulatan.

Tentu saja, Anda harus tetap masuk akal; mis. sebuah toko web sederhana mungkin tidak akan pernah mengalami masalah dengan pelampung presisi ganda, tetapi jika Anda melakukan mis. akuntansi atau hal lain yang membutuhkan penambahan jumlah angka yang besar (tidak dibatasi), Anda tidak akan mau menyentuh angka floating point dengan sepuluh kaki. tiang.

escitalopram
sumber
3
Ini sebenarnya jawaban yang lumayan. Dalam kebanyakan kasus, tidak apa-apa untuk menggunakannya.
Vahid Amiri
2
Perlu dicatat bahwa sebagian besar bank investasi menggunakan dua kali lipat seperti halnya kebanyakan program C ++. Beberapa menggunakan panjang tetapi dengan demikian memiliki masalah sendiri skala pelacakan.
Peter Lawrey
20

Memang benar bahwa tipe floating point hanya dapat mewakili data desimal aproksimasi, juga benar bahwa jika seseorang membulatkan angka ke presisi yang diperlukan sebelum mempresentasikannya, ia memperoleh hasil yang benar. Biasanya.

Biasanya karena tipe ganda memiliki ketelitian kurang dari 16 angka. Jika Anda membutuhkan presisi yang lebih baik, itu bukan tipe yang cocok. Juga perkiraan dapat menumpuk.

Harus dikatakan bahwa bahkan jika Anda menggunakan aritmatika titik tetap Anda masih harus membulatkan angka, jika bukan karena BigInteger dan BigDecimal memberikan kesalahan jika Anda mendapatkan angka desimal berkala. Jadi ada perkiraan juga di sini.

Misalnya COBOL, yang secara historis digunakan untuk perhitungan keuangan, memiliki ketepatan maksimum 18 angka. Jadi seringkali ada pembulatan tersirat.

Kesimpulannya, menurut saya, double tidak cocok untuk presisi 16 digit, yang bisa jadi tidak cukup, bukan karena perkiraan.

Pertimbangkan output berikut dari program selanjutnya. Ini menunjukkan bahwa setelah pembulatan ganda memberikan hasil yang sama dengan BigDecimal hingga presisi 16.

Precision 14
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611

Precision 15
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110

Precision 16
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101

Precision 17
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611011
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611013

Precision 18
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110111
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110125

Precision 19
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101111
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101252

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.MathContext;

public class Exercise {
    public static void main(String[] args) throws IllegalArgumentException,
            SecurityException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {
        String amount = "56789.012345";
        String quantity = "1111111111";
        int [] precisions = new int [] {14, 15, 16, 17, 18, 19};
        for (int i = 0; i < precisions.length; i++) {
            int precision = precisions[i];
            System.out.println(String.format("Precision %d", precision));
            System.out.println("------------------------------------------------------");
            execute("BigDecimalNoRound", amount, quantity, precision);
            execute("DoubleNoRound", amount, quantity, precision);
            execute("BigDecimal", amount, quantity, precision);
            execute("Double", amount, quantity, precision);
            System.out.println();
        }
    }

    private static void execute(String test, String amount, String quantity,
            int precision) throws IllegalArgumentException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {
        Method impl = Exercise.class.getMethod("divideUsing" + test, String.class,
                String.class, int.class);
        String price;
        try {
            price = (String) impl.invoke(null, amount, quantity, precision);
        } catch (InvocationTargetException e) {
            price = e.getTargetException().getMessage();
        }
        System.out.println(String.format("%-30s: %s / %s = %s", test, amount,
                quantity, price));
    }

    public static String divideUsingDoubleNoRound(String amount,
            String quantity, int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        String price = Double.toString(price0);
        return price;
    }

    public static String divideUsingDouble(String amount, String quantity,
            int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        MathContext precision0 = new MathContext(precision);
        String price = new BigDecimal(price0, precision0)
                .toString();
        return price;
    }

    public static String divideUsingBigDecimal(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);
        MathContext precision0 = new MathContext(precision);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0, precision0);

        // presentation
        String price = price0.toString();
        return price;
    }

    public static String divideUsingBigDecimalNoRound(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0);

        // presentation
        String price = price0.toString();
        return price;
    }
}
pengguna1593165
sumber
15

Hasil angka floating point tidak tepat, yang membuatnya tidak cocok untuk perhitungan keuangan yang membutuhkan hasil yang tepat dan bukan perkiraan. float dan double dirancang untuk perhitungan teknik dan ilmiah dan berkali-kali tidak menghasilkan hasil yang tepat juga hasil perhitungan floating point dapat bervariasi dari JVM ke JVM. Lihat contoh BigDecimal dan double primitive di bawah yang digunakan untuk mewakili nilai uang, cukup jelas bahwa perhitungan floating point mungkin tidak tepat dan orang harus menggunakan BigDecimal untuk perhitungan keuangan.

    // floating point calculation
    final double amount1 = 2.0;
    final double amount2 = 1.1;
    System.out.println("difference between 2.0 and 1.1 using double is: " + (amount1 - amount2));

    // Use BigDecimal for financial calculation
    final BigDecimal amount3 = new BigDecimal("2.0");
    final BigDecimal amount4 = new BigDecimal("1.1");
    System.out.println("difference between 2.0 and 1.1 using BigDecimal is: " + (amount3.subtract(amount4)));

Keluaran:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9
Dheeraj Arora
sumber
3
Mari kita coba sesuatu selain dari penambahan / pengurangan sepele dan mutplicaiton integer, Jika kode menghitung tingkat bulanan pinjaman 7%, kedua jenis akan gagal untuk memberikan nilai yang tepat dan perlu pembulatan ke 0,01 terdekat. Pembulatan ke unit moneter terendah adalah bagian dari perhitungan uang, Menggunakan tipe desimal menghindari kebutuhan itu dengan penambahan / pengurangan - tetapi tidak banyak lagi.
chux - Reinstate Monica
@ chux-ReinstateMonica: Jika bunga seharusnya bertambah setiap bulan, hitung bunga setiap bulan dengan menambahkan bersama saldo harian, kalikan dengan 7 (suku bunga), dan bagi, pembulatan ke pembulatan terdekat, dengan jumlah hari dalam tahun. Tidak ada pembulatan di mana pun kecuali sebulan sekali pada langkah terakhir.
supercat
@supercat Komentar saya menekankan menggunakan FP biner dari unit moneter terkecil atau FP desimal keduanya menimbulkan masalah pembulatan serupa - seperti dalam komentar Anda dengan "dan bagi, pembulatan ke pembulatan terdekat ke sen terdekat". Menggunakan basis 2 atau basis 10 FP tidak memberikan keuntungan dalam skenario Anda.
chux
@ chux-ReinstateMonica: Dalam skenario di atas, jika matematika menunjukkan bahwa bunganya harus persis sama dengan sejumlah setengah sen, program keuangan yang benar harus dibulatkan dengan cara yang ditentukan secara tepat. Jika perhitungan floating-point menghasilkan nilai bunga misalnya $ 1.23499941, tetapi nilai yang tepat secara matematis sebelum pembulatan seharusnya $ 1.235 dan pembulatan ditentukan sebagai "even even", penggunaan perhitungan floating-point seperti itu tidak akan menyebabkan hasil pergi dengan $ 0,000059, tetapi dengan keseluruhan $ 0,01, yang untuk tujuan akuntansi adalah Just Plain Wrong.
supercat
@supercat Menggunakan doubleFP biner ke cent tidak akan kesulitan menghitung ke 0,5 cent karena tidak akan FP desimal. Jika perhitungan floating-point menghasilkan nilai bunga misalnya 123.499941 ¢, baik melalui FP biner atau FP desimal, masalah pembulatan ganda adalah sama - tidak ada keuntungan juga. Premis Anda tampaknya mengasumsikan nilai yang tepat secara matematis dan FP desimal adalah sama - sesuatu yang bahkan FP desimal tidak menjamin. 0.5 / 7.0 * 7.0 adalah masalah untuk untuk biner dan deicmal FP. IAC, sebagian besar akan dapat diperdebatkan seperti yang saya harapkan versi C berikutnya untuk memberikan FP desimal.
chux - Reinstate Monica
11

Seperti yang dikatakan sebelumnya, "Merepresentasikan uang sebagai gandaan atau pelampung mungkin akan terlihat bagus pada awalnya ketika perangkat lunak menyelesaikan kesalahan kecil, tetapi ketika Anda melakukan lebih banyak penambahan, pengurangan, penggandaan, dan pembagian pada angka yang tidak tepat, Anda akan kehilangan lebih banyak dan lebih presisi karena kesalahan bertambah. Ini membuat float dan double tidak cukup untuk berurusan dengan uang, di mana akurasi sempurna untuk kelipatan kekuatan basis 10 diperlukan. "

Akhirnya Jawa memiliki cara standar untuk bekerja dengan Mata Uang dan Uang!

JSR 354: API Uang dan Mata Uang

JSR 354 menyediakan API untuk mewakili, mengangkut, dan melakukan perhitungan komprehensif dengan Uang dan Mata Uang. Anda dapat mengunduhnya dari tautan ini:

JSR 354: Unduhan API Uang dan Mata Uang

Spesifikasi terdiri dari hal-hal berikut:

  1. API untuk penanganan misalnya jumlah uang dan mata uang
  2. API untuk mendukung implementasi yang dapat dipertukarkan
  3. Pabrik untuk membuat instance dari kelas implementasi
  4. Fungsi untuk perhitungan, konversi, dan pemformatan jumlah uang
  5. API Java untuk bekerja dengan Uang dan Mata Uang, yang rencananya akan dimasukkan dalam Java 9.
  6. Semua kelas spesifikasi dan antarmuka terletak di paket javax.money. *.

Contoh Contoh JSR 354: API Uang dan Mata Uang:

Contoh membuat MonetaryAmount dan mencetaknya ke konsol terlihat seperti ini ::

MonetaryAmountFactory<?> amountFactory = Monetary.getDefaultAmountFactory();
MonetaryAmount monetaryAmount = amountFactory.setCurrency(Monetary.getCurrency("EUR")).setNumber(12345.67).create();
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

Saat menggunakan API implementasi referensi, kode yang diperlukan jauh lebih sederhana:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

API juga mendukung perhitungan dengan MonetaryAmounts:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));

CurrencyUnit dan MonetaryAmount

// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);

MonetaryAmount memiliki berbagai metode yang memungkinkan mengakses mata uang yang ditetapkan, jumlah numerik, ketepatannya, dan lainnya:

MonetaryAmount monetaryAmount = Money.of(123.45, euro);
CurrencyUnit currency = monetaryAmount.getCurrency();
NumberValue numberValue = monetaryAmount.getNumber();

int intValue = numberValue.intValue(); // 123
double doubleValue = numberValue.doubleValue(); // 123.45
long fractionDenominator = numberValue.getAmountFractionDenominator(); // 100
long fractionNumerator = numberValue.getAmountFractionNumerator(); // 45
int precision = numberValue.getPrecision(); // 5

// NumberValue extends java.lang.Number. 
// So we assign numberValue to a variable of type Number
Number number = numberValue;

Jumlah Moneter dapat dibulatkan menggunakan operator pembulatan:

CurrencyUnit usd = MonetaryCurrencies.getCurrency("USD");
MonetaryAmount dollars = Money.of(12.34567, usd);
MonetaryOperator roundingOperator = MonetaryRoundings.getRounding(usd);
MonetaryAmount roundedDollars = dollars.with(roundingOperator); // USD 12.35

Saat bekerja dengan koleksi MonetaryAmounts, beberapa metode utilitas yang bagus untuk memfilter, menyortir dan mengelompokkan tersedia.

List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(2, "EUR"));
amounts.add(Money.of(42, "USD"));
amounts.add(Money.of(7, "USD"));
amounts.add(Money.of(13.37, "JPY"));
amounts.add(Money.of(18, "USD"));

Operasi MonetaryAmount khusus

// A monetary operator that returns 10% of the input MonetaryAmount
// Implemented using Java 8 Lambdas
MonetaryOperator tenPercentOperator = (MonetaryAmount amount) -> {
  BigDecimal baseAmount = amount.getNumber().numberValue(BigDecimal.class);
  BigDecimal tenPercent = baseAmount.multiply(new BigDecimal("0.1"));
  return Money.of(tenPercent, amount.getCurrency());
};

MonetaryAmount dollars = Money.of(12.34567, "USD");

// apply tenPercentOperator to MonetaryAmount
MonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator); // USD 1.234567

Sumber:

Menangani uang dan mata uang di Jawa dengan JSR 354

Melihat ke dalam Java 9 Money and Currency API (JSR 354)

Lihat Juga: JSR 354 - Mata Uang dan Uang

Affy
sumber
5

Jika perhitungan Anda melibatkan berbagai langkah, aritmatika presisi sewenang-wenang tidak akan mencakup Anda 100%.

Satu-satunya cara yang dapat diandalkan untuk menggunakan representasi hasil yang sempurna (Gunakan tipe data Fraksi khusus yang akan mengelompokkan operasi divisi ke langkah terakhir) dan hanya mengonversi ke notasi desimal pada langkah terakhir.

Presisi sewenang-wenang tidak akan membantu karena selalu ada angka yang memiliki begitu banyak tempat desimal, atau beberapa hasil seperti 0.6666666... Tidak ada representasi sewenang-wenang yang akan mencakup contoh terakhir. Jadi, Anda akan memiliki kesalahan kecil di setiap langkah.

Kesalahan ini akan bertambah, mungkin pada akhirnya menjadi tidak mudah untuk diabaikan lagi. Ini disebut Propagasi Kesalahan .

Kemal Dağ
sumber
4

Sebagian besar jawaban telah menyoroti alasan mengapa seseorang seharusnya tidak menggunakan ganda untuk perhitungan uang dan mata uang. Dan saya sangat setuju dengan mereka.

Itu tidak berarti bahwa ganda tidak pernah bisa digunakan untuk tujuan itu.

Saya telah mengerjakan sejumlah proyek dengan persyaratan gc yang sangat rendah, dan memiliki objek BigDecimal adalah kontributor besar untuk overhead itu.

Kurangnya pemahaman tentang representasi ganda dan kurangnya pengalaman dalam menangani akurasi dan presisi yang menghasilkan saran bijak ini.

Anda dapat membuatnya bekerja jika Anda mampu menangani persyaratan presisi dan akurasi proyek Anda, yang harus dilakukan berdasarkan pada rentang nilai ganda apa yang satu berurusan.

Anda dapat merujuk ke metode FuzzyCompare jambu biji untuk mendapatkan lebih banyak ide. Toleransi parameter adalah kuncinya. Kami menangani masalah ini untuk aplikasi perdagangan efek dan kami melakukan penelitian mendalam tentang toleransi apa yang digunakan untuk nilai numerik yang berbeda dalam rentang yang berbeda.

Juga, mungkin ada situasi ketika Anda tergoda untuk menggunakan pembungkus ganda sebagai kunci peta dengan peta hash sebagai implementasinya. Ini sangat berisiko karena Double.equals dan kode hash misalnya nilai "0,5" & "0,6 - 0,1" akan menyebabkan kekacauan besar.

Dev Amitabh
sumber
2

Banyak jawaban yang diposting untuk pertanyaan ini membahas IEEE dan standar seputar aritmatika floating-point.

Berasal dari latar belakang ilmu non-komputer (fisika dan teknik), saya cenderung melihat masalah dari sudut pandang yang berbeda. Bagi saya, alasan mengapa saya tidak akan menggunakan double atau float dalam perhitungan matematis adalah bahwa saya akan kehilangan terlalu banyak informasi.

Apa alternatifnya? Ada banyak (dan banyak lagi yang saya tidak sadari!).

BigDecimal di Jawa adalah asli ke bahasa Jawa. Apfloat adalah pustaka presisi arbitrer lain untuk Java.

Tipe data desimal dalam C # adalah Microsoft .NET alternatif untuk 28 angka penting.

SciPy (Scientific Python) mungkin juga dapat menangani perhitungan keuangan (saya belum mencoba, tapi saya curiga demikian).

GNU Multiple Precision Library (GMP) dan GNU MFPR Library adalah dua sumber bebas dan sumber terbuka untuk C dan C ++.

Ada juga perpustakaan presisi numerik untuk JavaScript (!) Dan saya pikir PHP yang dapat menangani perhitungan keuangan.

Ada juga hak milik (khususnya, saya pikir, untuk Fortran) dan solusi open-source juga untuk banyak bahasa komputer.

Saya bukan ilmuwan komputer dengan pelatihan. Namun, saya cenderung condong ke arah BigDecimal di Jawa atau desimal di C #. Saya belum mencoba solusi lain yang saya daftarkan, tetapi mungkin juga sangat bagus.

Bagi saya, saya suka BigDecimal karena metode yang didukungnya. Desimal C # sangat bagus, tetapi saya belum memiliki kesempatan untuk menggunakannya sebanyak yang saya inginkan. Saya melakukan perhitungan ilmiah yang menarik bagi saya di waktu luang saya, dan BigDecimal tampaknya bekerja dengan sangat baik karena saya dapat mengatur ketepatan angka floating point saya. Kerugian BigDecimal? Kadang-kadang bisa lambat, terutama jika Anda menggunakan metode membagi.

Untuk kecepatan, Anda dapat melihat perpustakaan gratis dan eksklusif di C, C ++, dan Fortran.

nelayan itu
sumber
1
Mengenai SciPy / Numpy, presisi-tetap (yaitu desimal Python. Khusus) tidak didukung ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). Beberapa fungsi tidak akan berfungsi dengan baik dengan Desimal (misalnya isnan). Panda didasarkan pada Numpy dan diprakarsai di AQR, satu hedge-fund kuantitatif utama. Jadi, Anda memiliki jawaban Anda mengenai perhitungan keuangan (bukan akuntansi belanjaan).
comte
2

Untuk menambahkan jawaban sebelumnya, ada juga opsi untuk mengimplementasikan Joda-Money di Jawa, selain BigDecimal, ketika berhadapan dengan masalah yang dibahas dalam pertanyaan. Nama modul Java adalah org.joda.money.

Ini membutuhkan Java SE 8 atau lebih baru dan tidak memiliki dependensi.

Untuk lebih tepatnya, ada ketergantungan waktu kompilasi tetapi tidak diperlukan.

<dependency>
  <groupId>org.joda</groupId>
  <artifactId>joda-money</artifactId>
  <version>1.0.1</version>
</dependency>

Contoh menggunakan Joda Money:

  // create a monetary value
  Money money = Money.parse("USD 23.87");

  // add another amount with safe double conversion
  CurrencyUnit usd = CurrencyUnit.of("USD");
  money = money.plus(Money.of(usd, 12.43d));

  // subtracts an amount in dollars
  money = money.minusMajor(2);

  // multiplies by 3.5 with rounding
  money = money.multipliedBy(3.5d, RoundingMode.DOWN);

  // compare two amounts
  boolean bigAmount = money.isGreaterThan(dailyWage);

  // convert to GBP using a supplied rate
  BigDecimal conversionRate = ...;  // obtained from code outside Joda-Money
  Money moneyGBP = money.convertedTo(CurrencyUnit.GBP, conversionRate, RoundingMode.HALF_UP);

  // use a BigMoney for more complex calculations where scale matters
  BigMoney moneyCalc = money.toBigMoney();

Dokumentasi: http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html

Contoh implementasi: https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money

Tadija Malić
sumber
0

Beberapa contoh ... ini berfungsi (sebenarnya tidak berfungsi seperti yang diharapkan), pada hampir semua bahasa pemrograman ... Saya sudah mencoba dengan Delphi, VBScript, Visual Basic, JavaScript dan sekarang dengan Java / Android:

    double total = 0.0;

    // do 10 adds of 10 cents
    for (int i = 0; i < 10; i++) {
        total += 0.1;  // adds 10 cents
    }

    Log.d("round problems?", "current total: " + total);

    // looks like total equals to 1.0, don't?

    // now, do reverse
    for (int i = 0; i < 10; i++) {
        total -= 0.1;  // removes 10 cents
    }

    // looks like total equals to 0.0, don't?
    Log.d("round problems?", "current total: " + total);
    if (total == 0.0) {
        Log.d("round problems?", "is total equal to ZERO? YES, of course!!");
    } else {
        Log.d("round problems?", "is total equal to ZERO? NO... thats why you should not use Double for some math!!!");
    }

KELUARAN:

round problems?: current total: 0.9999999999999999 round problems?: current total: 2.7755575615628914E-17 round problems?: is total equal to ZERO? NO... thats why you should not use Double for some math!!!

WilliamK
sumber
3
Masalahnya bukan kesalahan pembulatan itu terjadi, tetapi Anda tidak menghadapinya. Bulatkan hasilnya menjadi dua tempat desimal (jika Anda ingin sen) dan Anda selesai.
maaartinus
0

Float adalah bentuk biner Desimal dengan desain yang berbeda; mereka adalah dua hal yang berbeda. Ada sedikit kesalahan antara dua jenis ketika dikonversi satu sama lain. Juga, float dirancang untuk mewakili sejumlah besar nilai tak terbatas untuk ilmiah. Itu berarti ia dirancang untuk kehilangan presisi ke jumlah sangat kecil dan sangat ekstrim dengan jumlah byte yang tetap. Desimal tidak dapat mewakili jumlah nilai yang tak terbatas, ia hanya terbatas pada angka desimal itu saja. Jadi Float dan Desimal untuk tujuan yang berbeda.

Ada beberapa cara untuk mengelola kesalahan untuk nilai mata uang:

  1. Gunakan bilangan bulat panjang dan hitung dalam sen.

  2. Gunakan presisi ganda, pertahankan angka signifikan Anda hanya sampai 15 sehingga desimal dapat disimulasikan dengan tepat. Bulat sebelum menyajikan nilai; Bulat sering ketika melakukan perhitungan.

  3. Gunakan perpustakaan desimal seperti Java BigDecimal sehingga Anda tidak perlu menggunakan ganda untuk mensimulasikan desimal.

ps menarik untuk mengetahui bahwa sebagian besar merek kalkulator ilmiah genggam bekerja pada desimal alih-alih mengambang. Jadi tidak ada satu keluhan kesalahan konversi float.

Chris Tsang
sumber