How to correctly multiply a float with an int and get a result only influenced by significant digits?
Solution 1
If you want to round to one decimal place for example
#include <iostream>
int main()
{
float f = 14.2f;
long long n = f * 1000000000LL;
std::cout << "float: " << n << '\n';
n = (f + 0.05) * 10;
n *= 100000000LL;
std::cout << "rounded: " << n << '\n';
return 0;
}
With two decimal places it's (f + 0.005) * 100
, ...,
and with six decimal places
n = ((long long)((f + 0.0000005) * 1000000)) * 1000LL;
If you want to consider significant digits (all digits), you must first take log10(f)
and then adjust rounding the decimal places.
But as @MarkB already said, if you use int64_t
in the first place, you don't need this at all.
Solution 2
By storing the value in a float
the damage has already been done, you've lost the original number whatever it was. You can guess at a value that might have been intended and then round, or if you're simply trying to display a value for the user you can round to a lower number of decimal places.
Instead, you can solve all these problems by using your fixed-point int64_t
representation throughout your entire code base, never converting to/from float
and avoiding throwing away precision during each conversion.
Johannes Schaub - litb
I'm a C++ programmer, interested in linux, compilers and toolchains and generally the embedded software stack. Standardese answers: How does boost::is_base_of work? Injected class name and constructor lookup weirdness What happens when op[] and op T* are both there. FAQ answers: Where to put "template" and "typename" on dependent names (now also covers C++11) Undefined behavior and sequence points Favourite answers: Plain new, new[], delete and delete[] in a nutshell. Assertion failure on T(a) but allowing T t(a) - forbids (accidental) temporaries. Explicitly instantiating a typedef to a class type Doing RAII the lazy way. C for-each over arrays. inline and the ODR in C++, and inline in C99
Updated on June 17, 2022Comments
-
Johannes Schaub - litb almost 2 years
I have code that converts between a float (representing a second) and an int64 (representing a nanosecond), taking
6
decimal places from the floatint64_t nanos = f * 1000000000LL;
However many decimal values stored in floats cannot be represented exactly in the binary float, so I get results like
14199999488
when my float is14.2f
. Currently I solve this issue by computing the significant number of digits after the radix pointconst float logOfSecs = std::log10(f); int precommaPlaces = 0; if(logOfSecs > 0) { precommaPlaces = std::ceil(logOfSecs); } int postcommaPlaces = 7 - precommaPlaces; if(postcommaPlaces < 0) { postcommaPlaces = 0; }
And then printing the float into a string to let Qt round the float correctly. Then I parse the string into a pre and post comma integer and multiple them with integer arithmetic.
const QString valueStr = QString::number(f, 'f', postcommaPlaces); qint64 nanos = 0; nanos += valueStr.section(".", 0, 0).toLongLong() * 1000000000LL; if(postcommaPlaces) { nanos += valueStr.section(".", 1).toLongLong() * std::pow(10.0, 9 - postcommaPlaces); }
This works fine, but I was wondering whether there is a better, perhaps faster way to do this?
-
Admin over 11 years+1 for solutions directly to the problem. wish I could do +2.
-
Johannes Schaub - litb over 11 yearsi do use an int throughout my code base but the float comes from user input :)
-
Mark Ransom over 11 years@JohannesSchaub-litb That's not rounding total digits, it's rounding digits after the decimal place. Depending on the magnitude of the original number and the number of digits you're asking for, it may still do the wrong thing.
-
Johannes Schaub - litb over 11 years@MarkRansom can you please give an example of where it goes wrong?
-
Olaf Dietsche over 11 years@JohannesSchaub-litb If you have 14.199999488 and you round to six decimal places, it will consider 14.199999. If you want to round six total digits, you must consider digits before and after the decimal point. This would be 14.1999. It depends on what you're interested in.
-
Olaf Dietsche over 11 yearsI think, what I got wrong, is the distinction between "significant digit" (all digits) and "decimal place" (digits after decimal point). I updated my answer accordingly.
-
Mark Ransom over 11 years@JohannesSchaub-litb, the problem isn't as severe as I first thought because the
f+0.05
expression converts to double. It's still possible though: ideone.com/D0dYGp -
aka.nice over 11 yearsHowever, if the float indeed results from a scanf, it's not too late, some overkill algorithm could recover a correctly rounded decimal fraction fitting input, unless user entered too many digits... See my answer.
-
Anthony Burleigh over 11 yearsI believe that this could also be accomplished maybe a bit more clearly with
long long ns = llround(s * pow(10, decimals)) * pow(10, 9 - decimals);
. Only requirescmath
. -
Olaf Dietsche over 11 years@AnthonyBurleigh Confirmed! I just tried this with
s=14.2f
anddecimals=6
and it gave14200000000
as well.