Tính toán tài chính chính xác trong JavaScript. Gotchas là gì?


125

Để tạo mã đa nền tảng, tôi muốn phát triển một ứng dụng tài chính đơn giản trong JavaScript. Các tính toán cần thiết liên quan đến lãi kép và số thập phân tương đối dài. Tôi muốn biết những lỗi cần tránh khi sử dụng JavaScript để thực hiện loại toán Toán này nếu có thể!

Câu trả lời:


107

Bạn có thể nên chia tỷ lệ giá trị thập phân của mình cho 100 và đại diện cho tất cả các giá trị tiền tệ trong toàn bộ xu. Điều này là để tránh các vấn đề với logic và số học dấu phẩy động . Không có kiểu dữ liệu thập phân trong JavaScript - loại dữ liệu số duy nhất là dấu phẩy động. Do đó, thông thường nên xử lý tiền như 2550xu thay vì 25.50đô la.

Hãy xem xét điều đó trong JavaScript:

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

Nhưng:

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

Biểu thức 0.1 + 0.2 === 0.3trả về false, nhưng may mắn là số học số nguyên trong dấu phẩy động là chính xác, do đó có thể tránh được các lỗi biểu diễn thập phân bằng cách chia tỷ lệ 1 .

Lưu ý rằng trong khi tập hợp các số thực là vô hạn, chỉ có một số hữu hạn trong số chúng (chính xác là 18,437,736,874,454,810,627) có thể được biểu diễn chính xác bằng định dạng dấu phẩy động JavaScript. Do đó, đại diện của các số khác sẽ là một xấp xỉ của số 2 thực tế .


1 Douglas Crockford: JavaScript: Các bộ phận tốt : Phụ lục A - Các bộ phận khủng khiếp (trang 105) .
2 David Flanagan: JavaScript: Hướng dẫn dứt khoát, Phiên bản thứ tư : 3.1.3 Văn học dấu phẩy động (trang 31) .


3
Và như một lời nhắc nhở, luôn luôn làm tròn các phép tính đến phần trăm và làm như vậy theo cách ít có lợi nhất cho người tiêu dùng, IE Nếu bạn đang tính thuế, hãy làm tròn. Nếu bạn đang tính lãi kiếm được, hãy cắt bớt.
Josh

6
@Cirrostratus: Bạn có thể muốn kiểm tra stackoverflow.com/questions/744099 . Nếu bạn tiếp tục với phương pháp chia tỷ lệ, nói chung, bạn sẽ muốn chia tỷ lệ giá trị của mình theo số chữ số thập phân bạn muốn giữ chính xác. Nếu bạn cần 2 chữ số thập phân, tỷ lệ 100, nếu bạn cần 4, tỷ lệ 10000.
Daniel Vassallo

2
... Về giá trị 3000,57, vâng, nếu bạn lưu trữ giá trị đó trong các biến JavaScript và bạn dự định thực hiện số học trên nó, bạn có thể muốn lưu trữ nó được chia tỷ lệ thành 300057 (số xu). Vì 3000.57 + 0.11 === 3000.68lợi nhuận false.
Daniel Vassallo

7
Đếm đồng xu thay vì đô la sẽ không giúp được gì. Khi đếm từng đồng xu, bạn mất khả năng thêm 1 vào một số nguyên vào khoảng 10 ^ 16. Khi đếm đô la, bạn mất khả năng thêm 0,01 vào một số tại 10 ^ 14. Cả hai cách đều giống nhau.
slashingweapon

1
Tôi vừa tạo một mô-đun npm và Bower hy vọng sẽ giúp với nhiệm vụ này!
Pensierinmusica

18

Thu nhỏ mọi giá trị bằng 100 là giải pháp. Làm điều đó bằng tay có lẽ là vô ích, vì bạn có thể tìm thấy các thư viện làm điều đó cho bạn. Tôi khuyên dùng moneysafe, cung cấp API chức năng rất phù hợp cho các ứng dụng ES6:

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

https://github.com/ericelliott/moneysafe

Hoạt động cả trong Node.js và trình duyệt.


2
Nâng cao. Điểm "scale by 100" đã được bao phủ trong câu trả lời được chấp nhận, tuy nhiên, thật tốt khi bạn đã thêm tùy chọn gói phần mềm với cú pháp JavaScript hiện đại. FWIW in$, $ tên giá trị không rõ ràng đối với người không sử dụng gói trước đó. Tôi biết rằng đó là lựa chọn của Eric để đặt tên theo cách đó, nhưng tôi vẫn cảm thấy đủ sai lầm mà có lẽ tôi đã đổi tên chúng trong tuyên bố yêu cầu nhập / hủy.
james_womack

4
Thu nhỏ theo 100 chỉ giúp cho đến khi bạn bắt đầu muốn làm một cái gì đó như tính tỷ lệ phần trăm (thực hiện phân chia, về cơ bản).
Mũi nhọn

2
Tôi ước tôi có thể nâng cao nhận xét nhiều lần. Thu nhỏ bằng 100 chỉ là không đủ. Kiểu dữ liệu số duy nhất trong JavaScript vẫn là kiểu dữ liệu dấu phẩy động và bạn vẫn sẽ gặp phải các lỗi làm tròn đáng kể.
Craig

1
Và một người khác đứng lên từ readme : Money$afe has not yet been tested in production at scale.. Chỉ cần chỉ ra điều đó để bất cứ ai cũng có thể cân nhắc nếu điều đó phù hợp với trường hợp sử dụng của họ
Nobita

7

Không có thứ gọi là tính toán tài chính "chính xác" vì chỉ có hai chữ số thập phân nhưng đó là một vấn đề tổng quát hơn.

Trong JavaScript, bạn có thể chia tỷ lệ cho mọi giá trị theo 100 và sử dụng Math.round()mỗi khi một phân số có thể xảy ra.

Bạn có thể sử dụng một đối tượng để lưu trữ các số và bao gồm làm tròn trong valueOf()phương thức nguyên mẫu của nó . Như thế này:

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

Theo cách đó, mỗi khi bạn sử dụng một đối tượng Tiền, nó sẽ được biểu diễn dưới dạng làm tròn thành hai số thập phân. Giá trị không có căn cứ vẫn có thể truy cập thông qua m.amount.

Bạn có thể xây dựng trong thuật toán làm tròn của riêng bạn vào Money.prototype.valueOf(), nếu bạn muốn.


Tôi thích cách tiếp cận hướng đối tượng này, thực tế là đối tượng Money giữ cả hai giá trị rất hữu ích. Đó là loại chức năng chính xác mà tôi muốn tạo trong các lớp Objective-C tùy chỉnh của mình.
james_womack

4
Nó không đủ chính xác để làm tròn.
Henry Tseng

3
Không nên sys.puts (m + n); //80.76 thực sự đọc sys.puts (m + n); //80.77? Tôi tin rằng bạn đã quên làm tròn 0,5.
Dave L

2
Cách tiếp cận này có một số vấn đề tinh tế có thể mọc lên. Chẳng hạn, bạn đã không triển khai các phương pháp cộng, trừ, nhân, v.v. an toàn, do đó bạn có thể gặp phải các lỗi làm tròn khi kết hợp số tiền
1800 THÔNG TIN

2
Vấn đề ở đây là ví dụ, Money(0.1)có nghĩa là nhà từ điển JavaScript đọc chuỗi "0,1" từ nguồn và sau đó chuyển đổi nó thành một dấu phẩy động nhị phân và sau đó bạn đã thực hiện làm tròn số ngoài ý muốn. Vấn đề là về đại diện (nhị phân so với thập phân) không phải về độ chính xác .
mgd

3

sử dụng binaryjs ... Đó là một thư viện rất tốt giải quyết một phần khắc nghiệt của vấn đề ...

Chỉ cần sử dụng nó trong tất cả các hoạt động của bạn.

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


2

Vấn đề của bạn bắt nguồn từ sự không chính xác trong tính toán dấu phẩy động. Nếu bạn chỉ sử dụng làm tròn để giải quyết điều này, bạn sẽ gặp lỗi lớn hơn khi nhân và chia.

Giải pháp dưới đây, một lời giải thích sau:

Bạn sẽ cần suy nghĩ về toán học đằng sau điều này để hiểu nó. Các số thực như 1/3 không thể được biểu diễn trong toán học với các giá trị thập phân vì chúng là vô tận (ví dụ: .333333333333333 ...). Một số số thập phân không thể được biểu diễn chính xác trong nhị phân. Ví dụ, 0,1 không thể được biểu diễn chính xác trong nhị phân với số chữ số giới hạn.

Để biết mô tả chi tiết hơn, hãy xem tại đây: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Hãy xem triển khai giải pháp: http://floating-point-gui.de/lacular/javascript/


1

Do tính chất nhị phân của mã hóa, một số số thập phân không thể được biểu diễn với độ chính xác hoàn hảo. Ví dụ

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

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

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

Nếu bạn cần sử dụng javascript thuần túy thì bạn cần phải suy nghĩ về giải pháp cho mọi tính toán. Đối với mã trên, chúng ta có thể chuyển đổi số thập phân thành toàn bộ số nguyên.

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

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

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

Tránh các vấn đề với Toán học thập phân trong JavaScript

Có một thư viện dành riêng cho các tính toán tài chính với tài liệu tuyệt vời. Tài chính.js


Tôi thích rằng Finance.js cũng có các ứng dụng ví dụ
james_womack

0

Thật không may, tất cả các câu trả lời cho đến nay đều bỏ qua thực tế là không phải tất cả các loại tiền tệ đều có 100 đơn vị con (ví dụ: cent là đơn vị phụ của đồng đô la Mỹ (USD)). Các loại tiền tệ như Dinar Iraq (IQD) có 1000 đơn vị phụ: một Dinar Iraq có 1000 phim. Yên Nhật (JPY) không có đơn vị phụ. Vì vậy, "nhân với 100 để làm số học số nguyên" không phải lúc nào cũng là câu trả lời đúng.

Ngoài ra để tính toán tiền tệ, bạn cũng cần theo dõi tiền tệ. Bạn không thể thêm Đô la Mỹ (USD) vào Rupee Ấn Độ (INR) (mà không chuyển đổi lần đầu tiên sang đồng đô la khác).

Cũng có những hạn chế về số lượng tối đa có thể được biểu thị bằng loại dữ liệu số nguyên của JavaScript.

Trong tính toán tiền tệ, bạn cũng phải nhớ rằng tiền có độ chính xác hữu hạn (thường là 0-3 điểm thập phân) & làm tròn cần phải được thực hiện theo những cách cụ thể (ví dụ: làm tròn "bình thường" so với làm tròn ngân hàng). Loại làm tròn được thực hiện cũng có thể thay đổi tùy theo thẩm quyền / loại tiền.

Cách xử lý tiền trong javascript có một cuộc thảo luận rất tốt về các điểm có liên quan.

Trong các tìm kiếm của mình, tôi đã tìm thấy thư viện dinero.js giải quyết nhiều vấn đề về tính toán tiền tệ . Chưa sử dụng nó trong một hệ thống sản xuất nên không thể đưa ra ý kiến ​​có căn cứ về nó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.