Reputation: 11300
I'm connecting to an external system that provides me with a stream of double
financial prices. I know that the values I receive are meant to be decimal values, and that therefore BigDecimal
would have been a better choice, but the provided client library forces me to use double
instead.
My goal is to build a BigDecimal
from the provided double
values. The simple solution is to just use BigDecimal.valueOf(double)
. However, as is a common problem when working with double
, the values that I receive look like 0.12000000001
instead of 0.12
, or 1.15999999998
instead of 1.16
. I could do some rounding by using BigDecimal.setScale()
, but the problem with that is that I don't know the exact intended precision of the value.
How can I reconstruct the intended decimal value?
My solution so far relies on the fact that the value I receive is always within 1 or 2 ULP of the intended decimal value. So I just try converting the value I receive as well as the values 1-2 ulp up/down to String
, and create a BigDecimal from the shortest of these strings.
private static BigDecimal shortestBigDecimal(double inputDouble) {
double ulp = Math.ulp(inputDouble);
String center = Double.toString(inputDouble);
String high = Double.toString(inputDouble + ulp);
String low = Double.toString(inputDouble - ulp);
String high2 = Double.toString(inputDouble + ulp + ulp);
String low2 = Double.toString(inputDouble - ulp - ulp);
String shortest = minLength(minLength(minLength(minLength(low2, low), center), high), high2);
return new BigDecimal(shortest);
}
private static String minLength(String a, String b) {
return a.length() < b.length() ? a : b;
}
This code passes all of my test cases, but it creates a lot of temporary String
s.
Is there a more efficient way to do this?
Upvotes: 3
Views: 122
Reputation: 26185
I have tested an alternative approach that involves no strings at all. Instead, it searches for the lowest scale factor BigDecimal that results from rounding of the original value and is in the range:
import java.math.BigDecimal;
import java.math.RoundingMode;
public strictfp class Test {
public static void main(String[] args) {
testit(0.1);
testit(1.15999999998);
testit(0.12000000000000001);
testit(100.00001);
testit(0.3);
double ulp = Math.ulp(0.3);
testit(0.3 + ulp);
testit(0.3 + 2*ulp);
testit(0.3 + 3*ulp);
}
private static void testit(double d) {
System.out.println(d + " " + shortestBigDecimal(d) + " " + alternative(d));
}
private static BigDecimal shortestBigDecimal(double inputDouble) {
double ulp = Math.ulp(inputDouble);
String center = Double.toString(inputDouble);
String high = Double.toString(inputDouble + ulp);
String low = Double.toString(inputDouble - ulp);
String high2 = Double.toString(inputDouble + ulp + ulp);
String low2 = Double.toString(inputDouble - ulp - ulp);
String shortest = minLength(minLength(minLength(minLength(low2, low), center), high), high2);
return new BigDecimal(shortest);
}
private static String minLength(String a, String b) {
return a.length() < b.length() ? a : b;
}
private static BigDecimal alternative(double d) {
BigDecimal original = new BigDecimal(d);
BigDecimal low = new BigDecimal(Math.nextDown(Math.nextDown(d)));
BigDecimal high = new BigDecimal(Math.nextUp(Math.nextUp(d)));
for(int scale = 0; scale <= original.scale(); scale++) {
BigDecimal candidate = original.setScale(scale, RoundingMode.HALF_EVEN);
if(candidate.compareTo(low) >= 0 && candidate.compareTo(high) <= 0) {
return candidate;
}
}
return original;
}
}
Output:
0.1 0.1 0.1
1.15999999998 1.15999999998 1.15999999998
0.12000000000000001 0.12 0.12
100.00001 100.00001 100.00001
0.3 0.3 0.3
0.30000000000000004 0.3 0.3
0.3000000000000001 0.3 0.3
0.30000000000000016 0.3000000000000002 0.3000000000000002
Upvotes: 1
Reputation: 19911
You can use the overloaded constructor of BigDecimal
where you can provide a MathContext
. Using DECIMAL32
we have a rather low precision (7
decimal places), meaning everything out of the range is rounded using RoundingMode.HALF_EVEN
. By using the method stripTrailingZeros
, we can get only the significant digits and get rid of trailing 0
.
public static BigDecimal shortestBigDecimal(double inputValue) {
return
new BigDecimal(
inputValue,
MathContext.DECIMAL32
)
.stripTrailingZeros();
}
Using this prints:
System.out.println(shortestBigDecimal(1.15999999998)); // 1.16
System.out.println(shortestBigDecimal(0.12000000000001)); // 0.12
Upvotes: 1