Extracting a specific digit from a float w/o floating-point implementation rounding issues

29 views (last 30 days)
I have a set of base-10 nonnegative floats with either three or four digits to the right of the decimal point. Can anyone suggest a reliable way to extract the very rightmost digit (that is, the 1E-4 place) without running into occasional floating-point issues?
My first attempt, lastDigit = round(10*rem(num*1000,1)), works most of the time:
>> num = 130404809.288; lastDigit = round(10*rem(num*1000,1)) % fourth digit right of the decimal point is zero
lastDigit =
0
>> num = 130404809.2882; lastDigit = round(10*rem(num*1000,1))
lastDigit =
2
But every once in a while, the finite nature of internal binary representation produces undesired results:
>> num = 136147309.434; lastDigit = round(10*rem(num*1000,1)) % should return zero
lastDigit =
10
I realize this is a common subtlety, and that my challenge is really not so much math as string parsing—but this is the data I have to work with.
Naively, I tried various combinations of floor, round, rem, etc. — but I haven't been able to find a clean way to extract that fourth digit that doesn't run into cases like the one above every so often. Can anyone set me straight?
  3 Comments
Walter Roberson
Walter Roberson on 17 Jul 2020
The correct last digit is not 1. The number is really 136147309.4339999854564666748046875 . You should floor() instead of round()
Steven Lord
Steven Lord on 17 Jul 2020
What are you doing that requires this digit? Perhaps there's an alternate way to achieve your ultimate goal that is more reliable.

Sign in to comment.

Accepted Answer

Stephen23
Stephen23 on 17 Jul 2020
Edited: Stephen23 on 17 Jul 2020
Some people will incorrectly tell you that this is not possible, but in reality there is a simple and efficient solution:
>> N = 130404809.288;
>> mod(round(10000*N),10)
ans = 0
>> N = 130404809.2882;
>> mod(round(10000*N),10)
ans = 2
>> N = 136147309.434;
>> mod(round(10000*N),10)
ans = 0
This relies on your statement that the values have "with either three or four digits to the right of the decimal point", i.e. that the fifth digit is zero (the sixth and any further digits, including any floating point error, are totally irrelevant). This means that after multiplying by 10000 (NOT by 1000 as you tried) a simple round will remove all floating point error from the number:
xyz.abcd0err % input
xyzabcd.0err % *10000
xyzabcd % round
d % mod 10
You were almost there, just off by a factor of ten.

More Answers (2)

Image Analyst
Image Analyst on 17 Jul 2020
Use sprintf() to round the number to a string with 4 digits, then extract the 4th digit after the decimal point and convert it to a number:
num = 130404809.288;
str = sprintf('%.4f', num)
decimalLocation = strfind(str, '.')
lastDigit = str2double(str(decimalLocation + 4))
num = 130404809.2882;
str = sprintf('%.4f', num)
decimalLocation = strfind(str, '.')
lastDigit = str2double(str(decimalLocation + 4))
num = 136147309.434;
str = sprintf('%.4f', num)
decimalLocation = strfind(str, '.')
lastDigit = str2double(str(decimalLocation + 4))

AMM
AMM on 17 Jul 2020
Edited: AMM on 17 Jul 2020
All,
Thank you for your thoughtful responses. Quick comments as follows:
@John: My apologies—in trying to pose the question as concisely as possible, I left out what I now realize is an essential detail. My inputs are floats that always have exactly three digits to the right of the decimal point. (They represent distances, in meters, and are specified to the millimeter by the interface standard.) That fourth digit is a hack—it's actually a status flag with possible values {0, 1, 2, ..., 7} and not part of the measurements per se. It just appears in the next column over in the source file (no space separator). Unfortunately, cleanly stripping out that digit from the raw data would require more string concatenation wizardry than I was able to come up with initially—hence this hack. (This is also why I mentioned string parsing.)
@Walter, Steven: The three digits to the right of the decimal point represent continuous (analog) values, but the fourth is actually a (one-digit) representation of a three-bit binary number: it's exact by construction. Of course you are correct that MATLAB's internal representation—the ".4339999..." in your example—is the closest one can do given the way floats are represented with the number of bits used "under the hood." A more precise way to pose my question would have been, how can I strip off that fourth digit, regardless of the limitations of the binary representation.
@Stephen: Your restatement of my (imprecisely worded) question is nearly spot on, except that I have no fifth digit: it's not zero, it's just not there—so I suppose I can simply treat it as zero. Regardless, your suggestion, lastDigit = mod(round(num*10000),10), appears to work well.
@ImageAnalyst: Your suggestion is roughly what I had in mind when I mentioned string parsing. It won't quite work as-is because I don't just have a single float; I have a large array (roughly 100x5000), so I think I'd need to reshape the output of the sprintf. But I think I can make it work without too much trouble.
Thanks, all, for your incredibly quick and thoughtful replies. I really appreciate it!
  3 Comments
Image Analyst
Image Analyst on 17 Jul 2020
AMM - half a million elements is far from a large array. Processing them all shouldn't take much time. If you use sprintf() like I did, .4339999 will show up as .4340 and give you zero which is what I think you want.
Anyway, why do you need to do this quirky thing. What's the use case?

Sign in to comment.

Products


Release

R2020a

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!