为什么 VBA 和 Excel 在两个单元格是否相等方面存在分歧?

Jok*_*oku 6 excel vba excel-365

我正在尝试比较表中的两个单元格:

桌子

使用公式计算“MR”列=ABS([@Value]-A1)以确定“值”列的移动极差。“值”列中的值未四舍五入。“MR”列(B3 和 B4)中突出显示的单元格是相等的。我可以将公式输入=B3=B4到单元格中,Excel 会显示 B3 等于 B4。

但是当我在VBA中比较它们时,VBA说B4大于B3。我可以选择单元格 B3 并在“立即窗口”中输入以下内容? selection.value = selection.offset(1).value。该语句的评估结果为 false。

我尝试从公式中删除绝对值,认为这可能与它有关,但 VBA 仍然说它们不相等。

我尝试添加另一行,其中 Value=1.78,因此 MR=0.18。有趣的是,新行(B5)中的 MR等于B3,但不等于B4。

然后我尝试增加 A4 的小数以匹配其他值,现在 VBA 说它们相等。但是当我将绝对值添加回公式时,VBA 再次表示它们不相等。我再次删除了绝对值,现在 VBA 说它们不相等。

为什么 VBA 告诉我单元格不相等,而 Excel 却说它们相等?今后如何通过 VBA 可靠地处理这种情况?

Exc*_*ero 12

问题在于IEEE 754 浮点运算标准在设计上并不精确。几乎所有编程语言都因此而受到影响。

\n

IEEE 754 是一个极其复杂的主题,当您研究了几个月并认为自己完全理解时,您只是在欺骗自己!

\n

准确的浮点值比较很困难并且容易出错。在尝试比较浮点数之前请仔细考虑!

\n

Excel 程序通过在应用程序端作弊来解决这个问题。另一方面,VBA 忠实地遵循双精度( binary64 ) 的 IEEE 754 规范。

\n

Double 值在内存中使用 64 位表示。这 64 位分为三个不同的字段,用于二进制科学记数法:

\n
    \n
  1. SIGN 位(1 位表示值的符号:正/负)
  2. \n
  3. 指数(11 位,值偏差 +1023)
  4. \n
  5. MANTISSA(53 位,存储的 52 位 + 隐含的 1 位)
  6. \n
\n

该系统中的尾数利用了所有二进制数都以 和 开头的事实,1因此它1不存储在位模式中。这意味着将正常值的尾数精度提高到 53 位。

\n

数学原理如下:存储值 = SIGN VALUE * 2^UNBIASED EXPONENT * MANTISSA

\n

1注意,符号位的存储值表示负符号值(-1),而a0表示正符号值(+1)。公式为SIGN VALUE = (-1) ^ (sign bit)。

\n

问题总是归结为同一件事。

\n
\n

绝大多数实数无法在该系统中精确表达,这会引入像杂草一样传播的小舍入误差。

\n
\n

将此系统视为规则间隔点的网格可能会有所帮助。该系统只能表示点值,不能表示点之间的实数。分配给浮点数的所有值都将四舍五入到一个点值(通常是最近的点,但有些模式强制向上舍入到下一个最高点,或向下舍入)。对浮点值进行任何计算实际上都会保证结果值需要四舍五入。

\n

为了强调显而易见的事实,该网格上相邻的可表示点值之间存在无限多个实数;并且所有这些都四舍五入到离散的网格点。

\n

更糟糕的是,当网格远离真正的零(在两个方向上)扩展时,间隙大小在每次二次方时都会加倍。2例如,对于to范围内的值,网格点之间的间隙长度是to4范围内的值的两倍。当表示足够大的值时,网格间隙长度变得很大,但接近真正的零时,它很小。12

\n

用你的例子数字...

\n
\n

1.24用以下二进制表示:

\n

符号位 = 0

\n

指数 = 01111111111

\n

尾数 = 0011110101110000101000111101011100001010001111010111

\n

完整 64 位的十六进制模式恰好是:3FF3D70A3D70A3D7。

\n

精度完全源自 53 位尾数,二进制的精确十进制值为:\n0.23999999999999999911182158029987476766109466552734375

\n

在这种情况下,1与尾数关联的隐藏位隐含了前导整数,因此完整的十进制值为:

\n

1.2399999999999999911182158029987476766109466552734375

\n

现在请注意,这并不完全1.24是问题的全部。

\n
\n

让我们检查一下1.42:

\n

符号位 = 0

\n

指数 = 01111111111

\n

尾数 = 0110101110000101000111101011100001010001111010111000

\n

完整 64 位的十六进制模式恰好是:3FF6B851EB851EB8。

\n

暗示1完整的十进制值存储为:

\n

1.4199999999999999289457264239899814128875732421875000

\n

再说一遍,不完全是1.42。

\n
\n

现在,让我们检查一下1.6:

\n

符号位 = 0

\n

指数 = 01111111111

\n

尾数 = 1001100110011001100110011001100110011001100110011010

\n

完整 64 位的十六进制模式恰好是:3FF999999999999A。

\n
\n

请注意,在这种情况下,当尾数位用完时,重复的二进制分数会被截断\n并四舍五入?显然,1.6 当以二进制基数 2 表示时,永远无法精确地表示,就像 1/3 永远无法以十进制基数 10 精确表示一样(0.33333333333333333333333... \xe2\x89\xa0 1/3)。

\n
\n

暗示1完整的十进制值存储为:

\n

1.6000000000000000888178419700125232338905334472656250

\n

不完全是1.6,但比其他人更接近!

\n
\n

现在让我们减去完整存储的双精度表示:

\n

1.60- 1.42= 0.18000000000000015987

\n

1.42- 1.24= 0.17999999999999993782

\n

正如您所看到的,它们根本不平等。

\n

解决这个问题的常用方法是阈值测试,基本上是检查两个值是否足够接近......这取决于您和您的要求。请预先警告,有效的阈值测试比乍一看要困难得多。

\n

这是一个可以帮助您开始比较两个双精度数字的函数。它可以很好地处理许多情况,但不能处理所有情况,因为没有函数可以。

\n
Function Roughly(a#, b#, Optional within# = 0.00001) As Boolean\n    Dim d#, x#, y#, z#\n    \n    Const TINY# = 1.17549435E-38    \'SINGLE_MIN\n    \n    If a = b Then Roughly = True: Exit Function\n            \n    x = Abs(a): y = Abs(b): d = Abs(a - b)\n    \n    If a <> 0# Then\n        If b <> 0# Then\n            z = x + y\n            If z > TINY Then\n                Roughly = d / z < within\n                Exit Function\n            End If\n        End If\n    End If\n    \n    Roughly = d < within * TINY\nEnd Function\n
Run Code Online (Sandbox Code Playgroud)\n

这里的想法是,True如果两个 Double 在一定范围内大致相同,则函数返回:

\n
MsgBox Roughly(3.14159, 3.141591)           \'<---dispays True\n
Run Code Online (Sandbox Code Playgroud)\n

内边距默认为 0.00001,但您可以传递所需的任何边距。

\n

虽然我们知道:

\n
MsgBox 1.60 - 1.42 = 1.42 - 1.24            \'<---dispays False\n
Run Code Online (Sandbox Code Playgroud)\n

考虑一下这个的实用性:

\n
MsgBox Roughly(1.60 - 1.42, 1.42 - 1.24)    \'<---dispays True\n
Run Code Online (Sandbox Code Playgroud)\n
\n

@chris neilsen 链接到一个关于 Excel 和 IEEE 754 的有趣的Microsoft 页面。

\n

请阅读 David Goldberg 的开创性著作《每个计算机科学家应该了解浮点运算》。它改变了我理解浮点数的方式。

\n