Perhitungan Keuangan Yang Tepat di JavaScript. Apakah Gotcha Itu?

125

Untuk kepentingan membuat kode lintas platform, saya ingin mengembangkan aplikasi keuangan sederhana dalam JavaScript. Perhitungan yang diperlukan melibatkan bunga majemuk dan angka desimal yang relatif panjang. Saya ingin tahu kesalahan apa yang harus dihindari saat menggunakan JavaScript untuk melakukan jenis matematika ini — jika memungkinkan!

james_womack
sumber

Jawaban:

107

Anda mungkin harus menskalakan nilai desimal Anda dengan 100, dan mewakili semua nilai moneter dalam sen penuh. Ini untuk menghindari masalah dengan logika floating-point dan aritmatika . Tidak ada tipe data desimal di JavaScript - satu-satunya tipe data numerik adalah floating-point. Oleh karena itu, umumnya disarankan untuk menangani uang sebagai 2550sen, bukan 25.50dolar.

Pertimbangkan itu di JavaScript:

var result = 1.0 + 2.0;     // (result === 3.0) returns true

Tapi:

var result = 0.1 + 0.2;     // (result === 0.3) returns false

Ekspresi 0.1 + 0.2 === 0.3kembali false, tetapi untungnya aritmatika integer dalam floating-point tepat, sehingga kesalahan representasi desimal dapat dihindari dengan penskalaan 1 .

Perhatikan bahwa sementara himpunan bilangan real tidak terbatas, hanya bilangan terbatas dari mereka (tepatnya 18.437.736.874.454.810.627) yang dapat direpresentasikan secara tepat oleh format titik mengambang JavaScript. Oleh karena itu representasi bilangan lain akan menjadi perkiraan dari bilangan sebenarnya 2 .


1 Douglas Crockford: JavaScript: Bagian yang Baik : Lampiran A - Bagian yang Mengerikan (halaman 105) .
2 David Flanagan: JavaScript: Panduan Definitif, Edisi Keempat : 3.1.3 Literal Titik Mengambang (halaman 31) .

Daniel Vassallo
sumber
3
Dan sebagai pengingat, selalu bulatkan kalkulasi ke sen, dan lakukan dengan cara yang paling tidak menguntungkan bagi konsumen, YAITU Jika Anda menghitung pajak, bulatkan. Jika Anda menghitung perolehan bunga, potong.
Josh
6
@Cirrostratus: Anda mungkin ingin memeriksa stackoverflow.com/questions/744099 . Jika Anda melanjutkan dengan metode penskalaan, secara umum Anda ingin menskalakan nilai Anda dengan jumlah digit desimal yang ingin Anda pertahankan presisi. Jika Anda membutuhkan 2 tempat desimal, skala dengan 100, jika Anda membutuhkan 4, skala dengan 10000.
Daniel Vassallo
2
... Mengenai nilai 3000,57, ya, jika Anda menyimpan nilai itu dalam variabel JavaScript, dan Anda bermaksud untuk melakukan aritmatika padanya, Anda mungkin ingin menyimpannya dengan skala 300057 (jumlah sen). Karena 3000.57 + 0.11 === 3000.68pengembalian false.
Daniel Vassallo
7
Menghitung uang bukan dolar tidak akan membantu. Saat menghitung uang, Anda kehilangan kemampuan untuk menambahkan 1 ke bilangan bulat sekitar 10 ^ 16. Saat menghitung dolar, Anda kehilangan kemampuan untuk menambahkan 0,01 ke angka pada 10 ^ 14. Itu sama saja.
Slashingweapon
1
Saya baru saja membuat modul npm dan Bower yang semoga membantu tugas ini!
Pensierinmusica
18

Menskalakan setiap nilai dengan 100 adalah solusinya. Melakukannya dengan tangan mungkin tidak berguna, karena Anda dapat menemukan perpustakaan yang melakukannya untuk Anda. Saya merekomendasikan moneysafe, yang menawarkan API fungsional yang cocok untuk aplikasi ES6:

const { in$, $ } = require('moneysafe');
console.log(in$($(10.5) + $(.3)); // 10.8

https://github.com/ericelliott/moneysafe

Bekerja di Node.js dan browser.

François Zaninotto
sumber
2
Suara positif. Poin "skala dengan 100" sudah tercakup dalam jawaban yang diterima, namun ada baiknya Anda menambahkan opsi paket perangkat lunak dengan sintaks JavaScript modern. FWIW in$, $ nama nilai ambigu untuk seseorang yang belum pernah menggunakan paket sebelumnya. Saya tahu itu adalah pilihan Eric untuk menamai hal-hal seperti itu, tetapi saya masih merasa itu adalah kesalahan yang cukup sehingga saya mungkin akan mengganti namanya dalam pernyataan persyaratan impor / dirusak.
james_womack
4
Penskalaan 100 hanya membantu sampai Anda mulai ingin melakukan sesuatu seperti menghitung persentase (melakukan pembagian, pada dasarnya).
Pointy
2
Saya berharap saya dapat memberi suara positif beberapa kali. Penskalaan dengan 100 saja tidak cukup. Satu-satunya tipe data numerik di JavaScript masih merupakan tipe data titik mengambang, dan Anda masih akan mendapatkan kesalahan pembulatan yang signifikan.
Craig
1
Dan kepala lain naik dari readme yang: Money$afe has not yet been tested in production at scale.. Hanya menunjukkan hal itu sehingga siapa pun dapat mempertimbangkan apakah itu sesuai untuk kasus penggunaan mereka
Nobita
7

Tidak ada yang namanya kalkulasi finansial yang "tepat" karena hanya menggunakan dua digit pecahan desimal, tetapi itu adalah masalah yang lebih umum.

Dalam JavaScript, Anda dapat menskalakan setiap nilai dengan 100 dan menggunakan Math.round() setiap pecahan dapat terjadi.

Anda dapat menggunakan objek untuk menyimpan angka dan menyertakan pembulatan dalam valueOf()metode prototipe . Seperti ini:

sys = require('sys');

var Money = function(amount) {
        this.amount = amount;
    }
Money.prototype.valueOf = function() {
    return Math.round(this.amount*100)/100;
}

var m = new Money(50.42355446);
var n = new Money(30.342141);

sys.puts(m.amount + n.amount); //80.76569546
sys.puts(m+n); //80.76

Dengan cara itu, setiap kali Anda menggunakan objek Uang, objek tersebut akan dibulatkan menjadi dua desimal. Nilai yang tidak dibulatkan masih dapat diakses melalui m.amount.

Anda dapat membangun algoritme pembulatan Anda sendiri Money.prototype.valueOf(), jika Anda mau.

selfawaresoup
sumber
Saya suka pendekatan berorientasi objek ini, fakta bahwa objek Uang memiliki kedua nilai sangat berguna. Ini adalah jenis fungsionalitas persis yang ingin saya buat di kelas Objective-C kustom saya.
james_womack
4
Ini tidak cukup akurat untuk dibulatkan.
Henry Tseng
3
Seharusnya tidak sys.puts (m + n); //80.76 sebenarnya membaca sys.puts (m + n); //80.77? Saya yakin Anda lupa membulatkan 0,5 ke atas.
Dave L
2
Pendekatan semacam ini memiliki sejumlah masalah halus yang dapat muncul. Misalnya, Anda belum menerapkan metode penjumlahan, pengurangan, perkalian yang aman, dan sebagainya, sehingga kemungkinan besar Anda akan mengalami kesalahan pembulatan saat menggabungkan jumlah uang
1800 INFORMATION
2
Masalahnya di sini adalah bahwa misalnya Money(0.1)lexer JavaScript membaca string "0.1" dari sumber dan kemudian mengubahnya menjadi floating point biner dan kemudian Anda sudah melakukan pembulatan yang tidak diinginkan. Masalahnya adalah tentang representasi (biner vs desimal) bukan tentang presisi .
mgd
3

gunakan decimaljs ... Ini adalah perpustakaan yang sangat bagus yang memecahkan bagian sulit dari masalah ...

gunakan saja di semua operasi Anda.

https://github.com/MikeMcl/decimal.js/

tlejmi
sumber
2

Masalah Anda berasal dari ketidakakuratan dalam perhitungan floating point. Jika Anda hanya menggunakan pembulatan untuk menyelesaikannya, Anda akan mendapatkan kesalahan yang lebih besar saat mengalikan dan membagi.

Solusinya dibawah ini, penjelasannya sbb:

Anda harus memikirkan matematika di balik ini untuk memahaminya. Bilangan real seperti 1/3 tidak dapat direpresentasikan dalam matematika dengan nilai desimal karena tidak ada habisnya (misalnya - .333333333333333 ...). Beberapa angka dalam desimal tidak dapat direpresentasikan dalam biner dengan benar. Misalnya, 0,1 tidak dapat direpresentasikan dalam biner dengan benar dengan jumlah digit yang terbatas.

Untuk penjelasan lebih rinci lihat di sini: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Lihat implementasi solusi: http://floating-point-gui.de/languages/javascript/

Henry Tseng
sumber
1

Karena sifat biner pengkodeannya, beberapa bilangan desimal tidak dapat direpresentasikan dengan akurasi yang sempurna. Sebagai contoh

var money = 600.90;
var price = 200.30;
var total = price * 3;

// Outputs: false
console.log(money >= total);

// Outputs: 600.9000000000001
console.log(total);

Jika Anda perlu menggunakan javascript murni maka Anda harus memikirkan solusi untuk setiap perhitungan. Untuk kode di atas kita dapat mengubah desimal menjadi bilangan bulat utuh.

var money = 60090;
var price = 20030;
var total = price * 3;

// Outputs: true
console.log(money >= total);

// Outputs: 60090
console.log(total);

Menghindari Masalah dengan Matematika Desimal di JavaScript

Ada perpustakaan khusus untuk perhitungan keuangan dengan dokumentasi yang bagus. Finance.js

Ali Rehman
sumber
Saya suka Finance.js juga memiliki aplikasi contoh
james_womack
0

Sayangnya semua jawaban sejauh ini mengabaikan fakta bahwa tidak semua mata uang memiliki 100 sub-unit (misalnya, sen adalah sub-unit dari dolar AS (USD)). Mata uang seperti Dinar Irak (IQD) memiliki 1000 sub-unit: Dinar Irak memiliki 1000 fils. Yen Jepang (JPY) tidak memiliki sub-unit. Jadi "kalikan dengan 100 untuk melakukan aritmatika integer" tidak selalu merupakan jawaban yang benar.

Selain itu, untuk kalkulasi moneter, Anda juga perlu melacak mata uang. Anda tidak dapat menambahkan Dolar AS (USD) ke Rupee India (INR) (tanpa mengonversikan satu ke yang lain terlebih dahulu).

Ada juga batasan jumlah maksimum yang dapat diwakili oleh tipe data integer JavaScript.

Dalam kalkulasi moneter, Anda juga harus ingat bahwa uang memiliki presisi yang terbatas (biasanya 0-3 desimal poin) & pembulatan perlu dilakukan dengan cara tertentu (misalnya, pembulatan "normal" vs. pembulatan bankir). Jenis pembulatan yang akan dilakukan mungkin juga berbeda menurut yurisdiksi / mata uang.

Bagaimana menangani uang di javascript memiliki diskusi yang sangat bagus tentang poin-poin yang relevan.

Dalam pencarian saya, saya menemukan perpustakaan dinero.js yang membahas banyak masalah dengan kalkulasi moneter . Belum menggunakannya dalam sistem produksi jadi tidak bisa memberikan opini yang terinformasi.

markvgti
sumber