BigDecimal - comparing invariant to DecimalFormat - java

I generate one BIgDecimal using a format ###,###.## which results in 660,000.00 and then I try to comapre it against a new BigDecimal(660000.00) but it returns false every time. Is there a way to compare the values invariant to the initial format they were created in?

This is because the scale of the two BigDecimals differ:
System.out.println(new BigDecimal("660000.00").scale()); // 2
System.out.println(new BigDecimal(660000.00).scale()); // 0
To compare them regardless of scale, you could do:
BigDecimal bd1 = new BigDecimal("660000.00");
BigDecimal bd2 = new BigDecimal(660000.00);
int s = Math.max(bd1.scale(), bd2.scale());
.. bd1.setScale(s).equals(bd2.setScale(s)) .. // == true.
A side-note: Beware that a double literal, such as .1 for instance, does not represent the real value .1, but an approximation of it. (This is due to inaccuracies in the floating point representation.) This can be seen by doing
System.out.println(new BigDecimal(.1));
which prints 0.1000000000000000055511151231257827021181583404541015625
However, new BigDecimal(".1") does indeed represent exactly .1, which is why
new BigDecimal(.1).equals(new BigDecimal(".1"))
is false.

Related

Observations with Round-ing in Android Studio - java. And some practical explanations expected

In Android Studio I had problems with calculating invoice totals because of the way java rounds. I know there are a lot of explanations, but many recommend methods that don't return reliable results.
For example:
1. Math.round((double)4.715 * (double)100 ) / (double)100 = 4.72 (expected 4.72)
2. Math.round((double)4.725 * (double)100 ) / (double)100 = 4.72 (but expected 4.73)
You can't put this code in an app for a client who calculates invoices. Because , in my case for example, the same invoice is calculated in another system and the result is different, meaning 4.72 respectively 4.73
I know that a double can't be represented exactly and the decimals are different than what we see. But we need a method that returns results as we expect.
Another example would be:
1. java.math.BigDecimal.valueOf(4.715).setScale(2,java.math.BigDecimal.ROUND_HALF_UP).doubleValue() = 4.72
2. new java.math.BigDecimal(4.715).setScale(2,java.math.BigDecimal.ROUND_HALF_UP).doubleValue() = 4.71
3. new java.math.BigDecimal( String.valueOf(4.715) ).setScale(2,java.math.BigDecimal.ROUND_HALF_UP).doubleValue() = 4.72
I think all these aspects could be well explained in Java documentation, but they should indicate a certain method for calculating rounds, a reliable method which returns results as we expected. I only wanted to round to 2 decimales.
In conclusion, which I hope will help some of the beginners, I think that the following method would return stable and good results:
java.math.BigDecimal.valueOf(4.715).setScale(2,java.math.BigDecimal.ROUND_HALF_UP).doubleValue() = 4.72
Or, at least, this is my observation after 3+ years of intensive usage of an app (500+ users every working day).
All practical explanations for these above are very welcome, so we can better understand how to avoid unexpected results.
For the BigDecimal examples the javadoc explains the difference.
BigDecimal(double value) ... is the exact decimal representation of the double's binary floating-point value.
Which we can check, by just printing the value.
System.out.println(new BigDecimal(4.715));
#4.714999999999999857891452847979962825775146484375
Which is barely less than 4.715, but enough such that it gets rounded down.
BigDecimal.valueOf(double value) uses the string representation of the double value from Double.toString(double value) which has quite a few rules.
System.out.println(Double.toString(4.715));
#4.715
The safest best is to just use BigDecimal for your calculations. Especially when dealing with arithmetic operations. It isn't clear when the value will switch to needing more decimal places. For example:
double d = 4.11547;
BigDecimal bd = BigDecimal.valueOf(d);
I this case, the string representation of d is 4.11547, so BigDecimal.valueOf returns the value that is written.
BigDecimal s1 = BigDecimal.valueOf(d-3);
BigDecimal s2 = bd.subtract(new BigDecimal(3));
It might be surprising to find s1 and s2 are different since '3' doesn't get rounded.
System.out.println(s1 + ", " + s2);
#1.1154700000000002, 1.11547
So it is best to use the BigDecimal methods for arithmetic too.
It's in the nature of binary floating point data types, like float and double in Java. double actually states this in his name. It has double precision compared to float - but it is not an exact representation of a decimal number.
Just adding some simplified math detail to the existing answer. This might help understand the seemingly strange behavior of Java floating point numbers.
The root cause of the problem is binary vs. decimal representation of numbers. You use decimal representation when you use a floating point literal in your code, e.g. double d = 1.5; or a String value, e.g. String s = "1.5";.
But the JVM uses a binary representation of the number. The mapping for integer numbers is easy (d for decimal, b for binary): 1 = 1b, 2d = 10b, 3d = 11b .... There is no issue with integer numbers. int and long work just the way you would expect. Except for the overflow...
But for floating point numbers things are different: 0.5d = 0.1b, 0.25d = 0.01b, 0.125d = 0.001b.... You are only able to add values for the series 1/2, 1/4, 1/8, 1/16... Now imagine, you want to show 0.1d in binary representation.
You start with 0.0001b = 0.0625d, which is the first binary value that is still less than 0.1d. 0.0375d remaining. You continue, and the next close value is 0.03125d, and so on. You'll acutally never get to exactly 0.1d. All you get is an approximation. You'll get closer and closer.
Consider the following piece of code. It does the approximation with the help of BigDecimal values:
public void approximate0dot1() {
BigDecimal destVal = new BigDecimal("0.1");
BigDecimal curVal = new BigDecimal("0");
BigDecimal inc = new BigDecimal("1");
BigDecimal div = new BigDecimal("2");
for (int step = 0; step < 20; step++) {
BigDecimal probeVal = curVal.add(inc);
int cmp = probeVal.compareTo(destVal);
if (cmp == 0) {
break;
} else if (cmp < 0) {
curVal = probeVal;
System.out.format("Added: %s, current value: %s, remaining: %s\n", inc, curVal, destVal.subtract(curVal));
}
inc = inc.divide(div);
}
System.out.format("Final value: %s\n", curVal);
}
And the output is:
Added: 0.0625, current value: 0.0625, remaining: 0.0375
Added: 0.03125, current value: 0.09375, remaining: 0.00625
Added: 0.00390625, current value: 0.09765625, remaining: 0.00234375
Added: 0.001953125, current value: 0.099609375, remaining: 0.000390625
Added: 0.000244140625, current value: 0.099853515625, remaining: 0.000146484375
Added: 0.0001220703125, current value: 0.0999755859375, remaining: 0.0000244140625
Added: 0.0000152587890625, current value: 0.0999908447265625, remaining: 0.0000091552734375
Added: 0.00000762939453125, current value: 0.09999847412109375, remaining: 0.00000152587890625
Final value: 0.09999847412109375
This is just a basic example to show the underlying issue. Internally, the JVM obviously does some optimization to get the best possible approximation for the available 64-bit precision, e.g.
System.out.println(new BigDecimal(0.1));
// prints 0.1000000000000000055511151231257827021181583404541015625
But this example shows, that there is already a rounding issue with decimal numbers a simple as a constant with the decimal value 0.1.
Some basic tips:
Do not use BigDecimal(double) constructor if you need exact decimal math, use BigDecimal(String) instead. Bad: new BigDecimal(0.1), Good: new BigDecimal("0.1")
Do not mix BigDecimal and floating point arithmetic, e.g. do not extract double value for further calculations like new BigDecimal("0.1").doubleValue();

Why HashSet differentiates 0.0 and -0.0

When I tried this:
HashSet<Double> set = new HashSet<>();
Double d1 = new Double(0);
Double d2 = new Double(0);
Double d3 = new Double(-0);
set.add(d1);
System.out.println(set.contains(d2));
System.out.println(set.contains(d3));
The output was what I expected:
true
true
But when I tried:
HashSet<Double> set = new HashSet<>();
Double d1 = new Double(0.0);
Double d2 = new Double(0.0);
Double d3 = new Double(-0.0);
set.add(d1);
System.out.println(set.contains(d2));
System.out.println(set.contains(d3));
or
set.add(Double.valueOf(d1));
System.out.println(set.contains(Double.valueOf(d2)));
System.out.println(set.contains(Double.valueOf(d3)));
To my surprise, the output was:
true
false
Why this happened? How do I make HashSet treat (0.0) and (-0.0) the same?
Is there a better way than if(num == -0.0) num = 0.0;?
This is explained by the docs for Double.
If d1 represents +0.0 while d2 represents -0.0, or vice versa, the equal test has the value false, even though +0.0==-0.0 has the value true.
So a Double created from 0.0 is not the same as a Double created from -0.0. The same is not true when you use 0 and -0 because integers use twos-complement, which has no notion of negative zero. -0 is the same as 0. doubles, on the other hand, use the IEEE standard for floating point values, which does recognize a negative zero value.
This behavior is all fixed, so there's no way to have a HashSet treat 0.0 and -0.0 as the same. If you want to do that, you'll need to manually convert all negative zero values into positive zeros before adding or searching for them.
-0.0 is a literal for a double value that is distinct from 0.0.
-0 is the negation operator applied to the int value 0, which gives just the int value 0.
Therefore, new Double(-0) is equivalent to new Double(0), whereas new Double(-0.0) and new Double(0.0) actually produce two non-equal Double objects.
See this question for some explanations for why it is necessary to have two different floating point zero values.
As explained in Wikipedia (thanks), the IEEE format for floating point actually supports negative and positive zeros for various reasons. https://en.wikipedia.org/wiki/Signed_zero
The reason why you find the 0.0 and -0.0 different in your hashmap is directly derived from the IEEE representation. The Double#hashCode method uses the raw bits of the floating point number to calculate the hashcode. Since 0.0 and -0.0 and possibly even +0.0 are different in terms of bits, as some numerical calculations obviously require this, their bits are different and therefore the hashcodes.

BigDecimal methods send formatted numbers

I'm trying to round my big BigDecimals off to three decimal places. For instance, let's say I have some
BigDecimal X = 1362.59633
I wanna get:
1362.596
Here is what I'm doing:
BigDecimal Y = X.round(new MathContext(3));
But I keep getting this: 1.36E+3.
What should I do in this case?
Thanks
You actually don't have to round but you have to set the scale of your BigDecimal:
BigDecimal X = new BigDecimal("1362.59633");
X = X.setScale(3, BigDecimal.ROUND_HALF_UP);
System.out.println(X.doubleValue());
This will print out 1362.596.
Note that setScale returns a new BigDecimal so you have to assign it like I did in my example.
edit: there are several kinds of rounding strategies. Check the BigDecimal class for reference. I edited my answer to use BigDecimal.ROUND_HALF_UP.
BigDecimal X = new BigDecimal(1362.59633);
X = X.setScale(3,RoundingMode.HALF_EVEN);
System.out.println(X); //1362.596
Note that since BigDecimal objects are immutable, calls of this method
do not result in the original object being modified, contrary to the
usual convention of having methods named setX mutate field X. Instead,
setScale returns an object with the proper scale; the returned object
may or may not be newly allocated.
BigDecimal y= x.round(new MathContext(7, RoundingMode.HALF_EVEN));
This should work for you.
If you use only double you can do
double d = 1362.59633;
double d2 = Math.round(d * 1e3) / 1e3;
System.out.println(d2);
prints
1362.596
Note: this doesn't create any objects to perform the rounding.
We can use DecimalFormat class to define the format of the double data. You can define like new DecimalFormat("####.000")
we can call format method the value you want.
Example
DecimalFormat format = new DecimalFormat("####.000");
format.format("1362.59633");
First set the scale to 3 using setScale with RoundingMode.DOWN as the rounding mode to truncate
Then call toPlainString to guarantee the return of a String representation without an exponent field
Calling toString on a BigDecimal does not guarantee the return of a String without an exponent. It returns the string representation using scientific notation if an exponent is needed
An example based on your question:
BigDecimal x = BigDecimal.valueOf( 1362.59633 );
System.out.println( x.setScale( 3, RoundingMode.DOWN ).toPlainString( ) );

Convert double to BigDecimal and set BigDecimal Precision

In Java, I want to take a double value and convert it to a BigDecimal and print out its String value to a certain precision.
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
double d=-.00012;
System.out.println(d+""); //This prints -1.2E-4
double c=47.48000;
BigDecimal b = new BigDecimal(c);
System.out.println(b.toString());
//This prints 47.47999999999999687361196265555918216705322265625
}
}
It prints this huge thing:
47.47999999999999687361196265555918216705322265625
and not
47.48
The reason I'm doing the BigDecimal conversion is sometimes the double value will contain a lot of decimal places (i.e. -.000012) and the when converting the double to a String will produce scientific notation -1.2E-4. I want to store the String value in non-scientific notation.
I want to have BigDecimal always have two units of precision like this: "47.48". Can BigDecimal restrict precision on conversion to string?
The reason of such behaviour is that the string that is printed is the exact value - probably not what you expected, but that's the real value stored in memory - it's just a limitation of floating point representation.
According to javadoc, BigDecimal(double val) constructor behaviour can be unexpected if you don't take into consideration this limitation:
The results of this constructor can be somewhat unpredictable. One
might assume that writing new BigDecimal(0.1) in Java creates a
BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with
a scale of 1), but it is actually equal to
0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double (or, for that
matter, as a binary fraction of any finite length). Thus, the value
that is being passed in to the constructor is not exactly equal to
0.1, appearances notwithstanding.
So in your case, instead of using
double val = 77.48;
new BigDecimal(val);
use
BigDecimal.valueOf(val);
Value that is returned by BigDecimal.valueOf is equal to that resulting from invocation of Double.toString(double).
It prints 47.48000 if you use another MathContext:
BigDecimal b = new BigDecimal(d, MathContext.DECIMAL64);
Just pick the context you need.
You want to try String.format("%f", d), which will print your double in decimal notation. Don't use BigDecimal at all.
Regarding the precision issue: You are first storing 47.48 in the double c, then making a new BigDecimal from that double. The loss of precision is in assigning to c. You could do
BigDecimal b = new BigDecimal("47.48")
to avoid losing any precision.
Why not :
b = b.setScale(2, RoundingMode.HALF_UP);
It's printing out the actual, exact value of the double.
Double.toString(), which converts doubles to Strings, does not print the exact decimal value of the input -- if x is your double value, it prints out exactly enough digits that x is the closest double to the value it printed.
The point is that there is no such double as 47.48 exactly. Doubles store values as binary fractions, not as decimals, so it can't store exact decimal values. (That's what BigDecimal is for!)
The String.format syntax helps us convert doubles and BigDecimals to strings of whatever precision.
This java code:
double dennis = 0.00000008880000d;
System.out.println(dennis);
System.out.println(String.format("%.7f", dennis));
System.out.println(String.format("%.9f", new BigDecimal(dennis)));
System.out.println(String.format("%.19f", new BigDecimal(dennis)));
Prints:
8.88E-8
0.0000001
0.000000089
0.0000000888000000000
BigDecimal b = new BigDecimal(c).setScale(2,BigDecimal.ROUND_HALF_UP);
In Java 9 the following is deprecated:
BigDecimal.valueOf(d).setScale(2, BigDecimal.ROUND_HALF_UP);
instead use:
BigDecimal.valueOf(d).setScale(2, RoundingMode.HALF_UP);
Example:
double d = 47.48111;
System.out.println(BigDecimal.valueOf(d)); //Prints: 47.48111
BigDecimal bigDecimal = BigDecimal.valueOf(d).setScale(2, RoundingMode.HALF_UP);
System.out.println(bigDecimal); //Prints: 47.48

Converting numeric value with currency symbol back to Decimal with NumberFormat

I would like to convert a possibly Decimal value prefixed with currency symbol into only numeric value.
For example -
The value can be like any of the following
String s1 = "£32,847,676.65";
String s2 = "£3,456.00";
String s3 = "£831,209";
I would like the result after conversion to be like - 32847676.65, 3456.00 and 831209.
I tried using the parse() method of the NumberFormat in this way -
NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.UK);
numberFormat.setMinimumFractionDigits(2);
Number num = nf.parse(s1);
double dd = num.doubleValue();
BigDecimal gg = new BigDecimal(dd);
System.out.println(gg);
But the result is - 32847676.649999998509883880615234375 which is not quite exactly the correct one.
I need it to be numeric so that may be I can perform some kind of calculation.
Can you guys guide me with what else can I try
You already parse the value correctly. The problem is this:
BigDecimal gg = new BigDecimal(dd);
You covnert the value to BigDecimal, and the rounding problems of doubles account for the decimal places after the dot. Use:
BigDecimal gg = new BigDecimal(dd).setScale(2);
or
BigDecimal gg = new BigDecimal(dd).setScale(2,RoundingMode.HALF_UP);
When playing with BigDecimal, the appropriate constructor is BigDecimal(String val)
NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.UK);
BigDecimal gg = new BigDecimal(nf.parse(s1).toString());
System.out.println(gg);
BigDecimal(double val) does construct an exact decimal representation of the double value, which is not the human readable value you expected.
"The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.
[...]
Therefore, it is generally recommended that the String constructor be used in preference to this one"
Source : BigDecimal javadoc
You can try the following without BigDecimal or NumberFormat. ;)
String s1 = "£32,847,676.65";
// remove the £ and ,
String s2 = s1.replaceAll("[£,]", "");
// then turn into a double
double d = Double.parseDouble(s2);
// and round up to two decimal places.
double value = (long) (d * 100 + 0.5) / 100.0;
System.out.printf("%.2f%n", value);
prints
32847676.65
If youw ant to avoid rounding error in your calculations but don't want the heavy weight BigDecimal you can use long cents.
// value in cents as an integer.
long value = (long) (d * 100 + 0.5);
// perform some calculations on value here
System.out.printf("%.2f%n", value / 100.0);
It is not guaranteed to work, but according to the NumberFormat API documentation, its getXyzInstance methods will return a DecimalFormat instance for "the vast majority of locales". This can probably be interpreted as "for all locales, unless proprietary locale service providers are installed".
If you can cast your NumberFormat to DecimalFormat, you can tell it to parse to a BigDecimal directly, reducing your code to:
DecimalFormat nf = (DecimalFormat) NumberFormat.getCurrencyInstance(Locale.UK);
nf.setParseBigDecimal(true);
BigDecimal gg = (BigDecimal) nf.parse(s1);
System.out.println(gg);
In this case, you will have no problem with the inaccuracy of binary floating point numbers.

Categories

Resources