4

I have a float-based storage of decimal by their nature numbers. The precision of float is fine for my needs. Now I want to perform some more precise calculations with these numbers using double.

An example:

float f = 0.1f;
double d = f; //d = 0.10000000149011612d
// but I want some code that will convert 0.1f to 0.1d;

I know very well that 0.1f != 0.1d. This question is not about precise decimal calculations. In other words...

Let's say I work with an API that returns float numbers for decimal MSFT stock prices. Believe or not, this API exists:

interface Stock {
    float[] getDayPrices();
    int[] getDayVolumesInHundreds();
}

It is known that the price of a MSFT share is a decimal number with no more than 5 digits, e.g. 31.455, 50.12, 45.888. Obviously the API does not work with BigDecimal because it would be a big overhead for the purpose to just pass the price.

Let's also say I want to calculate a weighted average of these prices with double precision:

float[] prices = msft.getDayPrices();
int[] volumes = msft.getDayVolumesInHundreds();
double priceVolumeSum = 0.0;
long volumeSum = 0;
for (int i = 0; i < prices.length; i++) {
    double doublePrice = decimalFloatToDouble(prices[i]);
    priceVolumeSum += doublePrice * volumes[i];
    volumeSum += volumes[i];
}
System.out.println(priceVolumeSum / volumeSum);

I need a performant implemetation of decimalFloatToDouble.

Now I use the following code, but I need something more clever:

double decimalFloatToDouble(float f) {
    return Double.parseDouble(Float.toString(f));
}
E_net4
  • 27,810
  • 13
  • 101
  • 139
Yahor
  • 639
  • 8
  • 16
  • 3
    You can't get either `0.1f` or `0.1d`. See why in [this paper](http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html). – Keppil Jun 15 '14 at 19:41
  • 2
    Use a `BigDecimal` if you need precision. – shmosel Jun 15 '14 at 19:41
  • I do not need exact decimal precision. I just need a way to convert 0.1f to 0.1d, 1.1f to 1.1d, 1.234f to 1.234d and so on. – Yahor Jun 15 '14 at 22:02
  • @Keppil: Where does it state that? – tmyklebu Jun 15 '14 at 23:19
  • 2
    As I understand the problem, you have the closest float to a number that is known to have a decimal representation with no more than 5 significant digits. You want the double that is closest to that decimal representation. I think you are going to have to convert to decimal at some point during the process, so that you can get the decimal rounding you want. Being clever is, if anything, a negative for code quality. Can you explain what you don't like about your current code? – Patricia Shanahan Jun 16 '14 at 02:39
  • If you ask a question that gets misunderstood but already has answers, it is better to ask a new, perhaps better phrased question than to edit the old one and make the existing answers wrong for the new question. I, for one, have better uses for my time than perpetually scanning the questions I have answered in the past for radical changes that mean that my answers do not make sense any more. – Pascal Cuoq Jun 16 '14 at 07:57
  • @PatriciaShanahan you understand correct. I would like to have a faster solution. For example `Math.round(f * 100) / 100.0` is about 10 times faster than `Double.parseDouble(Float.toString(f))`. But I need something more general. – Yahor Jun 16 '14 at 09:22

3 Answers3

3

EDIT: this answer corresponds to the question as initially phrased.

When you convert 0.1f to double, you obtain the same number, the imprecise representation of the rational 1/10 (which cannot be represented in binary at any precision) in single-precision. The only thing that changes is the behavior of the printing function. The digits that you see, 0.10000000149011612, were already there in the float variable f. They simply were not printed because these digits aren't printed when printing a float.

Ignore these digits and compute with double as you wish. The problem is not in the conversion, it is in the printing function.

Pascal Cuoq
  • 79,187
  • 7
  • 161
  • 281
  • 2
    The actual value of 0.1f is 0.100000001490116119384765625. – Patricia Shanahan Jun 15 '14 at 21:12
  • @PatriciaShanahan Isn't that a bit too many digits for a `float`? – awksp Jun 15 '14 at 21:18
  • @user3580294 There are a lot of digits because this is a base-10 representation of a number stored compactly in base-2. The representation of `0.1f` in hexadecimal, which better matches the storage format, is `0x1.99999ap-4`. – Pascal Cuoq Jun 15 '14 at 21:19
  • @PascalCuoq Oh, I see... Why so many digits compared to the Java printout, and is there a way to force Java to display a floating-point number to "full precision" like that? Edit: saw your edit, I *think* I might see what's going on... – awksp Jun 15 '14 at 21:22
  • @user3580294 Actually, I would be interested in the answer to this question, as I recently discovered that Java approaches floating-point-to-decimal conversion differently than I would have expected (see comments on http://stackoverflow.com/a/24121342/139746 ). I invite you to ask it as a StackOverflow question. – Pascal Cuoq Jun 15 '14 at 21:28
  • Read through that, and I feel a loooot is going over my head... More than I'm comfortable with to ask a good SO question... Might have to do some looking around, especially since I'm not too familiar with non-Java languages. @PatriciaShanahan: How did you get the "full" value of `0.1f`? – awksp Jun 15 '14 at 22:07
  • @pascal-cuoq what if I **know** that my float is actually a decimal number with several significant digits? I need to get back the precision error that I get while converting it to float. – Yahor Jun 15 '14 at 22:26
  • @Yahor the simple answer is use BigDecimal if you need to not have precision errors – mmmmmm Jun 15 '14 at 23:36
  • 3
    @user3580294 `System.out.println(new BigDecimal(0.1f));` The trick is a chain of operations each of which is exact: conversion of float to double, the BigDecimal(double) constructor, and BigDecimal's toString(). You will often be warned against using the BigDecimal(double) constructor, precisely because it gives you the value of the double, including rounding error. – Patricia Shanahan Jun 16 '14 at 02:34
  • @PatriciaShanahan Ah, I see. That makes a ton of sense. Thanks! – awksp Jun 16 '14 at 04:18
  • @PascalCuoq Digging through the chain of calls that `System.out.println(double d)` makes, and looks like Java dives deep into the nitty-gritty of how floating-point numbers are represented. If I ever understand this, it'd likely take a long time, and not sure if a question would get asked here anytime soon... – awksp Jun 16 '14 at 07:26
  • @user3580294 It's okay, Patricia Shanahan already answered your question (that I was also wondering about). – Pascal Cuoq Jun 16 '14 at 07:50
1

As I understand you, you know that the float is within one float-ulp of an integer number of hundredths, and you know that you're well inside the range where no two integer numbers of hundredths map to the same float. So the information isn't gone at all; you just need to figure out which integer you had.

To get two decimal places, you can multiply by 100, rint/Math.round the result, and multiply by 0.01 to get a close-by double as you wanted. (To get the closest, divide by 100.0 instead.) But I suspect you knew this already and are looking for something that goes a little faster. Try ((9007199254740992 + 100.0 * x) - 9007199254740992) * 0.01 and don't mess with the parentheses. Maybe strictfp that hack for good measure.

You said five significant figures, and apparently your question isn't limited to MSFT share prices. Up until doubles can't represent powers of 10 exactly, this isn't too bad. (And maybe this works beyond that threshold too.) The exponent field of a float narrows down the needed power of ten down to two things, and there are 256 possibilities. (Except in the case of subnormals.) Getting the right power of ten just needs a conditional, and the rounding trick is straightforward enough.

All of this is all going to be a mess, and I'd recommend you stick with the toString approach for all the weird cases.

tmyklebu
  • 13,915
  • 3
  • 28
  • 57
  • 1
    actually the number of digits after decimal point is not exactly 2 like `31.455`. So can't just multiply by 100, you need to get the 5 most significant digits – phuclv Jun 16 '14 at 02:23
  • @tmyklebu neither rint/Math.rount, nor your formula works properly for every case possible: ((9007199254740992l + 100.0 * 6.01f) - 9007199254740992l) * 0.01 => 6.0200000000000005d – Yahor Jun 16 '14 at 08:36
  • @tmyklebu Math.round((double)41.3f * 100) * 0.01 => 41.300000000000004 != 41.3d – Yahor Jun 16 '14 at 08:42
  • @tmyklebu however `Math.round((double)41.3f * 100) / 100.0` works properly, but I need something more general than just 2-digit fixed-point – Yahor Jun 16 '14 at 09:24
  • Yeah, sorry. I was off by a factor of two there. Try `4503599627370496.0` instead. Regarding your second example, indeed that's true. If you want correct rounding with that trick, you need to replace the multiplication by `0.01` by a division by `100.0`. If you want five significant figures over the whole range of `float`, you might want to `switch` over the thing's exponent, in some branches do a test against (the closest `float` to) a power of ten, and replace `100.0` with the appropriate power of ten in the above. As you can tell, this is icky, but it'll probably go faster. – tmyklebu Jun 16 '14 at 15:57
  • Thanks! These are awesome tricks! Especially to add/substract 4503599627370496.0=`0x1p52` it's a LOT faster than `Math.round()`, (though one should be aware of negative numbers). My measures show me >100x speedup in comparison to the initial `toString/parseDouble`! – Yahor Jun 17 '14 at 17:54
1

If your goal is to have a double whose canonical representation will match the canonical representation of a float converting the float to string and converting the result back to double would probably be the most accurate way of achieving that result, at least when it's possible (I don't know for certain whether Java's double-to-string logic would guarantee that there won't be a pair of consecutive double values which report themselves as just above and just-below a number with five significant figures).

If your goal is to round to five significant figures a value which is known to have been rounded to five significant figures while in float form, I would suggest that the simplest approach is probably to simply round to five significant figures. If your magnitude of your numbers will be roughly within the range 1E+/-12, start by finding the smallest power of ten which is smaller than your number, multiply that by 100,000, multiply your number by that, round to the nearest unit, and divide by that power of ten. Because division is often much slower than multiplication, if performance is critical, you might keep a table with powers of ten and their reciprocals. To avoid the possibility of rounding errors, your table should store for each power of then the closest power-of-two double to its reciprocal, and then the closest double to the difference between the first double and the actual reciprocal. Thus, the reciprocal of 100 would be stored as 0.0078125 + 0.0021875; the value n/100 would be computed as n*0.0078125 + n*0.0021875. The first term would never have any round-off error (multiplying by a power of two), and the second value would have precision beyond that needed for the final result, so the final result should thus be rounded accurately.

supercat
  • 77,689
  • 9
  • 166
  • 211