From 93e3a98a23289f27fd5de9c6318d38404df042ab Mon Sep 17 00:00:00 2001 From: "gu.martinm@gmail.com" Date: Sun, 27 Apr 2014 20:57:51 +0200 Subject: [PATCH] BigDecimal: never use float/double if decimals are important for you --- .../src/de/bigdecimal/test/MainTest.java | 352 +++++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 Allgemeines/BigDecimal/src/de/bigdecimal/test/MainTest.java diff --git a/Allgemeines/BigDecimal/src/de/bigdecimal/test/MainTest.java b/Allgemeines/BigDecimal/src/de/bigdecimal/test/MainTest.java new file mode 100644 index 0000000..aeaf190 --- /dev/null +++ b/Allgemeines/BigDecimal/src/de/bigdecimal/test/MainTest.java @@ -0,0 +1,352 @@ +package de.bigdecimal.test; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; + + +/* + * REMEMBER, double/float THE IN MEMORY VALUES ARE NOT USUALLY WHAT YOU EXPECT + * BECAUSE OF THE IEE 754. SO, IF DECIMALS ARE IMPORTANT FOR YOU (LIKE WITH CURRENCY) + * NEVER USE double/float IN ANY POINT OF YOUR APPLICATION. + * + */ + +/* + * NICE EXPLANATIONS ABOUT WHEN TO USE DOUBLE/FLOAT AND WHEN BIGDECIMAL + * + * 1. + * http://stackoverflow.com/questions/2545567/in-net-how-do-i-choose-between-a-decimal-and-a-double + * I usually think about natural vs artificial quantities. + * + * Natural quantities are things like weight, height and time. These will never be measured absolutely + * accurately, and there's rarely any idea of absolutely exact arithmetic on it: you shouldn't generally + * be adding up heights and then making sure that the result is exactly as expected. + * Use double for this sort of quantity. Doubles have a huge range, but limited precision; + * they're also extremely fast. + * + * The dominant artificial quantity is money. There is such a thing as "exactly $10.52", and if you + * add 48 cents to it you expect to have exactly $11. Use decimal for this sort of quantity. + * Justification: given that it's artificial to start with, the numbers involved are artificial + * too, designed to meet human needs - which means they're naturally expressed in base 10. + * Make the storage representation match the human representation. decimal doesn't have the range of double, + * but most artificial quantities don't need that extra range either. It's also slower than double, but + * I'd personally have a bank account which gave me the right answer slowly than a wrong answer quickly :) + * + * For a bit more information, I have articles on .NET binary floating point types and the .NET decimal type. + * (Note that decimal is a floating point type too - but the "point" in question is a decimal point, not a binary point.) + * + * FROM ME: + * BUT BE CAREFUL BECAUSE IF YOU HAVE MATH OPERATIONS WITH NATURAL QUANTITIES YOU COULD FINISH HAVING + * -0.0 or NaN, AND I GUESS YOU DO NOT WANT TO SHOW TO THE USER A WEIGHT OF -0.0 or NaN :/ SO DEPENDING ON + * THE NATURAL QUANTITIES (IF THEY ARE SMALL OR BIG NUMBERS) AND THE MATH OPERATIONS, PERHAPS + * YOU COULD WANT TO USE A FIXED-POINT NUMBER (LIKE BigDecimal IN JAVA OR decimal IN C#) + * SO, THIS IS A BIT COMPLICATED: PERHAPS EVEN IF YOU FINISH HAVING -0.0, YOU COULD DO SOMETHING + * LIKE THIS: + * public static boolean isNegative(double d) { + * return Double.compare(d, 0.0) < 0; + * } + * AND IN THAT CASE YOU COULD SHOW 0.0 INSTEAD OF -0.0. ALL DEPENDS ON WHAT ARE YOU DOING. + * I GUESS IF YOU DO NOT KNOW ANYTHING ABOUT THE NUMBERS YOU ARE USING YOU MUST USE + * FIXED-POINT NUMBER BigDecimal, decimal OR BUILDING YOURSELF SOME INTEGER WHERE THE LAST 4 NUMBERS + * COULD BE DECIMALS AND THEN YOU ALWAYS *10000 AND /10000. THIS IS THE FASTEST SOLUTION BUT + * I DO NOT THINK IT IS THE BEST. THE BEST (IMHO) IF YOU KNOW NOTHIG IS BigDecimal AND decimal + * (FIXED-POINT NUMBER) + * + * 2. + * SEE: http://randomascii.wordpress.com/2012/02/13/dont-store-that-in-a-float/ + * WITH VIDEO GAMES WHEN DECIMALS ARE IMPORTANT (FOR EXAMPLE WHEN CALCULATING FPS) YOU MIGHT + * WANT TO USE DOUBLE. BUT AT THE END (IMHO) THE BEST WOULD BE A FIXED-POINT NUMBER (LIKE BigDecimal IN + * JAVA OR decimal IN C#) + * + * 3. (fixed-point number) + * SEE: http://home.comcast.net/~tom_forsyth/blog.wiki.html#OffendOMatic link "A Matter of precision" + * YOU MUST ALWAYS USE FIXED-POINT NUMBER!!!! + * + * (note to 1.) + * + */ + +public class MainTest { + + public static void main(final String[] args) { + /** + * WARNING: if you write 165.01499999999998 after compiling this code + * javac will write "for you" the value 165.015 in your bytecode :/ + * Use: javap -verbose BigDecimal/bin/de/bigdecimal/test/MainTest You will see 165.015d + * instead of something closer to what you would have using double. + * For example, gcc with that value writes in your assembly code 4064A07AE147AE14. By the way + * 165.015d and 165.01499999999998 are the same in memory: 4064A07AE147AE14. They + * are adjacent values and because of that javac and Double.toString transform it to 165.015d. + * + * IF YOU READ THE Double.toString JAVADOC YOU WILL SEE THAT THE JAVA DEVELOPERS OF DOUBLE.TOSTRING + * DECIDED TO SHOW ALWAYS THE SMALLEST ADJACENT VALUE. THIS DECISION IS NOT GOOD OR BAD, + * IF FOR YOU IT IS A PROBLEM IT IS BECAUSE YOU SHOULD NOT HAVE USED SINCE THE FIRST + * VERY MOMENT DOUBLE OR FLOATS. IF DECIMALS ARE IMPORTANT FOR YOU MUST USE BigDecimal. + * SO, IF YOU THINK DOUBLE.TOSTRING IS WRONG, IT IS NOT THE PROBLEM OF DOUBLE.TOSTRING + * IT IS YOUR PROBLEM BECAUSE YOU ARE WORKING WHEN DOUBLE/FLOAT WHEN YOU SHOULD HAVE + * WORKED WITH BigDecimal. + **/ + + /** + * Double.toString TRANSFORMS 165.01499999999998 in 165.015d the same as javac does :/ + */ + + /** + * If decimals are important for you never use double/float. + **/ + + /** + * new BigDecimal(double) tries to represent the real value of a float/double. When I write + * real value I mean the value in memory, which should use IEE 754. + * new BigDecimal(String) tries to represent what the user expects to see. + * + * If you debug this code you will see that new BigDecimal(double) and new BigDecimal(String) + * are storing different values. The first one extracts (using Double.doubleToLongBits) + * the in memory value of some double/float and stores it in its internal instance variables. + * The second one takes the value in the string and stores it in its internal instance + * variables, it does no try to extract the in memory value (the IEE 754 value) + **/ + + final BigDecimal fromString = new BigDecimal("165.01499999999998"); + /** + * 1. I write: "165.01499999999998" + * 2. javac writes: string "165.01499999999998" + * 3. BigDecimal stores: 165.01499999999998 (the String value without modifications) + */ + + /** + * 1. extracts the stored value in BigDecimal + */ + System.out.println("fromString (BigDecimal stores the String value without modifications): " + fromString.toString()); + /** + * 1. extracts the stored value in BigDecimal + * 2. transform that value in double using FloatingDecimal.readJavaFormatString(s).doubleValue(); + * I do not know what FloatingDecimal.readJavaFormatString(s).doubleValue() returns but + * even if it does 165.0149999999999863575794734060764312744140625 then if we want to represent it we must use + * Double.toString and it ALWAYS TRANSFORM IT to 165.015 :( + * 3. create string with Double.toString() it ends up with 165.015 :( + * + */ + System.out.println("fromString using Double.toString (println): " + fromString.doubleValue()); + + /** + * Double.toHexString takes a double value and by means of Double.doubleToLongBits + * extracts the in memory values: (sign, mantissa and exponent) + * I do not know what FloatingDecimal.readJavaFormatString(s).doubleValue() returns but + * even if it does 165.0149999999999863575794734060764312744140625 it does not matter because + * 165.015, 165.01499999999998, 165.0149999999999863575794734060764312744140625 have the same in + * memory values: 165.0149999999999863575794734060764312744140625 + */ + System.out.println("fromString hexadecimal (IEE 754 value): " + Double.toHexString(fromString.doubleValue())); + + /** + * SCALE 2, HALF_UP we have 165.01499999999998 + * SCALE 2: 165.01 + * HALF_UP: 0.499999999998 <---- less than 0.5 then we should see as result 165.01 + */ + System.out.println("fromString rounding two: " + fromString.setScale(2, RoundingMode.HALF_UP).toString()); + System.out.println("fromString rounding two using Double.toString: " + fromString.setScale(2, RoundingMode.HALF_UP).doubleValue()); + /** + * SCALE 4, HALF_UP we have 165.01499999999998 + * SCALE 4: 165.0149 + * HALF_UP: 0.9999999998 <---- more than 0.5 then we should see as result 165.0150 + */ + System.out.println("fromString rounding four: " + fromString.setScale(4, RoundingMode.HALF_UP).toString()); + System.out.println("fromString rounding four using Double.toString: " + fromString.setScale(4, RoundingMode.HALF_UP).doubleValue()); + + + /** + * 1. I write: 165.01499999999998 + * 2. javac writes: string 165.015 + * 3. BigDecimal stores: 165.0149999999999863575794734060764312744140625 (the IEE 754 value, + * the in memory value) It uses Double.doubleToLongBits. As we know the in memory + * value of 165.015 is 165.0149999999999863575794734060764312744140625 and it is the same + * as the in memory value for 165.01499999999998 + */ + final BigDecimal fromDouble = new BigDecimal(165.01499999999998); + + /** + * 1. extracts the stored value in BigDecimal and shows it as string. + */ + System.out.println("fromDouble (BigDecimal stores the in memory value): " + fromDouble.toString()); + + /** + * 1. extracts the stored value in BigDecimal + * 2. transform that value in double using FloatingDecimal.readJavaFormatString(s).doubleValue(); + * I do not know what FloatingDecimal.readJavaFormatString(s).doubleValue() returns but + * even if it does 165.0149999999999863575794734060764312744140625 then if we want to represent it we must use + * Double.toString and it ALWAYS TRANSFORM IT to "165.015" (Double.toString removes adjacent values + * and shows the "smallest" adjacent value. :( + * 3. create string with Double.toString() it ends up with "165.015" :( + * + */ + System.out.println("fromDouble using Double.toString (println): " + fromDouble.doubleValue()); + + /** + * Double.toHexString takes a double value and by means of Double.doubleToLongBits + * extracts the in memory values: (sign, mantissa and exponent) + * I do not know what FloatingDecimal.readJavaFormatString(s).doubleValue() returns but + * even if it does 165.015 it does not matter because 165.015 has the same in + * memory value as 165.01499999999998: 165.0149999999999863575794734060764312744140625 + */ + System.out.println("fromDouble hexadecimal (IEE 754 value): " + Double.toHexString(fromDouble.doubleValue())); + System.out.println("fromDouble rounding two: " + fromDouble.setScale(2, RoundingMode.HALF_UP).toString()); + System.out.println("fromDouble rounding two using Double.toString: " + fromDouble.setScale(2, RoundingMode.HALF_UP).doubleValue()); + System.out.println("fromDouble rounding four: " + fromDouble.setScale(4, RoundingMode.HALF_UP).toString()); + System.out.println("fromDouble rounding four using Double.toString: " + fromDouble.setScale(4, RoundingMode.HALF_UP).doubleValue()); + + /** + * 1. I write: 165.01499999999998 + * 2. javac writes: string 165.015 + * 3. BigDecimal stores: 165.015 Even if javac did not take away decimals, because + * BigDecimal.valueOf is using Double.toString it will end up with 165.015 instead + * of 165.01499999999998. This BigDecimal uses the constructor new BigDecimal(String) + * which stores the value in String without transformations, the problem is + * there is Double.toString transformation before the constructor BigDecimal(String) + * IMHO this sucks, I do not understand why someone would want to use in this + * way BigDecimal, for example Apache MathUtils is using it for the round method + * and as you can see Double.toString applies some transformations, which in many cases + * could break our results... + * I would never use BigDecimal.valueOf, what I would try always to use is + * new BigDecimal(String) in the edges of my application (I would receive numbers + * as strings from the devices reading for example prices), then I would work in my + * whole application with BigDecimal and at the end (to show values to user) I would + * use BigDecimal.setScale(2, RoundingMode.HALF_UP).toString() IN THIS WAY MY APP + * WILL NEVER FAIL AND EVERYTHING WILL WORK AS EXPECTED!!!! + */ + final BigDecimal fromValueOf = BigDecimal.valueOf(165.01499999999998); + + /** + * 1. extracts the stored value in BigDecimal and shows it as string. + * I expected to see the same as in fromDouble.toString() but I WAS WRONG BECAUSE + * BigDecimal STORES DIFFERENT VALUES DEPENDING ON THE CONSTRUCTOR WE USE. + */ + System.out.println("fromValueOf (BigDecimal stores the Double.toString(double) value " + + "without modifications, it is Double.toString what would remove adjacent values. " + + "In this case, even before Double.toString, javac removed adjacent values and in byte code " + + "we have 165.015. Anyhow even without javac removing values, Double.toString would have " + + "finished with 165.015 instead of 165.01499999999998 WHEN DECIMALS ARE A PROBLEM, YOU MUST NO USE " + + "DOUBLE/FLOAT IF YOU ARE USING THEM, YOU ARE DOING IT WRONG): " + fromValueOf.toString()); + + /** + * 1. extracts the stored value in BigDecimal + * 2. transform that value in double using FloatingDecimal.readJavaFormatString(s).doubleValue(); + * I do not know what FloatingDecimal.readJavaFormatString(s).doubleValue() returns but + * even if it does 165.0149999999999863575794734060764312744140625 then if we want to represent + * it we must use Double.toString and it ALWAYS TRANSFORM IT to 165.015 :( + * 3. create string with Double.toString() it ends up with 165.015 :( + * + */ + System.out.println("fromValueOf using Double.toString (println): " + fromValueOf.doubleValue()); + + /** + * Double.toHexString takes a double value and by means of Double.doubleToLongBits + * extracts the in memory values: (sign, mantissa and exponent) + * I do not know what FloatingDecimal.readJavaFormatString(s).doubleValue() returns but + * even if it does 165.0149999999999863575794734060764312744140625 it does not matter because + * 165.015 has the same in memory value: 165.0149999999999863575794734060764312744140625 + */ + System.out.println("fromValueOf hexadecimal (IEE 754 value): " + Double.toHexString(fromValueOf.doubleValue())); + System.out.println("fromValueOf rounding two: " + fromValueOf.setScale(2, RoundingMode.HALF_UP).toString()); + System.out.println("fromValueOf rounding two using Double.toString: " + fromValueOf.setScale(2, RoundingMode.HALF_UP).doubleValue()); + System.out.println("fromValueOf rounding four: " + fromValueOf.setScale(4, RoundingMode.HALF_UP).toString()); + System.out.println("fromValueOf rounding four using Double.toString: " + fromValueOf.setScale(4, RoundingMode.HALF_UP).doubleValue()); + + + /** + * 1. Double.toString ALWAYS TAKES AWAY DECIMALS (if they are not useful, as in our case + * the in memory value is the same for 165.015 and for 165.01499999999998) + * 2. BigDecimal STORES DIFFERENT VALUES DEPENDING ON WHAT CONSTRUCTOR WE USE. + * BECAUSE OF THAT BigDecimal.toString RETURNS DIFFERENT VALUES DEPENDING ON THE + * CONSTRUCTOR. + * 3. BE CAREFUL, DEPENDING ON WHAT YOU WANT TO DO YOU MIGHT WANT TO USE BigDecimal(double) + * instead of BigDecimal(String) + * BigDecimal(String): stores the String as a BigDecimal value + * BigDecimal(double): stores the in memory value of that double as a BigDecimal value. + * 4. JUST IN THE MOMENT YOU CONVERT DOUBLE TO STRING THERE ARE PROBLEMS IF YOU WANT + * TO KEEP ALL YOUR DECIMALS. IN MEMORY: + * - 165.015 is 165.0149999999999863575794734060764312744140625 + * - 165.01499999999998 is 165.0149999999999863575794734060764312744140625 + * SO IF DOUBLE.TOSTRING SHOWS 165.015 IT IS NOT WRONG, THE PROBLEM IS JUST IN THE MOMENT + * YOU USE double YOUR REAL DECIMALS DISSAPEAR BECAUSE THE IEE 754 VALUE IS NOT WHAT YOU EXPECT!!!! + * 5. If you use double or float, some times will be impossible to retrieve the value + * you wrote because the in memory value (the IEE 754) is not what you expect. Because + * of that if Double.toString removes some decimals, it is not wrong because the in memory + * value is the same for adjacent values, it does not know what you wanted to see, + * it just know what there is in memory. Besides, the Java developers of Double.toString + * decided to remove decimals when having adjacent values and to show to the user + * the "smallest" adjacent value. That is why if you use + * Double.toString(165.0149999999999863575794734060764312744140625) you finish having + * "165.015" instead of "165.0149999999999863575794734060764312744140625". + */ + + /** + * AND OF COURSE, NEVER USE FLOAT OR DOUBLE IF THE DECIMALS ARE IMPORTANT FOR YOU!!!!!!! + * THAT IS WHY YOU MUST NEVER USE FLOAT/DOUBLE WITH CURRENCY. ROUND,DIVISION,SUM,REST + * OPERATIONS WILL BE WRONG FOR SURE IF YOU USE FLOAT/DOUBLE. THE WHOLE APPLICATION MUST USE + * BigDecimal WITH CURRENCY, JUST IN THE MOMENT YOU USE DOUBLE/FLOAT FOR ROUND,DIVISION,SUM, + * REST OPERATIONS YOUR APPLICATION WILL BE BROKEN. + * + * AND NEVER EVER TRY TO USE double == double / double != double / float == float / + * float != float + */ + + + /** + * I GUESS FOR SOME APPLICATION THE BEST WOULD BE TO USE IN THE EDGES (FOR EXAMPLE + * SOME DEVICE READING PRICES AND RETURNING PRICES AS STRINGS) STRING, THEN WE COULD USE + * STRING AS INPUT PARAMETER FOR BigDecimal, (WE WOULD USE new BigDecimal(String)) + * THE WHOLE APPLICATION WOULD WORK WITH BigDecimal FOR CURRENCY AND IN THIS WAY NOTHING + * WRONG SHOULD HAPPEN. + */ + + + final BigDecimal fromFunnyString = new BigDecimal("165.015"); + System.out.println("fromFunnyString (BigDecimal stores the String value without modifications): " + fromFunnyString.toString()); + System.out.println("fromFunnyString using Double.toString (println): " + fromFunnyString.doubleValue()); + System.out.println("fromFunnyString hexadecimal (IEE 754 value): " + Double.toHexString(fromFunnyString.doubleValue())); + System.out.println("fromFunnyString rounding two: " + fromFunnyString.setScale(2, RoundingMode.HALF_UP).toString()); + System.out.println("fromFunnyString rounding two using Double.toString: " + fromFunnyString.setScale(2, RoundingMode.HALF_UP).doubleValue()); + System.out.println("fromFunnyString rounding four: " + fromFunnyString.setScale(4, RoundingMode.HALF_UP).toString()); + System.out.println("fromFunnyString rounding four using Double.toString: " + fromFunnyString.setScale(4, RoundingMode.HALF_UP).doubleValue()); + + + final BigDecimal fromFunnyDouble = new BigDecimal(165.015); + System.out.println("fromFunnyDouble (BigDecimal stores the in memory value): " + fromFunnyDouble.toString()); + System.out.println("fromFunnyDouble using Double.toString (println): " + fromFunnyDouble.doubleValue()); + System.out.println("fromFunnyDouble hexadecimal (IEE 754 value): " + Double.toHexString(fromFunnyDouble.doubleValue())); + + /** + * BigDecimal stores the in memory value, and it will work with it. That is the reason + * we end up having "unexpected" results in the next two cases. + * There is not 165.015 value for IEE 754. The in memory value is: 165.0149999999999863575794734060764312744140625 + */ + System.out.println("(YOU DID NOT EXPECT THIS, DID YOU?) fromFunnyDouble rounding two: " + fromFunnyDouble.setScale(2, RoundingMode.HALF_UP).toString()); + System.out.println("(YOU DID NOT EXPECT THIS, DID YOU?) fromFunnyDouble rounding two using Double.toString: " + fromFunnyDouble.setScale(2, RoundingMode.HALF_UP).doubleValue()); + System.out.println("(YOU DID NOT EXPECT THIS, DID YOU?) fromFunnyDouble rounding four: " + fromFunnyDouble.setScale(4, RoundingMode.HALF_UP).toString()); + System.out.println("(YOU DID NOT EXPECT THIS, DID YOU?) fromFunnyDouble rounding four using Double.toString: " + fromFunnyDouble.setScale(4, RoundingMode.HALF_UP).doubleValue()); + + + final BigDecimal fromFunnyValueOf = BigDecimal.valueOf(165.015); + System.out.println("fromFunnyValueOf (BigDecimal stores the Double.toString(double) value " + + "without modifications, Double.toString removes adjacent values. In this case " + + "the Double.toString(165.015) is 165.015 but in other cases it could be a problem if " + + "Double.toString removes our decimals. Anyhow WHEN DECIMALS ARE A PROBLEM, YOU MUST NO USE " + + "DOUBLE/FLOAT IF YOU ARE USING THEM, YOU ARE DOING IT WRONG): " + fromFunnyValueOf.toString()); + System.out.println("fromFunnyValueOf using Double.toString (println): " + fromFunnyValueOf.doubleValue()); + System.out.println("fromFunnyValueOf hexadecimal (IEE 754 value): " + Double.toHexString(fromFunnyValueOf.doubleValue())); + System.out.println("fromFunnyValueOf rounding two: " + fromFunnyValueOf.setScale(2, RoundingMode.HALF_UP).toString()); + System.out.println("fromFunnyValueOf rounding two using Double.toString: " + fromFunnyValueOf.setScale(2, RoundingMode.HALF_UP).doubleValue()); + System.out.println("fromFunnyValueOf rounding four: " + fromFunnyValueOf.setScale(4, RoundingMode.HALF_UP).toString()); + System.out.println("fromFunnyValueOf rounding four using Double.toString: " + fromFunnyValueOf.setScale(4, RoundingMode.HALF_UP).doubleValue()); + + final DecimalFormat tempFormatter = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US); + tempFormatter.applyPattern("#####.#################"); + System.out.println("DecimalFormat: " + tempFormatter.format(165.01499999999998)); + System.out.println("String.format (printf style): " + String.format(Locale.US, "%.20f", 165.01499999999998)); + } + +} -- 2.1.4