Tại sao không sử dụng Double hoặc Float để đại diện cho tiền tệ?


939

Tôi luôn được dặn là không bao giờ đại diện cho tiền doublehoặc floatcác loại, và lần này tôi đặt ra câu hỏi cho bạn: tại sao?

Tôi chắc chắn có một lý do rất tốt, tôi chỉ đơn giản là không biết nó là gì.


4
Xem câu hỏi SO này: Lỗi làm tròn?
Jeff Ogata

80
Để rõ ràng, chúng không nên được sử dụng cho bất cứ điều gì đòi hỏi sự chính xác - không chỉ tiền tệ.
Jeff

152
Chúng không nên được sử dụng cho bất cứ điều gì đòi hỏi sự chính xác . Nhưng gấp đôi 53 bit đáng kể (~ 16 chữ số thập phân) thường đủ tốt cho những thứ chỉ đòi hỏi độ chính xác .
dan04

21
@jeff Nhận xét của bạn hoàn toàn sai về điểm nổi nhị phân là tốt cho cái gì và nó không tốt cho cái gì. Đọc câu trả lời của zneak bên dưới, và vui lòng xóa bình luận sai lệch của bạn.
Pascal Cuoq

Câu trả lời:


985

Bởi vì số float và double không thể đại diện chính xác cho 10 bội số cơ bản mà chúng ta sử dụng để kiếm tiền. Vấn đề này không chỉ dành cho Java, nó dành cho bất kỳ ngôn ngữ lập trình nào sử dụng các kiểu dấu phẩy động cơ sở 2.

Trong cơ sở 10, bạn có thể viết 10,25 là 1025 * 10 -2 (số nguyên nhân với lũy thừa 10). Các số dấu phẩy động của IEEE-754 là khác nhau, nhưng một cách rất đơn giản để suy nghĩ về chúng là nhân với một lũy thừa hai thay vào đó. Chẳng hạn, bạn có thể nhìn vào 164 * 2 -4 (số nguyên nhân với lũy thừa bằng hai), cũng bằng 10,25. Đó không phải là cách các con số được biểu diễn trong bộ nhớ, nhưng ý nghĩa toán học là như nhau.

Ngay cả trong cơ sở 10, ký hiệu này không thể biểu thị chính xác hầu hết các phân số đơn giản. Chẳng hạn, bạn không thể biểu thị 1/3: biểu diễn thập phân đang lặp lại (0,3333 ...), do đó không có số nguyên hữu hạn mà bạn có thể nhân với lũy thừa 10 để có được 1/3. Bạn có thể giải quyết một chuỗi dài 3 và một số mũ nhỏ, như 333333333 * 10 -10 , nhưng điều đó không chính xác: nếu bạn nhân số đó với 3, bạn sẽ không nhận được 1.

Tuy nhiên, với mục đích đếm tiền, ít nhất là đối với các quốc gia có tiền được định giá theo mức độ lớn của đồng đô la Mỹ, thông thường tất cả những gì bạn cần là có thể lưu trữ bội số của 10 -2 , vì vậy điều đó không thực sự quan trọng 1/3 không thể được đại diện.

Vấn đề với số float và double là phần lớn các số giống như tiền không có đại diện chính xác là số nguyên nhân với lũy thừa là 2. Thực tế, bội số duy nhất của 0,01 giữa 0 và 1 (rất có ý nghĩa khi giao dịch bằng tiền vì chúng là số nguyên xu) có thể được biểu diễn chính xác dưới dạng số dấu phẩy động nhị phân IEEE-754 là 0, 0,25, 0,5, 0,75 và 1. Tất cả các số khác đều bị giảm một lượng nhỏ. Tương tự như ví dụ 0.333333, nếu bạn lấy giá trị dấu phẩy động cho 0,1 và bạn nhân nó với 10, bạn sẽ không nhận được 1.

Đại diện cho tiền như là một doublehoặc floatcó thể sẽ nhìn tốt lúc đầu là vòng phần mềm tắt các lỗi nhỏ, nhưng khi bạn thực hiện bổ sung hơn về cộng, trừ, nhân, chia trên số không chính xác, sai sót sẽ cộng gộp lại và bạn sẽ kết thúc với giá trị mà là rõ ràng không chính xác. Điều này làm cho số float và nhân đôi không đủ để giao dịch với tiền, trong đó cần có độ chính xác hoàn hảo cho bội số của sức mạnh cơ sở 10.

Một giải pháp hoạt động với bất kỳ ngôn ngữ nào là sử dụng số nguyên thay thế và đếm xu. Chẳng hạn, 1025 sẽ là 10,25 đô la. Một số ngôn ngữ cũng có các loại tích hợp để giao dịch với tiền. Trong số những người khác, Java có BigDecimallớp và C # có decimalloại.


3
@Fran Bạn sẽ nhận được các lỗi làm tròn và trong một số trường hợp sử dụng số lượng lớn tiền tệ, việc tính toán lãi suất có thể được giảm hoàn toàn
linuxuser27

5
... hầu hết 10 phân số cơ bản, đó là. Ví dụ, 0,1 không có biểu diễn dấu phẩy động chính xác nhị phân. Vì vậy, 1.0 / 10 * 10có thể không giống như 1.0.
Chris Jester-Young

6
@ linuxuser27 Tôi nghĩ rằng Fran đã cố gắng để vui vẻ. Dù sao, câu trả lời của zneak là tốt nhất tôi từng thấy, thậm chí còn tốt hơn cả phiên bản cổ điển của Bloch.
Isaac Rabinovitch

5
Tất nhiên nếu bạn biết độ chính xác, bạn luôn có thể làm tròn kết quả và do đó tránh được toàn bộ vấn đề. Điều này nhanh hơn và đơn giản hơn nhiều so với việc sử dụng BigDecimal. Một cách khác là sử dụng độ chính xác cố định int hoặc dài.
Peter Lawrey

2
@zneak bạn có biết rằng các số hữu tỷ là tập con của các số thực không? Số thực của IEEE-754 số thực. Họ chỉ xảy ra với cũng là hợp lý.
Tim Seguine

314

Từ Bloch, J., Java hiệu quả, tái bản lần 2, Mục 48:

Các loại floatdoubleđặc biệt không phù hợp cho các tính toán tiền tệ vì không thể biểu thị 0,1 (hoặc bất kỳ sức mạnh tiêu cực nào khác của mười) là một floathoặc doublechính xác.

Ví dụ: giả sử bạn có $ 1,03 và bạn chi 42c. Bạn còn lại bao nhiêu tiền

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

in ra 0.6100000000000001.

Cách đúng để giải quyết vấn đề này là sử dụng BigDecimal,int hoặc long để tính toán tiền tệ.

Mặc dù BigDecimalcó một số cảnh báo (xin vui lòng xem câu trả lời hiện được chấp nhận).


6
Tôi hơi bối rối trước khuyến nghị sử dụng int hoặc dài để tính toán tiền tệ. Làm thế nào để bạn đại diện cho 1.03 là một int hoặc dài? Tôi đã thử "dài a = 1,04;" và "dài a = 104/100;" vô ích.
Peter

49
@Peter, bạn sử dụng long a = 104và đếm bằng xu thay vì đô la.
zneak 17/03/2016

@zneak Còn khi tỷ lệ phần trăm cần được áp dụng như lãi kép hoặc tương tự thì sao?
trusktr

3
@trusktr, tôi sẽ sử dụng loại thập phân của nền tảng của bạn. Trong Java, đó là BigDecimal.
zneak

13
@maaartinus ... và bạn không nghĩ sử dụng gấp đôi cho những thứ như vậy là dễ bị lỗi? Tôi đã thấy vấn đề làm tròn số nổi lên các hệ thống thực sự khó khăn . Ngay cả trong ngân hàng. Vui lòng không đề xuất nó, hoặc nếu bạn làm thế, hãy cung cấp câu trả lời riêng biệt (để chúng tôi có thể đánh giá thấp nó: P)
eis

75

Đây không phải là vấn đề chính xác, cũng không phải là vấn đề chính xác. Vấn đề là đáp ứng sự mong đợi của những người sử dụng cơ sở 10 để tính toán thay vì cơ sở 2. Ví dụ: sử dụng gấp đôi để tính toán tài chính không tạo ra câu trả lời "sai" theo nghĩa toán học, nhưng nó có thể tạo ra câu trả lời không phải là những gì được mong đợi trong một ý nghĩa tài chính.

Ngay cả khi bạn làm tròn kết quả của mình vào phút cuối trước khi xuất, đôi khi bạn vẫn có thể nhận được kết quả bằng cách sử dụng gấp đôi không phù hợp với mong đợi.

Sử dụng máy tính hoặc tính toán kết quả bằng tay, chính xác là 1,40 * 165 = 231. Tuy nhiên, bên trong sử dụng nhân đôi, trên môi trường trình biên dịch / hệ điều hành của tôi, nó được lưu trữ dưới dạng số nhị phân gần 230.99999 ... vì vậy, nếu bạn cắt bớt số, bạn nhận được 230 thay vì 231. Bạn có thể lý do rằng làm tròn thay vì cắt ngắn sẽ đã đưa ra kết quả mong muốn của 231. Điều đó là đúng, nhưng làm tròn luôn liên quan đến việc cắt ngắn. Dù bạn sử dụng kỹ thuật làm tròn nào, vẫn có những điều kiện biên như thế này sẽ làm tròn xuống khi bạn mong đợi nó sẽ làm tròn. Chúng hiếm đến mức chúng thường không được tìm thấy thông qua thử nghiệm hoặc quan sát thông thường. Bạn có thể phải viết một số mã để tìm kiếm các ví dụ minh họa các kết quả không hoạt động như mong đợi.

Giả sử bạn muốn làm tròn một cái gì đó đến đồng xu gần nhất. Vì vậy, bạn lấy kết quả cuối cùng của mình, nhân với 100, thêm 0,5, cắt ngắn, sau đó chia kết quả cho 100 để lấy lại đồng xu. Nếu số nội bộ bạn đã lưu trữ là 3.46499999 .... thay vì 3.465, bạn sẽ nhận được 3,46 thay vì 3,47 khi bạn làm tròn số đến đồng xu gần nhất. Nhưng các tính toán cơ sở 10 của bạn có thể đã chỉ ra rằng câu trả lời phải chính xác là 3.465, trong đó rõ ràng nên làm tròn đến 3,47, không giảm xuống 3,46. Những điều này đôi khi xảy ra trong cuộc sống thực khi bạn sử dụng gấp đôi để tính toán tài chính. Nó là hiếm, vì vậy nó thường không được chú ý là một vấn đề, nhưng nó xảy ra.

Nếu bạn sử dụng cơ sở 10 cho các tính toán nội bộ của mình thay vì nhân đôi, các câu trả lời luôn chính xác là những gì con người mong đợi, giả sử không có lỗi nào khác trong mã của bạn.


2
Liên quan, thú vị: Trong bảng điều khiển chrome js của tôi: Math.round (.4999999999999999): 0 Math.round (.49999999999999999): 1
Curtis Yallop

16
Câu trả lời này là sai lệch. 1,40 * 165 = 231. Bất kỳ số nào ngoài chính xác 231 đều sai theo nghĩa toán học (và tất cả các giác quan khác).
Karu

2
@Karu Tôi nghĩ đó là lý do tại sao Randy nói rằng phao là xấu ... Kết quả là bảng điều khiển Chrome JS của tôi hiển thị 230.99999999999997. Đó sai, đó là điểm được thực hiện trong câu trả lời.
trusktr

6
@Karu: Imho câu trả lời không sai về mặt toán học. Chỉ có 2 câu hỏi một câu trả lời không phải là câu hỏi. Câu hỏi mà trình biên dịch của bạn trả lời là 1.39999999 * 164.99999999 và do đó, về mặt toán học đúng bằng 230.99999 .... Rõ ràng tha không phải là câu hỏi đã được hỏi ngay từ đầu ....
markus

2
@CurtisYallop vì đóng giá trị gấp đôi thành 0,49999999999999999 là 0,5 Tại sao Math.round(0.49999999999999994)trả về 1?
phuclv

53

Tôi gặp rắc rối bởi một số trong những phản ứng này. Tôi nghĩ rằng đôi và phao có một vị trí trong tính toán tài chính. Chắc chắn, khi cộng và trừ các khoản tiền không phân đoạn sẽ không mất độ chính xác khi sử dụng các lớp nguyên hoặc các lớp BigDecimal. Nhưng khi thực hiện các thao tác phức tạp hơn, bạn thường kết thúc với kết quả đi ra một vài hoặc nhiều vị trí thập phân, bất kể bạn lưu trữ các số như thế nào. Vấn đề là làm thế nào bạn trình bày kết quả.

Nếu kết quả của bạn nằm ở đường biên giữa được làm tròn lên và làm tròn xuống, và đồng xu cuối cùng đó thực sự có vấn đề, có lẽ bạn nên nói với người xem rằng câu trả lời gần ở giữa - bằng cách hiển thị nhiều chữ số thập phân hơn.

Vấn đề với nhân đôi, và hơn thế nữa với phao, là khi chúng được sử dụng để kết hợp số lượng lớn và số lượng nhỏ. Trong java,

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

kết quả trong

1.1875

16
ĐIỀU NÀY!!!! Tôi đã tìm kiếm tất cả các câu trả lời để tìm SỰ THẬT ĐÁNG TIN CẬY này !!! Trong tính toán bình thường, không ai quan tâm nếu bạn chỉ bằng một phần trăm xu, nhưng ở đây với số lượng cao dễ dàng bị mất một số đô la cho mỗi giao dịch!
Falco

22
Và bây giờ hãy tưởng tượng ai đó nhận được doanh thu hàng ngày 0,01% trên 1 triệu đô la của mình - anh ta sẽ không nhận được gì mỗi ngày - và sau một năm, anh ta đã không nhận được 1000 đô la, VẤN ĐỀ NÀY
Falco

6
Vấn đề không phải là độ chính xác mà là float không cho bạn biết rằng nó trở nên không chính xác. Một số nguyên chỉ có thể chứa tối đa 10 chữ số, một số float có thể chứa tối đa 6 chữ số mà không trở nên không chính xác (khi bạn cắt nó cho phù hợp). Nó cho phép điều này trong khi một số nguyên bị tràn và một ngôn ngữ như java sẽ cảnh báo bạn hoặc sẽ không cho phép điều đó. Khi bạn sử dụng gấp đôi, bạn có thể lên tới 16 chữ số, đủ cho nhiều trường hợp sử dụng.
sigi

39

Phao và đôi là gần đúng. Nếu bạn tạo một BigDecimal và chuyển một float vào hàm tạo, bạn sẽ thấy những gì float thực sự bằng:

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

đây có lẽ không phải là cách bạn muốn đại diện cho $ 1,01.

Vấn đề là thông số kỹ thuật của IEEE không có cách biểu diễn chính xác tất cả các phân số, một số trong số chúng kết thúc dưới dạng phân số lặp lại để bạn kết thúc với các lỗi gần đúng. Vì kế toán thích những thứ được đưa ra chính xác với đồng xu và khách hàng sẽ khó chịu nếu họ thanh toán hóa đơn và sau khi thanh toán được xử lý, họ nợ 0,01 và họ bị tính phí hoặc không thể đóng tài khoản của mình, nên sử dụng tốt hơn các loại chính xác như thập phân (bằng C #) hoặc java.math.BigDecimal trong Java.

Không phải là lỗi không thể kiểm soát được nếu bạn làm tròn: xem bài viết này của Peter Lawrey . Nó chỉ dễ dàng hơn để không phải làm tròn ở nơi đầu tiên. Hầu hết các ứng dụng xử lý tiền không đòi hỏi nhiều toán học, các hoạt động bao gồm thêm các thứ hoặc phân bổ số tiền vào các nhóm khác nhau. Giới thiệu điểm nổi và làm tròn chỉ làm phức tạp mọi thứ.


5
float, doubleBigDecimallà đại diện chính xác giá trị. Mã để chuyển đổi đối tượng là không chính xác cũng như các hoạt động khác. Các loại bản thân chúng không chính xác.
chux - Phục hồi Monica

1
@chux: đọc lại điều này, tôi nghĩ bạn có một điểm mà từ ngữ của tôi có thể được cải thiện. Tôi sẽ chỉnh sửa nó và tua lại.
Nathan Hughes

28

Tôi sẽ có nguy cơ bị hạ thấp, nhưng tôi nghĩ rằng sự không phù hợp của số dấu phẩy động để tính toán tiền tệ được đánh giá quá cao. Miễn là bạn chắc chắn rằng bạn thực hiện chính xác cent-rounding và có đủ các chữ số có nghĩa để làm việc để chống lại sự không phù hợp của biểu diễn thập phân nhị phân được giải thích bởi zneak, sẽ không có vấn đề gì.

Những người tính toán bằng tiền tệ trong Excel luôn sử dụng số float chính xác gấp đôi (không có loại tiền nào trong Excel) và tôi vẫn chưa thấy ai phàn nàn về lỗi làm tròn.

Tất nhiên, bạn phải ở trong lý trí; vd cây sào.


3
Đây thực sự là một câu trả lời khá tốt. Trong hầu hết các trường hợp, nó hoàn toàn tốt để sử dụng chúng.
Vahid Amiri

2
Cần lưu ý rằng hầu hết các ngân hàng đầu tư sử dụng gấp đôi so với hầu hết các chương trình C ++. Một số sử dụng lâu dài nhưng do đó có vấn đề riêng về quy mô theo dõi.
Peter Lawrey

20

Mặc dù đúng là loại dấu phẩy động chỉ có thể biểu thị dữ liệu thập phân gần đúng, nhưng cũng đúng là nếu một số làm tròn số với độ chính xác cần thiết trước khi trình bày chúng, người ta sẽ có được kết quả chính xác. Thông thường.

Thông thường vì loại kép có độ chính xác dưới 16 hình. Nếu bạn yêu cầu độ chính xác tốt hơn thì đó không phải là một loại phù hợp. Cũng gần đúng có thể tích lũy.

Phải nói rằng ngay cả khi bạn sử dụng số học điểm cố định, bạn vẫn phải làm tròn số, không phải vì BigInteger và BigDecimal có lỗi nếu bạn có được số thập phân định kỳ. Vì vậy, có một xấp xỉ cũng ở đây.

Ví dụ, COBOL, trong lịch sử được sử dụng để tính toán tài chính, có độ chính xác tối đa là 18 con số. Vì vậy, thường có một làm tròn ngầm.

Kết luận, theo tôi, gấp đôi không phù hợp với độ chính xác 16 chữ số của nó, có thể không đủ, không phải vì nó gần đúng.

Hãy xem xét đầu ra sau đây của chương trình tiếp theo. Nó cho thấy rằng sau khi làm tròn gấp đôi cho kết quả tương tự như BigDecimal cho đến độ chính xác 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;
    }
}

15

Kết quả của số dấu phẩy động là không chính xác, điều này làm cho chúng không phù hợp với bất kỳ phép tính tài chính nào đòi hỏi kết quả chính xác và không gần đúng. float và double được thiết kế để tính toán kỹ thuật và khoa học và nhiều lần không tạo ra kết quả chính xác, kết quả của phép tính dấu phẩy động có thể thay đổi từ JVM sang JVM. Nhìn vào ví dụ dưới đây về BigDecimal và double primitive được sử dụng để biểu thị giá trị tiền, khá rõ ràng rằng phép tính dấu phẩy động có thể không chính xác và người ta nên sử dụng BigDecimal để tính toán tài chính.

    // 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)));

Đầu ra:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9

3
Chúng ta hãy thử một cái gì đó ngoài phép cộng / trừ tầm thường và mutplicaiton số nguyên, Nếu mã tính tỷ lệ hàng tháng của khoản vay 7%, cả hai loại sẽ không thể cung cấp một giá trị chính xác và cần làm tròn đến 0,01 gần nhất. Làm tròn đến đơn vị tiền tệ thấp nhất là một phần của tính toán tiền, Sử dụng các loại thập phân tránh nhu cầu đó bằng phép cộng / trừ - nhưng không nhiều.
chux - Phục hồi Monica

@ chux-ReinstateMonica: Nếu tiền lãi được tính gộp hàng tháng, hãy tính lãi mỗi tháng bằng cách cộng số dư hàng ngày, nhân với 7 (lãi suất) và chia, làm tròn đến đồng xu gần nhất, theo số ngày trong năm. Không làm tròn bất cứ nơi nào ngoại trừ một lần mỗi tháng ở bước cuối cùng.
supercat

@supercat Nhận xét của tôi nhấn mạnh việc sử dụng FP nhị phân của đơn vị tiền tệ nhỏ nhất hoặc FP thập phân đều phát sinh các vấn đề làm tròn tương tự - như trong nhận xét của bạn với "và chia, làm tròn đến đồng xu gần nhất". Sử dụng cơ sở 2 hoặc cơ sở 10 FP không mang lại lợi thế nào trong kịch bản của bạn.
chux - Tái lập lại

@ chux-ReinstateMonica: Trong trường hợp trên, nếu toán học tính ra rằng tiền lãi phải chính xác bằng một số nửa xu, một chương trình tài chính chính xác phải làm tròn theo cách chính xác. Nếu các phép tính dấu phẩy động mang lại giá trị lãi suất là $ 1,23499941, nhưng giá trị chính xác về mặt toán học trước khi làm tròn phải là $ 1,235 và làm tròn được chỉ định là "gần nhất" ,, việc sử dụng các phép tính dấu phẩy động như vậy sẽ không gây ra kết quả được giảm 0,0059 đô la, nhưng thay vì toàn bộ 0,01 đô la, với mục đích kế toán là Just Plain Wrong.
supercat

@supercat Sử dụng doubleFP nhị phân cho phần trăm sẽ không gặp khó khăn khi tính toán đến 0,5 phần trăm vì cả FP cũng không thập phân. Nếu các phép tính dấu phẩy động mang lại giá trị lãi suất, ví dụ 123.499941, thông qua FP nhị phân hoặc FP thập phân, thì bài toán làm tròn kép là như nhau - không có lợi thế nào cả. Tiền đề của bạn dường như giả định giá trị chính xác về mặt toán học và FP thập phân là như nhau - một điều mà ngay cả FP thập phân cũng không đảm bảo. 0,5 / 7.0 * 7.0 là một vấn đề đối với FP nhị phân và khử ion. IAC, hầu hết sẽ được mô phỏng như tôi mong đợi phiên bản tiếp theo của C sẽ cung cấp FP thập phân.
chux - Phục hồi lại

11

Như đã nói trước đó "Đại diện cho tiền gấp đôi hoặc thả nổi có thể sẽ trông tốt ngay từ đầu khi phần mềm xử lý các lỗi nhỏ, nhưng khi bạn thực hiện nhiều phép cộng, phép trừ, phép nhân và phép chia trên các số không chính xác, bạn sẽ mất nhiều hơn và chính xác hơn khi các lỗi cộng lại. Điều này làm cho số float và nhân đôi không đủ để xử lý tiền, trong đó cần có độ chính xác hoàn hảo cho bội số của các sức mạnh cơ sở 10 ".

Cuối cùng, Java có một cách tiêu chuẩn để làm việc với Tiền tệ và Tiền!

JSR 354: API tiền và tiền tệ

JSR 354 cung cấp API để thể hiện, vận chuyển và thực hiện các tính toán toàn diện với Tiền và Tiền tệ. Bạn có thể tải nó từ liên kết này:

JSR 354: Tải xuống API tiền và tiền tệ

Các đặc điểm kỹ thuật bao gồm những điều sau đây:

  1. Một API để xử lý, ví dụ như số tiền và tiền tệ
  2. API để hỗ trợ triển khai thay thế cho nhau
  3. Các nhà máy để tạo các thể hiện của các lớp thực hiện
  4. Chức năng tính toán, chuyển đổi và định dạng số tiền
  5. API Java để làm việc với Tiền và Tiền tệ, được lên kế hoạch đưa vào Java 9.
  6. Tất cả các lớp và giao diện đặc tả được đặt trong gói javax.money. *.

Ví dụ mẫu về JSR 354: API tiền và tiền tệ:

Một ví dụ về việc tạo Tiền tệ và in nó lên bàn điều khiển trông như thế này ::

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));

Khi sử dụng API triển khai tham chiếu, mã cần thiết đơn giản hơn nhiều:

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

API cũng hỗ trợ tính toán với Mon MoneyAmounts:

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

Đơn vị tiền tệ và tiền tệ

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

Mon MoneyAmount có nhiều phương thức khác nhau cho phép truy cập loại tiền được chỉ định, số lượng, độ chính xác của nó và hơn thế nữa:

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;

Tiền tệ có thể được làm tròn bằng cách sử dụng toán tử làm tròn:

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

Khi làm việc với các bộ sưu tập của Mon MoneyAmounts, một số phương pháp tiện ích tuyệt vời để lọc, sắp xếp và nhóm có sẵn.

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"));

Các hoạt động kiếm tiền tùy chỉnh

// 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

Tài nguyên:

Xử lý tiền và tiền tệ trong Java với JSR 354

Nhìn vào API tiền và tiền tệ Java 9 (JSR 354)

Xem thêm: JSR 354 - Tiền tệ và tiền


5

Nếu tính toán của bạn bao gồm các bước khác nhau, số học chính xác tùy ý sẽ không bao gồm bạn 100%.

Cách đáng tin cậy duy nhất để sử dụng biểu diễn kết quả hoàn hảo (Sử dụng loại dữ liệu Phân số tùy chỉnh sẽ phân chia các hoạt động cho bước cuối cùng) và chỉ chuyển đổi sang ký hiệu thập phân ở bước cuối cùng.

Độ chính xác tùy ý sẽ không giúp ích vì luôn có thể có các số có rất nhiều số thập phân hoặc một số kết quả như 0.6666666 ... Không có biểu diễn tùy ý sẽ bao gồm ví dụ cuối cùng. Vì vậy, bạn sẽ có lỗi nhỏ trong từng bước.

Những lỗi này sẽ bổ sung, cuối cùng có thể trở nên không dễ bỏ qua nữa. Điều này được gọi là Tuyên truyền lỗi .


4

Hầu hết các câu trả lời đã nhấn mạnh lý do tại sao người ta không nên sử dụng gấp đôi để tính toán tiền và tiền tệ. Và tôi hoàn toàn đồng ý với họ.

Điều đó không có nghĩa là đôi khi không bao giờ có thể được sử dụng cho mục đích đó.

Tôi đã làm việc trên một số dự án với yêu cầu gc rất thấp và việc có các đối tượng BigDecimal là một đóng góp lớn cho chi phí đó.

Đó là sự thiếu hiểu biết về đại diện kép và thiếu kinh nghiệm trong việc xử lý tính chính xác và chính xác mang lại gợi ý khôn ngoan này.

Bạn có thể làm cho nó hoạt động nếu bạn có thể xử lý các yêu cầu chính xác và chính xác của dự án của bạn, điều này phải được thực hiện dựa trên phạm vi của các giá trị kép là một xử lý.

Bạn có thể tham khảo phương pháp FuzzyCompare của ổi để có thêm ý tưởng. Dung sai tham số là chìa khóa. Chúng tôi đã giải quyết vấn đề này cho một ứng dụng giao dịch chứng khoán và chúng tôi đã thực hiện một nghiên cứu toàn diện về việc sử dụng dung sai nào cho các giá trị số khác nhau trong các phạm vi khác nhau.

Ngoài ra, có thể có những tình huống khi bạn muốn sử dụng Trình bao bọc kép làm khóa bản đồ với bản đồ băm là việc triển khai. Điều này rất rủi ro vì Double.equals và mã băm cho các giá trị ví dụ "0,5" & "0,6 - 0,1" sẽ gây ra một mớ hỗn độn lớn.


2

Nhiều câu trả lời được đăng cho câu hỏi này thảo luận về IEEE và các tiêu chuẩn xung quanh số học dấu phẩy động.

Xuất thân từ một nền tảng khoa học phi máy tính (vật lý và kỹ thuật), tôi có xu hướng nhìn vấn đề từ một khía cạnh khác. Đối với tôi, lý do tại sao tôi sẽ không sử dụng gấp đôi hoặc thả nổi trong phép tính toán là vì tôi sẽ mất quá nhiều thông tin.

Các lựa chọn thay thế là gì? Có rất nhiều (và nhiều trong số đó tôi không biết!).

BigDecimal trong Java có nguồn gốc từ ngôn ngữ Java. Apfloat là một thư viện chính xác tùy ý khác cho Java.

Kiểu dữ liệu thập phân trong C # là .NET thay thế cho 28 số liệu quan trọng.

SciPy (Python khoa học) có thể cũng có thể xử lý các tính toán tài chính (tôi chưa thử, nhưng tôi nghi ngờ như vậy).

GNU Multi Precision Library (GMP) và GNU MFPR Library là hai tài nguyên nguồn mở và miễn phí cho C và C ++.

Ngoài ra còn có các thư viện chính xác về số cho JavaScript (!) Và tôi nghĩ PHP có thể xử lý các tính toán tài chính.

Ngoài ra còn có các giải pháp độc quyền (đặc biệt, tôi nghĩ, đối với Fortran) và các giải pháp nguồn mở cũng như cho nhiều ngôn ngữ máy tính.

Tôi không phải là một nhà khoa học máy tính bằng cách đào tạo. Tuy nhiên, tôi có xu hướng nghiêng về BigDecimal trong Java hoặc thập phân trong C #. Tôi đã không thử các giải pháp khác mà tôi đã liệt kê, nhưng chúng có lẽ cũng rất tốt.

Đối với tôi, tôi thích BigDecimal vì các phương thức mà nó hỗ trợ. Số thập phân của C # rất đẹp, nhưng tôi chưa có cơ hội làm việc với nó nhiều như tôi muốn. Tôi thực hiện các tính toán khoa học mà tôi quan tâm trong thời gian rảnh rỗi và BigDecimal dường như hoạt động rất tốt vì tôi có thể đặt độ chính xác của các số dấu phẩy động của mình. Bất lợi cho BigDecimal? Đôi khi nó có thể bị chậm, đặc biệt nếu bạn đang sử dụng phương pháp chia.

Về tốc độ, bạn có thể xem xét các thư viện miễn phí và độc quyền trong C, C ++ và Fortran.


1
Về SciPy / Numpy, độ chính xác cố định (tức là số thập phân của Python) không được hỗ trợ ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). Một số chức năng sẽ không hoạt động đúng với Decimal (ví dụ isnan). Pandas dựa trên Numpy và được khởi xướng tại AQR, một quỹ đầu cơ định lượng lớn. Vì vậy, bạn có câu trả lời của bạn về tính toán tài chính (không phải kế toán tạp hóa).
comte

2

Để thêm vào các câu trả lời trước đó, cũng có tùy chọn triển khai Joda-Money trong Java, bên cạnh BigDecimal, khi xử lý vấn đề được giải quyết trong câu hỏi. Tên modul Java là org.joda.money.

Nó yêu cầu Java SE 8 trở lên và không có phụ thuộc.

Nói chính xác hơn, có sự phụ thuộc thời gian biên dịch nhưng nó không bắt buộc.

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

Ví dụ về sử dụng tiền Joda:

  // 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();

Tài liệu: http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html

Ví dụ triển khai: https://www.programcalet.com/java-api-examples/?api=org.joda.money.Money


0

Một số ví dụ ... điều này hoạt động (thực tế không hoạt động như mong đợi), trên hầu hết mọi ngôn ngữ lập trình ... Tôi đã thử với Delphi, VBScript, Visual Basic, JavaScript và bây giờ với 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!!!");
    }

ĐẦU RA:

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!!!


3
Vấn đề không phải là lỗi làm tròn xảy ra, mà là bạn không giải quyết nó. Làm tròn kết quả đến hai chữ số thập phân (nếu bạn muốn xu) và bạn đã hoàn thành.
maaartinus

0

Float là dạng nhị phân của Decimal với thiết kế khác nhau; Họ là hai việc khác nhau. Có rất ít lỗi giữa hai loại khi chuyển đổi sang nhau. Ngoài ra, float được thiết kế để đại diện cho số lượng lớn các giá trị vô hạn cho khoa học. Điều đó có nghĩa là nó được thiết kế để mất độ chính xác đến số cực nhỏ và cực lớn với số byte cố định đó. Số thập phân không thể biểu thị số lượng giá trị vô hạn, nó chỉ giới hạn số chữ số thập phân đó. Vì vậy, Float và Decimal là dành cho mục đích khác nhau.

Có một số cách để quản lý lỗi cho giá trị tiền tệ:

  1. Sử dụng số nguyên dài và tính bằng xu thay thế.

  2. Sử dụng độ chính xác kép, chỉ giữ các chữ số có nghĩa của bạn đến 15 để số thập phân có thể được mô phỏng chính xác. Làm tròn trước khi trình bày các giá trị; Vòng thường khi làm tính toán.

  3. Sử dụng thư viện thập phân như Java BigDecimal để bạn không cần sử dụng gấp đôi để mô phỏng thập phân.

Thật thú vị khi biết rằng hầu hết các thương hiệu máy tính khoa học cầm tay hoạt động trên số thập phân thay vì nổi. Vì vậy, không ai khiếu nại lỗi chuyển đổi float.

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.