Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way of checking if a floating point is an integer

[There are a few questions on this but none of the answers are particularly definitive and several are out of date with the current C++ standard].

My research shows these are the principal methods used to check if a floating point value can be converted to an integral type T.

  1. if (f >= std::numeric_limits<T>::min() && f <= std::numeric_limits<T>::max() && f == (T)f))

  2. using std::fmod to extract the remainder and test equality to 0.

  3. using std::remainder and test equality to 0.

The first test assumes that a cast from f to a T instance is defined. Not true for std::int64_t to float, for example.

With C++11, which one is best? Is there a better way?

like image 560
P45 Imminent Avatar asked Oct 13 '14 13:10

P45 Imminent


People also ask

How do you check whether a number is integer or not?

To check if a String contains digit character which represent an integer, you can use Integer. parseInt() . To check if a double contains a value which can be an integer, you can use Math. floor() or Math.

Can floating point represent integers?

Real numbers are represented in C by the floating point types float, double, and long double. Just as the integer types can't represent all integers because they fit in a bounded number of bytes, so also the floating-point types can't represent all real numbers.

How is a floating point value different from an integer value?

Floating point numbers are different from integer numbers in that they contain fractional parts. Even if the number to the right of the decimal point is 0 (or decimal comma, if your locale uses commas instead of periods), it's still a fractional part of the number. Floating point numbers can be positive or negative.


2 Answers

Conclusion:

The answer is use std::trunc(f) == f the time difference is insignificant when comparing all these methods. Even if the specific IEEE unwinding code we write in the example below is technically twice is fast we are only talking about 1 nano second faster.

The maintenance costs in the long run though would be significantly higher. So use a solution that is easier to read and understand by the maintainer is better.

Time in microseconds to complete 12,000,000 operations on a random set of numbers:

  • IEEE breakdown:                                               18
  • std::trunc(f) == f                                  32
  • std::floor(val) - val == 0                35
  • ((uint64_t)f) - f) == 0.0                  38
  • std::fmod(val, 1.0) == 0                     87

The Working out of the conclusion.

A floating point number is two parts:

mantissa:      The data part of the value.
exponent:      a power to multiply it by.

such that:

   value =  mantissa * (2^exponent)

So the exponent is basically how many binary digits we are going to shift the "binary point" down the mantissa. A positive value shifts it right a negative value shifts it left. If all the digits to the right of the binary point are zero then we have an integer.

If we assume IEEE 754

We should note that this representation the value is normalized so that the most significant bit in the mantissa is shifted to be 1. Since this bit is always set it is not actually stored (the processor knows its there and compensates accordingly).

So:

If the exponent < 0 then you definitely do not have an integer as it can only be representing a fractional value. If the exponent >= <Number of bits In Mantissa> then there is definately no fractual part and it is an integer (though you may not be able to hold it in an int).

Otherwise we have to do some work. if the exponent >= 0 && exponent < <Number of bits In Mantissa> then you may be representing an integer if the mantissa is all zero in the bottom half (defined below).

Additional as part of the normalization 127 is added to the exponent (so that there are no negative values stored in the 8 bit exponent field).

#include <limits>
#include <iostream>
#include <cmath>

/*
 *  Bit  31      Sign
 *  Bits 30-23   Exponent
 *  Bits 22-00   Mantissa
 */
bool is_IEEE754_32BitFloat_AnInt(float val)
{
    // Put the value in an int so we can do bitwise operations.
    int  valAsInt = *reinterpret_cast<int*>(&val);

    // Remember to subtract 127 from the exponent (to get real value)
    int  exponent = ((valAsInt >> 23) & 0xFF) - 127;

    int bitsInFraction = 23 - exponent;
    int mask = exponent < 0
                    ? 0x7FFFFFFF
                    : exponent > 23
                         ? 0x00
                         : (1 << bitsInFraction) - 1;

    return !(valAsInt & mask);
}
/*
 *  Bit  63      Sign
 *  Bits 62-52   Exponent
 *  Bits 51-00   Mantissa
 */
bool is_IEEE754_64BitFloat_AnInt(double val)
{
    // Put the value in an long long so we can do bitwise operations.
    uint64_t  valAsInt = *reinterpret_cast<uint64_t*>(&val);

    // Remember to subtract 1023 from the exponent (to get real value)
    int  exponent = ((valAsInt >> 52) & 0x7FF) - 1023;

    int bitsInFraction = 52 - exponent;
    uint64_t mask = exponent < 0
                    ? 0x7FFFFFFFFFFFFFFFLL
                    : exponent > 52
                        ? 0x00
                        : (1LL << bitsInFraction) - 1;

    return !(valAsInt & mask);
}

bool is_Trunc_32BitFloat_AnInt(float val)
{
    return (std::trunc(val) - val == 0.0F);
}

bool is_Trunc_64BitFloat_AnInt(double val)
{
    return (std::trunc(val) - val == 0.0);
}

bool is_IntCast_64BitFloat_AnInt(double val)
{
    return (uint64_t(val) - val == 0.0);
}

template<typename T, bool isIEEE = std::numeric_limits<T>::is_iec559>
bool isInt(T f);

template<>
bool isInt<float, true>(float f) {return is_IEEE754_32BitFloat_AnInt(f);}

template<>
bool isInt<double, true>(double f) {return is_IEEE754_64BitFloat_AnInt(f);}

template<>
bool isInt<float, false>(float f) {return is_Trunc_64BitFloat_AnInt(f);}

template<>
bool isInt<double, false>(double f) {return is_Trunc_64BitFloat_AnInt(f);}

int main()
{
    double  x = 16;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 16.4;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 123.0;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 0.0;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 2.0;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 4.0;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 5.0;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 1.0;
    std::cout << x << "=> " << isInt(x) << "\n";
}

Results:

> ./a.out
16=> 1
16.4=> 0
123=> 1
0=> 1
2=> 1
4=> 1
5=> 1
1=> 1

Running Some Timing tests.

Test data was generated like this:

(for a in {1..3000000};do echo $RANDOM.$RANDOM;done ) > test.data
(for a in {1..3000000};do echo $RANDOM;done ) >> test.data
(for a in {1..3000000};do echo $RANDOM$RANDOM0000;done ) >> test.data
(for a in {1..3000000};do echo 0.$RANDOM;done ) >> test.data

Modified main() to run tests:

int main()
{
    // ORIGINAL CODE still here.
    // Added this trivial speed test.

    std::ifstream   testData("test.data");  // Generated a million random numbers
    std::vector<double>  test{std::istream_iterator<double>(testData), std::istream_iterator<double>()};
    std::cout << "Data Size: " << test.size() << "\n";
    int count1 = 0;
    int count2 = 0;
    int count3 = 0;

    auto start = std::chrono::system_clock::now();
    for(auto const& v: test)
    {   count1 += is_IEEE754_64BitFloat_AnInt(v);
    }
    auto p1 = std::chrono::system_clock::now();
    for(auto const& v: test)
    {   count2 += is_Trunc_64BitFloat_AnInt(v);
    }
    auto p2 = std::chrono::system_clock::now();
    for(auto const& v: test)
    {   count3 += is_IntCast_64BitFloat_AnInt(v);
    }

    auto end = std::chrono::system_clock::now();

    std::cout << "IEEE  " << count1 << " Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(p1 - start).count() << "\n";
    std::cout << "Trunc " << count2 << " Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(p2 - p1).count()    << "\n";
    std::cout << "Int Cast " << count3 << " Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - p2).count()   << "\n";    }

The tests show:

> ./a.out
16=> 1
16.4=> 0
123=> 1
0=> 1
2=> 1
4=> 1
5=> 1
1=> 1
Data Size: 12000000
IEEE  6000199 Time: 18
Trunc 6000199 Time: 32
Int Cast 6000199 Time: 38

The IEEE code (in this simple test) seem to beat the truncate method and generate the same result. BUT the amount of time is insignificant. Over 12 million calls we saw a difference in 14 milliseconds.

like image 192
Martin York Avatar answered Oct 16 '22 12:10

Martin York


Use std::fmod(f, 1.0) == 0.0 where f is either a float, double, or long double. If you're worried about spurious effects of unwanted floating point promotions when using floats, then use either 1.0f or the more comprehensive

std::fmod(f, static_cast<decltype(f)>(1.0)) == 0.0

which will force, obviously at compile time, the correct overload to be called. The return value of std::fmod(f, ...) will be in the range [0, 1) and it's perfectly safe to compare to 0.0 to complete your integer check.

If it turns out that f is an integer, then make sure it's within the permitted range of your chosen type before attempting a cast: else you risk invoking undefined behaviour. I see that you're already familiar with std::numeric_limits which can help you here.

My reservations against using std::remainder are possibly (i) my being a Luddite and (ii) it not being available in some compilers partially implementing the C++11 standard, such as MSVC12. I don't like solutions involving casts since the notation hides that reasonably expensive operation and you need to check in advance for safety. If you must adopt your first choice, at least replace the C-style cast with static_cast<T>(f);

like image 11
Bathsheba Avatar answered Oct 16 '22 13:10

Bathsheba