ฉันได้รับการบอกเสมอว่าไม่เคยเป็นตัวแทนของเงินdouble
หรือfloat
ประเภทและคราวนี้ฉันตั้งคำถามกับคุณ: ทำไม?
ฉันแน่ใจว่ามีเหตุผลที่ดีมากฉันไม่ทราบว่ามันคืออะไร
ฉันได้รับการบอกเสมอว่าไม่เคยเป็นตัวแทนของเงินdouble
หรือfloat
ประเภทและคราวนี้ฉันตั้งคำถามกับคุณ: ทำไม?
ฉันแน่ใจว่ามีเหตุผลที่ดีมากฉันไม่ทราบว่ามันคืออะไร
คำตอบ:
เนื่องจากการลอยและการทวีคูณไม่สามารถแทนค่าฐานทวีคูณที่เราใช้เพื่อเงินอย่างถูกต้อง ปัญหานี้ไม่ได้มีไว้สำหรับ Java เท่านั้น แต่สำหรับภาษาการเขียนโปรแกรมใด ๆ ที่ใช้ชนิดทศนิยม 2 ฐาน
ในฐาน 10 คุณสามารถเขียน 10.25 เป็น 1025 * 10 -2 (จำนวนเต็มคูณด้วยกำลัง 10) ตัวเลขทศนิยมของ IEEE-754นั้นแตกต่างกัน แต่วิธีที่ง่ายมากในการคิดเกี่ยวกับมันคือการคูณด้วยกำลังสองแทน ตัวอย่างเช่นคุณอาจดูที่ 164 * 2 -4 (จำนวนเต็มคูณด้วยกำลังสอง) ซึ่งเท่ากับ 10.25 นั่นไม่ใช่ตัวเลขที่แสดงในหน่วยความจำ แต่ความหมายทางคณิตศาสตร์เหมือนกัน
แม้แต่ในฐาน 10 สัญลักษณ์นี้ไม่สามารถแสดงเศษส่วนที่ง่ายที่สุดได้อย่างแม่นยำ ตัวอย่างเช่นคุณไม่สามารถแทน 1/3: การแทนทศนิยมกำลังทำซ้ำ (0.3333 ... ) ดังนั้นจึงไม่มีจำนวนเต็ม จำกัด ที่คุณสามารถคูณด้วยกำลัง 10 เพื่อให้ได้ 1/3 คุณสามารถชำระในลำดับที่ยาวเป็น 3 และเลขชี้กำลังขนาดเล็กเช่น 333333333 * 10 -10แต่มันไม่ถูกต้อง: ถ้าคุณคูณด้วย 3 คุณจะไม่ได้ 1
อย่างไรก็ตามเพื่อจุดประสงค์ในการนับเงินอย่างน้อยสำหรับประเทศที่มีมูลค่าเงินอยู่ในลำดับความสำคัญของเงินดอลลาร์สหรัฐโดยปกติแล้วสิ่งที่คุณต้องมีก็คือสามารถเก็บทวีคูณได้ 10 -2ได้ดังนั้นมันจึงไม่สำคัญ 1/3 นั้นไม่สามารถแสดงได้
ปัญหาเกี่ยวกับการลอยตัวและเป็นสองเท่าคือส่วนใหญ่ของเงินเหมือนตัวเลขไม่ได้มีการแสดงที่แน่นอนเป็นจำนวนเต็มคูณด้วยพลังของ 2 ในความเป็นจริงเพียงทวีคูณของ 0.01 ระหว่าง 0 และ 1 (ซึ่งมีความสำคัญเมื่อจัดการ ด้วยเงินเพราะเป็นจำนวนเต็มเซ็นต์) ซึ่งสามารถแทนได้อย่างแน่นอนว่าเป็นเลขทศนิยมเลขฐานสองของ IEEE-754 คือ 0, 0.25, 0.5, 0.75 และ 1 ส่วนอื่น ๆ ทั้งหมดจะถูกปิดด้วยจำนวนเล็กน้อย เช่นเดียวกับตัวอย่าง 0.333333 หากคุณใช้ค่าทศนิยมสำหรับ 0.1 และคูณด้วย 10 คุณจะไม่ได้ 1
การแสดงเงินในฐานะdouble
หรือfloat
อาจจะดูดีในตอนแรกเมื่อซอฟต์แวร์ลบความผิดพลาดเล็ก ๆ น้อย ๆ แต่เมื่อคุณทำการบวกการลบการคูณและการหารในจำนวนที่ไม่แน่นอนข้อผิดพลาดจะรวมกันและคุณจะได้ค่าที่เห็นได้ชัด ไม่ถูกต้อง สิ่งนี้ทำให้การลอยตัวและไม่เพียงพอเป็นสองเท่าสำหรับการรับมือกับเงินซึ่งต้องการความแม่นยำที่สมบูรณ์แบบสำหรับการทวีคูณของพลัง 10 ฐาน
วิธีแก้ปัญหาที่ใช้งานได้ในเกือบทุกภาษาคือการใช้จำนวนเต็มแทนและนับเซนต์ ตัวอย่างเช่น 1,025 จะเป็น $ 10.25 หลายภาษายังมีประเภทในตัวเพื่อจัดการกับเงิน ในหมู่คนอื่น ๆ Java มีBigDecimal
ชั้นเรียนและ C # มีdecimal
ประเภท
1.0 / 10 * 10
อาจไม่เท่ากับ 1.0
จาก Bloch, J. , Java ที่มีประสิทธิภาพ, 2nd ed, รายการ 48:
float
และdouble
ประเภทโดยเฉพาะอย่างยิ่งไม่เหมาะสำหรับการคำนวณทางการเงินเพราะมันเป็นไปไม่ได้ที่จะเป็นตัวแทน 0.1 (หรืออำนาจเชิงลบอื่น ๆ ของสิบ) เป็นfloat
หรือdouble
ว่าตัวอย่างเช่นสมมติว่าคุณมี $ 1.03 และคุณใช้จ่าย 42c คุณเหลือเงินเท่าไหร่?
System.out.println(1.03 - .42);
0.6100000000000001
พิมพ์ออกวิธีการที่เหมาะสมในการแก้ปัญหานี้คือการใช้งาน
BigDecimal
,int
หรือlong
สำหรับการคำนวณทางการเงิน
แม้ว่าจะBigDecimal
มีคำเตือนอยู่บ้าง (โปรดดูคำตอบที่ยอมรับในปัจจุบัน)
long a = 104
และนับเป็นเซ็นต์แทนดอลลาร์
BigDecimal
ว่า
นี่ไม่ใช่เรื่องของความถูกต้องและไม่เป็นเรื่องของความแม่นยำ มันเป็นเรื่องของการประชุมความคาดหวังของมนุษย์ที่ใช้ฐาน 10 สำหรับการคำนวณแทนฐาน 2 ตัวอย่างเช่นการใช้คู่สำหรับการคำนวณทางการเงินไม่ได้ผลิตคำตอบที่ "ผิด" ในแง่คณิตศาสตร์ แต่สามารถสร้างคำตอบที่เป็น ไม่ใช่สิ่งที่คาดหวังในแง่การเงิน
แม้ว่าคุณจะปัดเศษผลลัพธ์ของคุณในนาทีสุดท้ายก่อนที่จะส่งออกคุณยังสามารถได้รับผลลัพธ์เป็นครั้งคราวโดยใช้คู่ที่ไม่ตรงกับความคาดหวัง
ใช้เครื่องคิดเลขหรือคำนวณผลลัพธ์ด้วยมือ 1.40 * 165 = 231 อย่างไรก็ตามภายในใช้คู่ผสมในสภาพแวดล้อมของคอมไพเลอร์ / ระบบปฏิบัติการของฉันมันถูกเก็บไว้เป็นเลขฐานสองใกล้กับ 230.99999 ... ดังนั้นถ้าคุณตัดทอนตัวเลขคุณจะได้รับ 230 แทน 231 คุณอาจมีเหตุผลว่าการปัดเศษแทนการตัดทอน ให้ผลลัพธ์ที่ต้องการเป็น 231 นั่นเป็นความจริง แต่การปัดเศษจะต้องมีการปัดเศษเสมอ ไม่ว่าคุณจะใช้เทคนิคการปัดเศษยังคงมีเงื่อนไขขอบเขตเช่นนี้ที่จะปัดเศษเมื่อคุณคาดว่าจะปัดเศษขึ้น พวกเขาหายากมากพอที่จะไม่พบบ่อยครั้งผ่านการทดสอบหรือสังเกตการณ์ คุณอาจต้องเขียนโค้ดเพื่อค้นหาตัวอย่างที่แสดงผลลัพธ์ที่ไม่ทำงานตามที่คาดไว้
สมมติว่าคุณต้องการปัดเศษบางสิ่งให้เป็นเงินที่ใกล้ที่สุด ดังนั้นคุณจะได้ผลลัพธ์สุดท้ายคูณด้วย 100 บวก 0.5 ตัดทอนแล้วหารผลด้วย 100 เพื่อกลับไปที่เพนนี หากหมายเลขภายในที่คุณเก็บไว้คือ 3.46499999 .... แทน 3.465 คุณจะได้รับ 3.46 แทน 3.47 เมื่อคุณปัดจำนวนเป็นเงินที่ใกล้ที่สุด แต่การคำนวณฐาน 10 ของคุณอาจแสดงให้เห็นว่าคำตอบนั้นควรเป็น 3.465 ซึ่งแน่นอนว่าควรจะสูงถึง 3.47 ไม่ใช่ลงที่ 3.46 สิ่งเหล่านี้เกิดขึ้นเป็นครั้งคราวในชีวิตจริงเมื่อคุณใช้เป็นสองเท่าในการคำนวณทางการเงิน มันเป็นของหายากจึงมักจะไม่มีใครสังเกตเห็นว่าเป็นปัญหา แต่มันเกิดขึ้น
หากคุณใช้ฐาน 10 สำหรับการคำนวณภายในของคุณแทนที่จะเป็นสองเท่าคำตอบคือสิ่งที่มนุษย์คาดหวังเสมอโดยไม่มีข้อบกพร่องอื่น ๆ ในรหัสของคุณ
Math.round(0.49999999999999994)
ส่งคืน 1
ฉันมีปัญหากับคำตอบเหล่านี้ ฉันคิดว่าคู่และลอยมีสถานที่ในการคำนวณทางการเงิน แน่นอนว่าเมื่อเพิ่มและลบจำนวนเงินที่ไม่เป็นเศษส่วนจะไม่มีการสูญเสียความแม่นยำเมื่อใช้คลาสจำนวนเต็มหรือคลาส BigDecimal แต่เมื่อทำการดำเนินการที่ซับซ้อนมากขึ้นคุณมักจะจบลงด้วยผลลัพธ์ที่ออกมาหลายตำแหน่งหรือทศนิยมไม่ว่าคุณจะเก็บตัวเลขอย่างไร ปัญหาคือวิธีที่คุณแสดงผลลัพธ์
หากผลลัพธ์ของคุณอยู่บนเส้นขอบระหว่างการปัดเศษขึ้นและปัดเศษลงและเพนนีสุดท้ายมีความสำคัญจริง ๆ คุณควรจะบอกผู้ชมว่าคำตอบเกือบตรงกลาง - โดยแสดงตำแหน่งทศนิยมเพิ่มเติม
ปัญหาที่เกิดขึ้นกับคู่และอื่น ๆ ด้วยลอยคือเมื่อพวกเขาถูกนำมาใช้เพื่อรวมจำนวนมากและตัวเลขขนาดเล็ก ในจาวา
System.out.println(1000000.0f + 1.2f - 1000000.0f);
ผลลัพธ์ใน
1.1875
ลอยและคู่เป็นประมาณ หากคุณสร้าง BigDecimal และส่งทุ่นลอยไปที่ Constructor คุณจะเห็นว่าการลอยนั้นเท่ากับจริง:
groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375
นี่อาจไม่ใช่วิธีที่คุณต้องการเป็นตัวแทน $ 1.01
ปัญหาคือสเป็คของ IEEE ไม่มีวิธีที่จะแสดงเศษส่วนทั้งหมดได้อย่างแน่นอนซึ่งบางส่วนก็กลายเป็นเศษส่วนซ้ำดังนั้นคุณจึงมีข้อผิดพลาดโดยประมาณ เนื่องจากนักบัญชีชอบสิ่งต่าง ๆ ออกมาตรงกับเงินและลูกค้าจะรำคาญถ้าพวกเขาจ่ายบิลและหลังจากการชำระเงินถูกประมวลผลพวกเขาเป็นหนี้. 01 และพวกเขาถูกเรียกเก็บค่าธรรมเนียมหรือไม่สามารถปิดบัญชีได้ดีกว่าที่จะใช้ ประเภทที่แน่นอนเช่นทศนิยม (ใน C #) หรือ java.math.BigDecimal ใน Java
มันไม่ได้ว่าข้อผิดพลาดที่ไม่สามารถควบคุมได้ถ้าคุณรอบ: ดูบทความนี้โดยปีเตอร์ Lawrey มันง่ายกว่าที่จะไม่ต้องปัดเศษในตอนแรก แอปพลิเคชั่นส่วนใหญ่ที่จัดการเงินไม่ต้องใช้คณิตศาสตร์จำนวนมากการดำเนินการประกอบด้วยการเพิ่มสิ่งต่าง ๆ หรือจัดสรรจำนวนเงินให้กับถังที่แตกต่างกัน การแนะนำจุดลอยตัวและการปัดเศษเป็นเรื่องยุ่งยาก
float
, double
และBigDecimal
มีความหมายที่แน่นอนค่า การแปลงรหัสเป็นวัตถุไม่แน่นอนเช่นเดียวกับการดำเนินการอื่น ๆ ประเภทตัวเองไม่แน่นอน
ฉันจะเสี่ยงต่อการถูก downvote แต่ฉันคิดว่าความไม่เหมาะสมของตัวเลขจุดลอยตัวสำหรับการคำนวณสกุลเงินนั้นเกินจริง ตราบใดที่คุณตรวจสอบให้แน่ใจว่าคุณทำการปัดเศษเซ็นอย่างถูกต้องและมีตัวเลขที่สำคัญพอที่จะทำงานด้วยเพื่อตอบโต้การไม่ตรงกันการแทนค่าฐานสองทศนิยมที่อธิบายโดย zneak จะไม่มีปัญหา
ผู้ที่คำนวณด้วยสกุลเงินใน Excel มักใช้ความแม่นยำสองเท่าแบบลอยตัว (ไม่มีประเภทสกุลเงินใน Excel) และฉันยังไม่เห็นใครบ่นเกี่ยวกับข้อผิดพลาดในการปัดเศษ
แน่นอนคุณต้องอยู่ในเหตุผล; เช่นเว็บช็อปแบบง่าย ๆ อาจจะไม่ประสบปัญหาใด ๆ กับการลอยความแม่นยำสองเท่า แต่ถ้าคุณทำเช่นการบัญชีหรืออะไรก็ตามที่ต้องเพิ่มตัวเลขจำนวนมาก (ไม่ จำกัด ) คุณไม่ต้องการสัมผัสตัวเลขทศนิยมด้วยสิบฟุต เสา.
ในขณะที่มันเป็นความจริงที่ว่าประเภทจุดลอยตัวสามารถแสดงข้อมูลทศนิยมโดยประมาณเท่านั้น แต่ยังเป็นความจริงที่ว่าหากหนึ่งรอบตัวเลขเป็นความแม่นยำที่จำเป็นก่อนที่จะนำเสนอพวกเขาหนึ่งได้รับผลลัพธ์ที่ถูกต้อง มักจะ
มักจะเป็นเพราะประเภทสองมีความแม่นยำน้อยกว่า 16 ตัวเลข หากคุณต้องการความแม่นยำที่ดีกว่านี้ไม่ใช่ประเภทที่เหมาะสม การประมาณยังสามารถสะสม
ต้องบอกว่าแม้ว่าคุณจะใช้เลขคณิตจุดคงที่คุณยังต้องปัดเศษตัวเลขไม่ใช่เพราะข้อเท็จจริงที่ว่า BigInteger และ BigDecimal ให้ข้อผิดพลาดหากคุณได้รับตัวเลขทศนิยมเป็นระยะ มีการประมาณยังอยู่ตรงนี้ด้วย
ตัวอย่างเช่น COBOL ซึ่งใช้ในอดีตสำหรับการคำนวณทางการเงินมีความแม่นยำสูงสุด 18 ตัวเลข ดังนั้นมักจะมีการปัดเศษโดยนัย
โดยสรุปแล้วในความคิดของฉันคู่ที่ไม่เหมาะสมส่วนใหญ่สำหรับความแม่นยำ 16 หลักซึ่งอาจไม่เพียงพอไม่ใช่เพราะมันประมาณ
พิจารณาผลลัพธ์ต่อไปนี้ของโปรแกรมที่ตามมา มันแสดงให้เห็นว่าหลังจากการปัดเศษคู่ให้ผลลัพธ์เหมือนกับ BigDecimal มากถึง 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;
}
}
ผลลัพธ์ของเลขทศนิยมไม่ถูกต้องซึ่งทำให้ไม่เหมาะสมสำหรับการคำนวณทางการเงินใด ๆ ที่ต้องการผลลัพธ์ที่แน่นอนและไม่ประมาณ float และ double ถูกออกแบบมาสำหรับการคำนวณทางวิศวกรรมและวิทยาศาสตร์และหลายครั้งไม่ได้ผลลัพธ์ที่แน่นอนนอกจากนี้การคำนวณจุดลอยตัวอาจแตกต่างจาก JVM ถึง JVM ดูตัวอย่างด้านล่างของ BigDecimal และ double primitive ซึ่งใช้แทนค่าเงินค่อนข้างชัดเจนว่าการคำนวณจุดลอยตัวอาจไม่แม่นยำและควรใช้ BigDecimal สำหรับการคำนวณทางการเงิน
// 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)));
เอาท์พุท:
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 ไปยัง cent จะไม่มีปัญหาในการคำนวณถึง 0.5 cent เนื่องจาก FP แบบทศนิยมจะไม่มี หากการคำนวณจุดลอยตัวให้ผลตอบแทนที่น่าสนใจเช่น 123.499941 ¢ไม่ว่าจะผ่านทาง FP แบบไบนารีหรือ FP แบบทศนิยมปัญหาการปัดเศษสองครั้งจะเหมือนกัน - ไม่ได้เปรียบในทางใดทางหนึ่ง หลักฐานของคุณดูเหมือนว่าจะถือว่าค่าที่แม่นยำทางคณิตศาสตร์และ FP แบบทศนิยมนั้นเหมือนกัน - บางอย่างที่ FP แบบทศนิยมไม่รับประกัน 0.5 / 7.0 * 7.0 เป็นปัญหาสำหรับสำหรับไบนารีและ deicmal FP IAC ส่วนใหญ่จะเป็น moot ตามที่ฉันคาดหวังรุ่นถัดไปของ C เพื่อให้ FP ทศนิยม
ดังที่ได้กล่าวไว้ก่อนหน้านี้ว่า "การแสดงเงินเป็นสองเท่าหรือลอยนั้นอาจดูดีในตอนแรกเนื่องจากซอฟต์แวร์ปัดเศษข้อผิดพลาดเล็ก ๆ น้อย ๆ แต่เมื่อคุณทำการเพิ่มเติมการลบการคูณและการหารบนตัวเลขที่ไม่แน่นอนมากขึ้น เมื่อมีข้อผิดพลาดเกิดขึ้นทำให้สิ่งนี้ลอยตัวและไม่เพียงพอเป็นสองเท่าสำหรับการจัดการกับเงิน
ในที่สุด Java ก็มีวิธีมาตรฐานในการทำงานกับสกุลเงินและเงิน!
JSR 354: API เงินและสกุลเงิน
JSR 354 จัดทำ API สำหรับการเป็นตัวแทนการขนส่งและการคำนวณที่ครอบคลุมด้วยเงินและสกุลเงิน คุณสามารถดาวน์โหลดได้จากลิงค์นี้:
JSR 354: การดาวน์โหลด API เงินและสกุลเงิน
ข้อมูลจำเพาะประกอบด้วยสิ่งต่าง ๆ ดังต่อไปนี้:
- API สำหรับจัดการเช่นจำนวนเงินและสกุลเงิน
- API เพื่อสนับสนุนการใช้งานที่สามารถเปลี่ยนแทนกันได้
- โรงงานสำหรับสร้างอินสแตนซ์ของคลาสการใช้งาน
- ฟังก์ชันการคำนวณการแปลงและการจัดรูปแบบของจำนวนเงิน
- Java API สำหรับการทำงานกับ Money และ Currencies ซึ่งมีการวางแผนที่จะรวมอยู่ใน Java 9
- คลาสข้อมูลจำเพาะและอินเทอร์เฟซทั้งหมดอยู่ในแพ็คเกจ javax.money. *
ตัวอย่างตัวอย่างของ JSR 354: API เงินและสกุลเงิน:
ตัวอย่างการสร้าง MonetaryAmount และพิมพ์ไปยังคอนโซลมีลักษณะดังนี้ ::
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));
เมื่อใช้ API การใช้งานอ้างอิงรหัสที่จำเป็นนั้นง่ายกว่ามาก:
MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));
API ยังสนับสนุนการคำนวณด้วย MonetaryAmounts:
MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));
หน่วยสกุลเงินและจำนวนเงิน
// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);
MonetaryAmount มีวิธีการต่าง ๆ ที่อนุญาตให้เข้าถึงสกุลเงินที่กำหนดจำนวนตัวเลขความแม่นยำและอื่น ๆ :
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;
MonetaryAmounts สามารถปัดเศษโดยใช้ตัวดำเนินการปัดเศษ:
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
เมื่อทำงานกับคอลเลกชันของ MonetaryAmounts วิธีการอรรถประโยชน์ที่ดีสำหรับการกรองการเรียงลำดับและการจัดกลุ่มจะพร้อมใช้งาน
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"));
การดำเนินการ MonetaryAmount ที่กำหนดเอง
// 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
แหล่งข้อมูล:
การจัดการเงินและสกุลเงินใน Java ด้วย JSR 354
การค้นหา Java 9 Money และ Currency API (JSR 354)
ดูเพิ่มเติมที่: JSR 354 - สกุลเงินและเงิน
หากการคำนวณของคุณเกี่ยวข้องกับขั้นตอนต่าง ๆ เลขคณิตความแม่นยำโดยพลการจะไม่ครอบคลุม 100%
วิธีที่เชื่อถือได้เพียงวิธีเดียวในการใช้การแสดงผลลัพธ์ที่สมบูรณ์แบบ (ใช้ชนิดข้อมูลเศษส่วนแบบกำหนดเองที่จะทำการแบ่งส่วนการดำเนินการเป็นขั้นตอนสุดท้าย) และแปลงเป็นสัญลักษณ์ทศนิยมในขั้นตอนสุดท้ายเท่านั้น
ความแม่นยำโดยพลการจะไม่ช่วยได้เพราะอาจมีตัวเลขที่มีตำแหน่งทศนิยมจำนวนมากเสมอหรือผลลัพธ์บางอย่างเช่น0.6666666
... ไม่มีการแทนค่าโดยพลการจะครอบคลุมตัวอย่างสุดท้าย ดังนั้นคุณจะมีข้อผิดพลาดเล็กน้อยในแต่ละขั้นตอน
ข้อผิดพลาดเหล่านี้จะเพิ่มขึ้นในที่สุดอาจกลายเป็นเรื่องง่ายที่จะไม่สนใจอีกต่อไป นี้เรียกว่าข้อผิดพลาดในการขยายพันธุ์
คำตอบส่วนใหญ่ให้ความสำคัญกับเหตุผลที่ไม่ควรใช้การคำนวณเงินและสกุลเงินเป็นสองเท่า และฉันเห็นด้วยกับพวกเขาทั้งหมด
ไม่ได้หมายความว่าจะไม่สามารถใช้คู่ผสมเพื่อจุดประสงค์นั้นได้
ฉันทำงานหลายโครงการที่มีข้อกำหนด gc ต่ำมากและการมีวัตถุ BigDecimal เป็นผู้มีส่วนร่วมอย่างมากในค่าใช้จ่ายนั้น
มันขาดความเข้าใจเกี่ยวกับการแสดงซ้ำและขาดประสบการณ์ในการจัดการความถูกต้องและความแม่นยำที่นำมาซึ่งข้อเสนอแนะที่ชาญฉลาดนี้
คุณสามารถทำให้มันทำงานได้ถ้าคุณสามารถจัดการกับความต้องการความแม่นยำและความแม่นยำของโครงการของคุณซึ่งจะต้องทำตามช่วงของค่าสองเท่าที่เกี่ยวข้องกับ
คุณสามารถอ้างถึงวิธี FuzzyCompare ของฝรั่งเพื่อให้ได้แนวคิดเพิ่มเติม ความอดทนพารามิเตอร์เป็นกุญแจสำคัญ เราจัดการกับปัญหานี้สำหรับแอปพลิเคชันการซื้อขายหลักทรัพย์และเราได้ทำการวิจัยอย่างละเอียดเกี่ยวกับความอดทนที่จะใช้สำหรับค่าตัวเลขที่แตกต่างกันในช่วงที่แตกต่างกัน
นอกจากนี้อาจมีสถานการณ์เมื่อคุณถูกล่อลวงให้ใช้ Double wrappers เป็นคีย์แผนที่พร้อมกับแผนที่แฮชที่กำลังใช้งานอยู่ มันมีความเสี่ยงมากเพราะ Double.equals และรหัสแฮชสำหรับค่าตัวอย่าง "0.5" & "0.6 - 0.1" จะทำให้เกิดความยุ่งเหยิง
คำตอบจำนวนมากที่โพสต์ในคำถามนี้จะกล่าวถึง IEEE และมาตรฐานที่อยู่รอบ ๆ เลขทศนิยม
มาจากพื้นหลังวิทยาศาสตร์ที่ไม่ใช่คอมพิวเตอร์ (ฟิสิกส์และวิศวกรรม) ฉันมักจะมองปัญหาจากมุมมองที่แตกต่างกัน สำหรับฉันเหตุผลที่ฉันจะไม่ใช้ double หรือ float ในการคำนวณทางคณิตศาสตร์คือฉันจะสูญเสียข้อมูลมากเกินไป
ทางเลือกคืออะไร? มีมากมาย (และอีกมากมายที่ฉันไม่ทราบ!)
BigDecimal ใน Java เป็นภาษาพื้นเมืองของ Java Apfloat เป็นอีกหนึ่งไลบรารีที่มีความแม่นยำตามอำเภอใจสำหรับ Java
ประเภทข้อมูลเลขฐานสิบใน C # เป็นทางเลือก. NET ของ Microsoft สำหรับตัวเลข 28 หลัก
SciPy (Scientific Python) อาจจะสามารถจัดการกับการคำนวณทางการเงินได้ (ฉันไม่ได้ลอง แต่ก็สงสัยเช่นกัน)
GNU Multiple Precision Library (GMP) และ GNU MFPR Library เป็นแหล่งข้อมูลฟรีและโอเพ่นซอร์สสองแหล่งสำหรับ C และ C ++
นอกจากนี้ยังมีไลบรารีความแม่นยำเชิงตัวเลขสำหรับ JavaScript (!) และฉันคิดว่า PHP ซึ่งสามารถจัดการการคำนวณทางการเงินได้
นอกจากนี้ยังมีกรรมสิทธิ์ (โดยเฉพาะอย่างยิ่งฉันคิดว่าสำหรับ Fortran) และโซลูชั่นโอเพ่นซอร์สเช่นเดียวกับภาษาคอมพิวเตอร์จำนวนมาก
ฉันไม่ใช่นักวิทยาศาสตร์คอมพิวเตอร์โดยการฝึกฝน อย่างไรก็ตามฉันมักจะพึ่งพา BigDecimal ใน Java หรือทศนิยมใน C # ฉันไม่ได้ลองใช้วิธีแก้ไขปัญหาอื่น ๆ ที่ฉันระบุไว้ แต่อาจจะดีมากเช่นกัน
สำหรับฉันฉันชอบ BigDecimal เพราะวิธีการที่รองรับ ทศนิยมของ C # นั้นดีมาก แต่ฉันไม่มีโอกาสได้ทำงานกับมันมากเท่าที่ฉันต้องการ ฉันทำการคำนวณทางวิทยาศาสตร์ที่น่าสนใจสำหรับฉันในเวลาว่างและ BigDecimal ดูเหมือนว่าจะทำงานได้ดีมากเพราะฉันสามารถตั้งค่าความแม่นยำของตัวเลขทศนิยมได้ ข้อเสียของ BigDecimal คืออะไร? อาจช้าในบางครั้งโดยเฉพาะถ้าคุณใช้วิธีแบ่ง
คุณอาจมองเข้าไปในห้องสมุดที่ให้บริการฟรีและเป็นกรรมสิทธิ์ใน C, C ++ และ Fortran
เพื่อเพิ่มคำตอบก่อนหน้านี้ยังมีตัวเลือกในการใช้Joda-Moneyใน Java นอกเหนือจาก BigDecimal เมื่อจัดการกับปัญหาที่ระบุในคำถาม ชื่อ Java modul คือ org.joda.money
ต้องใช้ Java SE 8 หรือใหม่กว่าและไม่มีการขึ้นต่อกัน
เพื่อความแม่นยำมากขึ้นมีการพึ่งพาเวลารวบรวม แต่ไม่จำเป็น
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
ตัวอย่างการใช้ 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();
เอกสารประกอบ: http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html
ตัวอย่างการใช้งาน: https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money
ตัวอย่างบางส่วน ... งานนี้ (ใช้งานไม่ได้ตามที่คาดหวัง) ในเกือบทุกภาษาการเขียนโปรแกรม ... ฉันได้ลองใช้ Delphi, VBScript, Visual Basic, JavaScript และตอนนี้กับ 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!!!");
}
เอาท์พุท:
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!!!
โฟลทเป็นรูปแบบเลขฐานสิบของทศนิยมที่มีการออกแบบที่แตกต่างกัน พวกเขาเป็นสองสิ่งที่แตกต่างกัน มีข้อผิดพลาดเล็กน้อยระหว่างสองประเภทเมื่อแปลงเป็นแบบอื่น นอกจากนี้โฟลตยังได้รับการออกแบบมาเพื่อแสดงค่าจำนวนมากสำหรับวิทยาศาสตร์ นั่นหมายความว่ามันถูกออกแบบมาให้สูญเสียความแม่นยำไปยังจำนวนที่น้อยมากและมากที่สุดด้วยจำนวนไบต์คงที่ ทศนิยมไม่สามารถแสดงถึงค่าจำนวนอนันต์ แต่จะเป็นเพียงจำนวนหลักทศนิยมนั้น ดังนั้นโฟลตและทศนิยมจึงมีวัตถุประสงค์ที่แตกต่างกัน
มีบางวิธีในการจัดการข้อผิดพลาดสำหรับค่าสกุลเงิน:
ใช้จำนวนเต็มยาวและนับเป็นเซ็นต์แทน
ใช้ความแม่นยำสองเท่ารักษาตัวเลขสำคัญของคุณไว้ที่ 15 เท่านั้นเพื่อให้สามารถจำลองทศนิยมได้อย่างแม่นยำ ปัดเศษก่อนนำเสนอค่า ปัดเศษเมื่อทำการคำนวณ
ใช้ไลบรารีทศนิยมเช่น Java BigDecimal ดังนั้นคุณไม่จำเป็นต้องใช้ double เพื่อจำลองทศนิยม
ป.ล. เป็นที่น่าสนใจที่จะรู้ว่าเครื่องคิดเลขวิทยาศาสตร์แบบใช้มือถือส่วนใหญ่ทำงานเป็นทศนิยมแทนที่จะลอย ดังนั้นไม่มีใครร้องเรียนข้อผิดพลาดการแปลงลอย