Tôi luôn được dặn là không bao giờ đại diện cho tiền double
hoặc float
cá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ì.
Tôi luôn được dặn là không bao giờ đại diện cho tiền double
hoặc float
cá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ì.
Câu trả lời:
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 double
hoặc float
có 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ó BigDecimal
lớp và C # có decimal
loại.
1.0 / 10 * 10
có thể không giống như 1.0.
Từ Bloch, J., Java hiệu quả, tái bản lần 2, Mục 48:
Các loại
float
vàdouble
đặ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ộtfloat
hoặcdouble
chí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ặclong
để tính toán tiền tệ.
Mặc dù BigDecimal
có một số cảnh báo (xin vui lòng xem câu trả lời hiện được chấp nhận).
long a = 104
và đếm bằng xu thay vì đô la.
BigDecimal
.
Đâ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.
Math.round(0.49999999999999994)
trả về 1?
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
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ứ.
float
, double
Và BigDecimal
là đạ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.
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.
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;
}
}
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
double
FP 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.
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:
- Một API để xử lý, ví dụ như số tiền và tiền tệ
- API để hỗ trợ triển khai thay thế cho nhau
- Các nhà máy để tạo các thể hiện của các lớp thực hiện
- Chức năng tính toán, chuyển đổi và định dạng số tiền
- 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.
- 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
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 .
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.
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.
Để 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
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!!!
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ệ:
Sử dụng số nguyên dài và tính bằng xu thay thế.
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.
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.