如何检查浮点数是否可以精确表示为整数

18

我正在寻找一种相对高效的方法来确定一个浮点数(double)是否可以被整数数据类型(long, 64位)准确地表示。

我的初步想法是检查指数是否为0(或更确切地说是127)。但这不会奏效,因为2.0的e=1 m=1 ...

所以基本上,我陷入了困境。我有一种感觉,可以用位掩码来实现,但目前还没有头绪。

那么,如何检查一个double能否被准确地表示为long呢?

谢谢


你可以提取尾数和指数部分,然后检查(在从尾数左侧删除_exponent + 1_位数字后)所有其他位是否为0(这意味着没有小数部分)。您需要以不同的方式处理负指数(任何具有负指数的非零“double”都将是分数)。 - Seth Carnegie
@SethCarnegie 您的评论可以作为答案进行点赞。 - Sergey Kalinichenko
@dasblinkenlight不用了,我认为Mysticial的回答更好。 - Seth Carnegie
C/C++标准规定了一个预处理器宏/已知值,即DBL_MANT_DIG,表示double的尾数中数字的数量。标准中“数字”的单位是FLT_RADIX;对于“普通”的IEEE754 double,基数为2,尾数有53个这样的“数字”(也称为...位)。因此,在完全精度下最具代表性的整数应为(FLT_RADIX << (DBL_MANT_DIG-1)) - 1 - FrankH.
可能是double类型可以存储的最大整数的重复问题。 - FrankH.
6个回答

11

我想我已经找到了一种以符合标准的方式将一个 double 转换为整数的方法(这不是问题的真正问题,但这可以很大程度上帮助解决问题)。首先,我们需要看看为什么显而易见的代码是错误的

// INCORRECT CODE
uint64_t double_to_uint64 (double x)
{
    if (x < 0.0) {
        return 0;
    }
    if (x > UINT64_MAX) {
        return UINT64_MAX;
    }
    return x;
}
问题在于第二个比较中,UINT64_MAX被隐式转换为double。C标准没有精确规定这种转换的工作方式,只规定它要四舍五入到可表示的值。这意味着,即使第二个比较在数学上应该是真的(当UINT64_MAX四舍五入,而'x'在UINT64_MAX(double)UINT64_MAX之间时可能发生),它也可能是假的。因此,在这种边缘情况下,将double转换为uint64_t可能会导致未定义的行为。
令人惊讶的是,解决方案非常简单。考虑到虽然UINT64_MAXdouble中可能无法完全表示,但是UINT64_MAX+1作为二的幂(并且不太大),肯定是可以表示的。因此,如果我们先将输入舍入为整数,则比较x > UINT64_MAX等同于x >= UINT64_MAX+1,除了可能在加法中溢出。我们可以使用ldexp而不是将一个加到UINT64_MAX来修复溢出。也就是说,以下代码应该是正确的。
/* Input: a double 'x', which must not be NaN.
 * Output: If 'x' is lesser than zero, then zero;
 *         otherwise, if 'x' is greater than UINT64_MAX, then UINT64_MAX;
 *         otherwise, 'x', rounded down to an integer.
 */
uint64_t double_to_uint64 (double x)
{
    assert(!isnan(x));
    double y = floor(x);
    if (y < 0.0) {
        return 0;
    }
    if (y >= ldexp(1.0, 64)) {
        return UINT64_MAX;
    }
    return y;
}

现在回到你的问题:在 uint64_t 中,x 是否能够被精确表示?只有在它既没有舍入也没有夹紧的情况下才可以。

/* Input: a double 'x', which must not be NaN.
 * Output: If 'x' is exactly representable in an uint64_t,
 *         then 1, otherwise 0.
 */
int double_representable_in_uint64 (double x)
{
    assert(!isnan(x));
    return (floor(x) == x && x >= 0.0 && x < ldexp(1.0, 64));
}

同样的算法可以用于不同大小的整数,也可以通过微小修改用于带符号整数。以下代码对 uint32_tuint64_t 版本进行了一些非常基本的测试(只能可能捕获到假阳性),但也适合手动检查边缘情况。

#include <inttypes.h>
#include <math.h>
#include <limits.h>
#include <assert.h>
#include <stdio.h>

uint32_t double_to_uint32 (double x)
{
    assert(!isnan(x));
    double y = floor(x);
    if (y < 0.0) {
        return 0;
    }
    if (y >= ldexp(1.0, 32)) {
        return UINT32_MAX;
    }
    return y;
}

uint64_t double_to_uint64 (double x)
{
    assert(!isnan(x));
    double y = floor(x);
    if (y < 0.0) {
        return 0;
    }
    if (y >= ldexp(1.0, 64)) {
        return UINT64_MAX;
    }
    return y;
}

int double_representable_in_uint32 (double x)
{
    assert(!isnan(x));
    return (floor(x) == x && x >= 0.0 && x < ldexp(1.0, 32));
}

int double_representable_in_uint64 (double x)
{
    assert(!isnan(x));
    return (floor(x) == x && x >= 0.0 && x < ldexp(1.0, 64));
}

int main ()
{
    {
        printf("Testing 32-bit\n");
        for (double x = 4294967295.999990; x < 4294967296.000017; x = nextafter(x, INFINITY)) {
            uint32_t y = double_to_uint32(x);
            int representable = double_representable_in_uint32(x);
            printf("%f -> %" PRIu32 " representable=%d\n", x, y, representable);
            assert(!representable || (double)(uint32_t)x == x);
        }
    }
    {
        printf("Testing 64-bit\n");
        double x = ldexp(1.0, 64) - 40000.0;
        for (double x = 18446744073709510656.0; x < 18446744073709629440.0; x = nextafter(x, INFINITY)) {
            uint64_t y = double_to_uint64(x);
            int representable = double_representable_in_uint64(x);
            printf("%f -> %" PRIu64 " representable=%d\n", x, y, representable);
            assert(!representable || (double)(uint64_t)x == x);
        }
    }
}

10

以下方法在大多数情况下都可以使用。如果您输入 NaNINF 或非常大的(溢出)数字,我不确定它是否会失效,也不确定如何失效...
(尽管我认为它们都将返回 false - 无法准确表示。)

您可以:

  1. 将其转换为整数。
  2. 再将其转换回浮点数。
  3. 与原始值进行比较。

类似这样:

double val = ... ;  //  Value

if ((double)(long long)val == val){
    //  Exactly representable
}

floor()ceil()同样适用(尽管如果值超过整数范围可能会失败):

floor(val) == val
ceil(val) == val

这里是一个混乱的位掩码解决方案:
该解决方案使用联合类型转换,假设IEEE双精度。 联合类型转换仅在C99 TR2及更高版本中有效。

int representable(double x){
    //  Handle corner cases:
    if (x == 0)
      return 1;

    //  -2^63 is representable as a signed 64-bit integer, but +2^63 is not.
    if (x == -9223372036854775808.)
      return 1;

    //  Warning: Union type-punning is only valid in C99 TR2 or later.
    union{
        double f;
        uint64_t i;
    } val;

    val.f = x;

    uint64_t exp = val.i & 0x7ff0000000000000ull;
    uint64_t man = val.i & 0x000fffffffffffffull;
    man |= 0x0010000000000000ull;  //  Implicit leading 1-bit.

    int shift = (exp >> 52) - 1075;
    //  Out of range
    if (shift < -52 || shift > 10)
        return 0;

    //  Test mantissa
    if (shift < 0){
        shift = -shift;
        return ((man >> shift) << shift) == man;
    }else{
        return ((man << shift) >> shift) == man;
    }
}

1
转换是否有可能被优化掉?还是不可能发生? - Dan Fego
1
关于编辑:为什么不使用ieee754.h中包含的标准ieee754_float联合来获取尾数和指数,而要使用非标准联合转换呢? - ircmaxell
1
因为我不知道那个存在 :) 即使在回答问题时,你还是能学到东西...编辑:MSVC没有那个头文件... - Mysticial
1
ieee754.h 是由哪个标准规定的? - R.. GitHub STOP HELPING ICE
1
这是不正确的。如果整数部分无法在整数类型中表示,浮点数转换为整数是未定义行为。请参阅C99标准6.3.1.4p1。 - Ambroz Bizjak
显示剩余14条评论

4

您可以使用modf函数将浮点数拆分为整数和小数部分。modf函数在标准C库中。

#include <math.h>
#include <limits.h>   

double val = ...
double i;
long l;

/* check if fractional part is 0 */
if (modf(val, &i) == 0.0) {
    /* val is an integer. check if it can be stored in a long */
    if (val >= LONG_MIN && val <= LONG_MAX) {
        /* can be exactly represented by a long */
        l = val;
    }
}

1
如何检查浮点数是否可以精确地表示为整数? 我正在寻找一种合理高效的方法,确定浮点值double是否可以被整数数据类型long(64位)精确表示。 需要进行范围(LONG_MIN,LONG_MAX)和分数(frexp())测试。还需要注意非数字情况。
通常的想法是像这样测试:(double)(long)x == x,但要避免直接使用它。当x超出范围时,(long)x是未定义行为(UB)。 (long)x的有效转换范围为LONG_MIN - 1 < x < LONG_MAX + 1,因为在转换过程中代码会丢弃x的任何小数部分。因此,代码需要使用FP math来测试x是否在范围内。
#include <limits.h>
#include <stdbool.h>
#define DBL_LONG_MAXP1 (2.0*(LONG_MAX/2+1)) 
#define DBL_LONG_MINM1 (2.0*(LONG_MIN/2-1)) 

bool double_to_long_exact_possible(double x) {
  if (x < DBL_LONG_MAXP1) {
    double whole_number_part;
    if (frexp(x, &whole_number_part) != 0.0) {
      return false;  // Fractional part exist.
    }
    #if -LONG_MAX == LONG_MIN
    // rare non-2's complement machine 
    return x > DBL_LONG_MINM1;
    #else
    return x - LONG_MIN > -1.0;
    #endif 
  }
  return false;  // Too large or NaN
}

0

任何IEEE浮点数的doublefloat值,其大小在2^52或2^23及以上,将成为整数。将2^52或2^23加到其大小小于该值的正数上将导致其四舍五入为整数。减去添加的值将产生一个整数,如果原始值是整数,则等于原始值。请注意,此算法对于某些大于2^52的数字将失败,但对于那么大的数字不需要使用此算法。


-1

你能使用模运算符来检查双精度数是否可以被1整除吗?还是我完全误解了这个问题?

double val = ... ;  //  Value

if(val % 1 == 0) {
    // Val is evenly divisible by 1 and is therefore a whole number
}

2
R % 1 == 0 总是成立。 - user529649
2
double类型没有operator%运算符,对吧? - Seth Carnegie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接