获取下一个最小的双精度浮点数。

29

在单元测试中,我需要测试一些边界条件。一个方法接受一个 System.Double 参数。

有没有办法获取 比当前值略小 的双精度浮点数?(即将尾数减少1个单位值)?

我考虑使用 Double.Epsilon,但是这种方法不可靠,因为它只是从零开始的最小增量,所以对于更大的值(即 9999999999 - Double.Epsilon == 9999999999),它无法起作用。

那么,需要什么算法或代码才能使结果变为:

NextSmallest(Double d) < d

...总是正确的。


如果你只是除以10呢? - Hogan
4
我认为你的问题已经在这里得到了回答:https://dev59.com/MHM_5IYBdhLWcg3wvV9w#2283565。 - p.s.w.g
3个回答

24

如果您的数字是有限的,您可以在BitConverter类中使用一些便利的方法:

long bits = BitConverter.DoubleToInt64Bits(value);
if (value > 0)
    return BitConverter.Int64BitsToDouble(bits - 1);
else if (value < 0)
    return BitConverter.Int64BitsToDouble(bits + 1);
else
    return -double.Epsilon;

IEEE-754格式的设计是使组成指数和尾数的比特位形成一个整数,其排序与浮点数相同。因此,要获得最大的较小数,如果该值为正,则可以从该数中减去1,如果该值为负,则可以加上1。

这样做的关键原因是尾数的首位未存储。如果您的尾数全为零,则您的数是2的幂。如果您从指数/尾数组合中减去1,则会得到所有的1,并且您将不得不从指数位借位。换句话说:您必须递减指数,这正是我们想要的。


谢谢,我最终采用了这种方法,它对我很有效。 - Dai
1
那么NaN呢?我们需要为它们设置特殊情况吗? - m93a

3

2
维基百科关于双精度浮点数的页面在这里:http://en.wikipedia.org/wiki/Double_precision_floating-point_format 我写了一些代码来分解double格式的二进制表示,减少尾数并重新组成结果。由于尾数中有一个隐含的位,我们必须检查它并相应地修改指数,在接近极限时可能会失败。
以下是代码:
public static double PrevDouble(double src)
{
    // check for special values:
    if (double.IsInfinity(src) || double.IsNaN(src))
        return src;
    if (src == 0)
        return -double.MinValue;

    // get bytes from double
    byte[] srcbytes = System.BitConverter.GetBytes(src);

    // extract components
    byte sign = (byte)(srcbytes[7] & 0x80);
    ulong exp = ((((ulong)srcbytes[7]) & 0x7F) << 4) + (((ulong)srcbytes[6] >> 4) & 0x0F);
    ulong mant = ((ulong)1 << 52) | (((ulong)srcbytes[6] & 0x0F) << 48) | (((ulong)srcbytes[5]) << 40) | (((ulong)srcbytes[4]) << 32) | (((ulong)srcbytes[3]) << 24) | (((ulong)srcbytes[2]) << 16) | (((ulong)srcbytes[1]) << 8) | ((ulong)srcbytes[0]);

    // decrement mantissa
    --mant;

    // check if implied bit has been removed and shift if so
    if ((mant & ((ulong)1 << 52)) == 0)
    {
        mant <<= 1;
        exp--;
    }

    // build byte representation of modified value
    byte[] bytes = new byte[8];
    bytes[7] = (byte)((ulong)sign | ((exp >> 4) & 0x7F));
    bytes[6] = (byte)((((ulong)exp & 0x0F) << 4) | ((mant >> 48) & 0x0F));
    bytes[5] = (byte)((mant >> 40) & 0xFF);
    bytes[4] = (byte)((mant >> 32) & 0xFF);
    bytes[3] = (byte)((mant >> 24) & 0xFF);
    bytes[2] = (byte)((mant >> 16) & 0xFF);
    bytes[1] = (byte)((mant >> 8) & 0xFF);
    bytes[0] = (byte)(mant & 0xFF);

    // convert back to double and return
    double res = System.BitConverter.ToDouble(bytes, 0);
    return res;
}

所有这些都会给您一个价值,该价值与尾数的最低位发生变化的初始值不同...理论上 :)

下面是一个测试:

public static Main(string[] args)
{
    double test = 1.0/3;
    double prev = PrevDouble(test);
    Console.WriteLine("{0:r}, {1:r}, {2:r}", test, prev, test - prev);
}

在我的电脑上,会得到以下结果:
0.33333333333333331, 0.33333333333333326, 5.5511151231257827E-17

两者间确有差异,但可能低于舍入阈值。然而,表达式test == prev的结果为false,正如上面所示,实际上存在差异 :)


3
我建议将你的格式字符串更改为"{0:r}, {1:r}, {2:r}"以打印出“roundtrippable”格式的双精度数,这将显示它们之间的差异。它会打印出:"0.33333333333333331, 0.33333333333333326, 5.5511151231257827E-17"。 - porges
谢谢@Porges,我会修改答案 :) - Corey
1
这并不检查数字是否是次正常数。它也不是一个下一个更小的实现,而是一个朝零方向的实现。 - Paul Childs

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