Floating point precision not preserved when converting floats #332
Replies: 28 comments 4 replies
-
Hi @daniel-shuy - why is that an issue? |
Beta Was this translation helpful? Give feedback.
-
That's because the precision is not preserved. It should return
I've created a PR (#331) to fix this. |
Beta Was this translation helpful? Give feedback.
-
@daniel-shuy according to https://jcp.org/en/participation/members/S you don't seem to be a JCP member, would you be willing to join because otherwise we cannot merge this PR? |
Beta Was this translation helpful? Give feedback.
-
@daniel-shuy - by reading into your PR, I can see you are converting a float (32bit IEEE 754) into its rounded decimal representation (thats the toString conversion) and then parse it with a If you think there is a bug, please demonstrate the issue with a test case. |
Beta Was this translation helpful? Give feedback.
-
I know that's a bit of a puzzler but eg. |
Beta Was this translation helpful? Give feedback.
-
@andi-huber well to be more exact, we actually don't want to gain precision (eg. we want You can test the behavior with this: var number = Float.valueOf(3.524f);
System.out.println(BigDecimal.valueOf(number.doubleValue())); // 3.5239999294281006
System.out.println(new BigDecimal(number.toString())); // 3.524 But good point, I'll add some test cases. |
Beta Was this translation helpful? Give feedback.
-
+1 it would be nice to back a changed behavior (but also the existing one) with more JUnit tests. |
Beta Was this translation helpful? Give feedback.
-
My humble advice: please have a short excursion into floating point number representation according to IEEE 754, before writing any supposedly fixing code, which we are not going to merge. |
Beta Was this translation helpful? Give feedback.
-
@andi-huber Also added you as reviewer for the PR, if there are functional problems please discuss it there, I think maybe it could be better to call it a Draft PR for now, WDTY? |
Beta Was this translation helpful? Give feedback.
-
Converting an IEEE 754 number with
So replacing Currently it is not even necessary to do a |
Beta Was this translation helpful? Give feedback.
-
@desruisseaux unfortunately, when you pass a Float to public double doubleValue() {
return (double)value;
} Try running this code and you'll see what I mean: var number = Float.valueOf(3.524f);
System.out.println(BigDecimal.valueOf(number)); // 3.5239999294281006
System.out.println(new BigDecimal(number.toString())); // 3.524 |
Beta Was this translation helpful? Give feedback.
-
@daniel-shuy yes I know, that sentence was specifically about the |
Beta Was this translation helpful? Give feedback.
-
@desruisseaux ah sorry I misunderstood, that's a good point, that will greatly simplify the code, thanks! |
Beta Was this translation helpful? Give feedback.
-
No problem. As a side note (but not something needed for this particular case), below is a code for converting a |
Beta Was this translation helpful? Give feedback.
-
I made both @desruisseaux and @andi-huber reviewers for the PR, please check it out and comment or suggest changes (or close if you really think it wasn't appropriate) From a process point we should have proof, @daniel-shuy requested to become an Associate JCP Member or his name already showing in https://jcp.org/en/participation/members/S. |
Beta Was this translation helpful? Give feedback.
-
Thanks @keilw. I'll try to apply to become a JCP Member as soon as possible. |
Beta Was this translation helpful? Give feedback.
-
A better example: System.out.println(Quantities.getQuantity(4.1f, Units.METRE).to(MetrixPrefix.CENTI(Units.METRE)).getValue()) prints |
Beta Was this translation helpful? Give feedback.
-
The key question I guess then is, as we eventually have to convert a float (binary representation) to decimal representation which method is best. As it seems, we are discussing 2 methods:
If I understand correctly A is 19 decimal digits, whereas B is only 9 digits. However for the 'widening' of float to double, all binary places we are winning are set to null, which makes the difference between method (1) and (2) less dramatic. But still, its not clear to me why we would want to prefer method (2). |
Beta Was this translation helpful? Give feedback.
-
When widening from
I agree that formatting a number as a string and parsing it back looks like an ugly hack. But actually a more mathematically elegant solution would be very complicated. |
Beta Was this translation helpful? Give feedback.
-
@andi-huber I may have misunderstood the original purpose If that is the case, then it is indeed not a bug, but an enhancement. While the change is not strictly necessary - I can simply format the value output with Because of the current behavior, I initially assumed that the precision is kept between unit conversions. Eg. when converting If you all do decide not to merge the PR, I hope that at least the documentation can be updated to reflect that (maybe |
Beta Was this translation helpful? Give feedback.
-
Hi @desruisseaux - I agree, that setting the additional bits to 0 when doing the widening part is an arbitrary choice, unless the float number actually is an exact representation of the corresponding number! In which case the widening is not just noise, but the correct extension. Granted, we can discuss how relevant that is in practice, but still there is a case in my opinion to keep it that way. |
Beta Was this translation helpful? Give feedback.
-
Both |
Beta Was this translation helpful? Give feedback.
-
@andi-huber : yes you are right. But this is an information that only the user know. @keilw : saying that both About performance, yes there is a penalty. But |
Beta Was this translation helpful? Give feedback.
-
If it's mainly for values passed as float, then it might be OK. For performance purists they may not like to use |
Beta Was this translation helpful? Give feedback.
-
I believe this is not a discussion about memory footprint or performance. When a calculation needs widening of types to BigDecimal, we do this regardless, of the initial value (float or double) that was passed into the Quantity factory method. Purists of any kind will have to resort to other means anyway. I'd rather say the discussion is about which of the 2 methods (from above) we want to implement. I do see the benefits of the suggested changes (method (2), eg. less confusion for the user; smaller decimal number representation, as intended by the user anyway), but I'm also concerned about breaking a few corner cases, that's all. |
Beta Was this translation helpful? Give feedback.
-
Here's one corner case, which is in favor of method (1) in our discussion: Lets multiply 2 floating point numbers a and b where the result is exactly import java.math.BigDecimal;
public class FloatIssue {
private static float a = 1024f*1024f;
private static float b = 1f/a;
static void floatCalc() {
float x = a * b;
System.out.println("floatCalc: " + x);
}
static void bigDecCalc1() {
BigDecimal x1 = new BigDecimal(Double.toString(a));
BigDecimal x2 = new BigDecimal(Double.toString(b));
BigDecimal x = x1.multiply(x2);
System.out.printf("float to ~18 digit %s (error: %s)%n", x, error(x));
}
static void bigDecCalc2() {
BigDecimal x1 = new BigDecimal(Float.toString(a));
BigDecimal x2 = new BigDecimal(Float.toString(b));
BigDecimal x = x1.multiply(x2);
System.out.printf("float to ~9 digit %s (error: %s)%n", x, error(x));
}
private static BigDecimal error(BigDecimal x) {
return BigDecimal.ONE.subtract(x).abs();
}
public static void main(String[] args) {
floatCalc(); // 1.0
bigDecCalc1(); // float to ~18 digit 1.000000000000000000000 (error: 0E-21)
bigDecCalc2(); // float to ~9 digit 0.99999998279680 (error: 1.720320E-8)
}
} |
Beta Was this translation helpful? Give feedback.
-
Option A (for vote)Use pros: does the conversion to decimal representation using ~18 decimal places, which covers corner cases well, when the float in binary representation is an exact (or close) representation of the real number it is meant to represent/approximate cons: Java |
Beta Was this translation helpful? Give feedback.
-
Option B (for vote)Use pros: Java cons: not every float originates from a float literal; this method does the conversion to decimal representation using only ~9 decimal places, which covers corner cases badly, when the float in binary representation is an exact (or close) representation of the real number it is meant to represent/approximate |
Beta Was this translation helpful? Give feedback.
-
Indriya retains floating point precision when converting doubles, but not floats.
eg.
prints
0.003524
, butprints
0.0035239999294281006
.Beta Was this translation helpful? Give feedback.
All reactions