How to correctly multiply a float with an int and get a result only influenced by significant digits?

12,875

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.

Share:
12,875
Johannes Schaub - litb
Author by

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, 2022

Comments

  • Johannes Schaub - litb
    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 float

    int64_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 is 14.2f. Currently I solve this issue by computing the significant number of digits after the radix point

    const 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
    Admin over 11 years
    +1 for solutions directly to the problem. wish I could do +2.
  • Johannes Schaub - litb
    Johannes Schaub - litb over 11 years
    i do use an int throughout my code base but the float comes from user input :)
  • Mark Ransom
    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
    Johannes Schaub - litb over 11 years
    @MarkRansom can you please give an example of where it goes wrong?
  • Olaf Dietsche
    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
    Olaf Dietsche over 11 years
    I 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
    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
    aka.nice over 11 years
    However, 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
    Anthony Burleigh over 11 years
    I 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 requires cmath.
  • Olaf Dietsche
    Olaf Dietsche over 11 years
    @AnthonyBurleigh Confirmed! I just tried this with s=14.2f and decimals=6 and it gave 14200000000 as well.