比较VBA精度问题中的双精度值

问题描述 投票:0回答:10

我在 Excel VBA 中比较 2 个双精度数时遇到问题

假设我有以下代码

Dim a as double
Dim b as double
a = 0.15
b = 0.01

对 b 进行一些操作后,b 现在等于 0.6

然而,与双精度数据类型相关的不精确性让我头疼,因为

if a = b then
 //this will never trigger
end if

你知道如何消除双精度类型的尾随不精确吗?

excel vba comparison double
10个回答
16
投票

您无法比较浮点值是否相等。请参阅这篇关于“比较浮点数”的文章,了解如何处理内在错误的讨论。

这并不像与恒定误差范围进行比较那么简单,除非您事先确定浮点数的绝对范围是多少。


7
投票

如果你要这样做......

Dim a as double  
 Dim b as double  
 a = 0.15  
 b = 0.01

您需要在 IF 语句中添加舍入函数,如下所示...

  If Round(a,2) = Round(b,2) Then   
     //code inside block will now trigger.
  End If  

另请参阅此处获取其他 Microsoft 参考资料


5
投票

比较双精度数的相等性永远是不明智的。

一些十进制值映射到多个浮点表示形式。所以一个 0.6 并不总是等于另一个 0.6。

如果我们减去一个,我们可能会得到类似 0.00000000051 的结果。

我们现在可以将相等定义为差异小于某个误差范围。


5
投票

这是我写的一个简单的函数:

Function dblCheckTheSame(number1 As Double, number2 As Double, Optional Digits As Integer = 12) As Boolean

If (number1 - number2) ^ 2 < (10 ^ -Digits) ^ 2 Then
    dblCheckTheSame = True
Else
    dblCheckTheSame = False
End If

End Function

调用它:

MsgBox dblCheckTheSame(1.2345, 1.23456789)
MsgBox dblCheckTheSame(1.2345, 1.23456789, 4)
MsgBox dblCheckTheSame(1.2345678900001, 1.2345678900002)
MsgBox dblCheckTheSame(1.2345678900001, 1.2345678900002, 14)

3
投票

答案很晚,但令我惊讶的是,尚未发布解决方案来解决(当前)接受的答案中链接的文章中概述的问题,即:

  • 舍入检查具有绝对公差的相等性(例如,如果四舍五入到 4d.p.,则为 0.0001 个单位),这在比较多个数量级的不同值时是垃圾(因此不仅仅是与 0 进行比较)
  • 当前答案中没有提到与同时比较的数字之一成比例的相对容差,但在非零比较上表现良好(但是,随着缩放比例的放大,与零比较时会很糟糕)。

为了解决这个问题,我从 Python 中获得了灵感:PEP 485 - 用于测试近似相等的函数来实现以下内容(在标准模块中):

代码

'@NoIndent: Don't want to lose our description annotations
'@Folder("Tests.Utils")

Option Explicit
Option Private Module

'Based on Python's math.isclose https://github.com/python/cpython/blob/17f94e28882e1e2b331ace93f42e8615383dee59/Modules/mathmodule.c#L2962-L3003
'math.isclose -> boolean
'    a: double
'    b: double
'    relTol: double = 1e-09
'        maximum difference for being considered "close", relative to the
'        magnitude of the input values, e.g. abs(a - b)/(a OR b) < relTol
'    absTol: double = 0.0
'        maximum difference for being considered "close", regardless of the
'        magnitude of the input values, e.g. abs(a - b) < absTol
'Determine whether two floating point numbers are close in value.
'Return True if a is close in value to b, and False otherwise.
'For the values to be considered close, the difference between them
'must be smaller than at least one of the tolerances.
'-inf, inf and NaN behave similarly to the IEEE 754 Standard.  That
'is, NaN is not close to anything, even itself.  inf and -inf are
'only close to themselves.
'@Description("Determine whether two floating point numbers are close in value, accounting for special values in IEEE 754")
Public Function IsClose(ByVal a As Double, ByVal b As Double, _
                        Optional ByVal relTol As Double = 0.000000001, _
                        Optional ByVal absTol As Double = 0 _
                        ) As Boolean
                        
    If relTol < 0# Or absTol < 0# Then
        Err.Raise 5, Description:="tolerances must be non-negative"
    ElseIf a = b Then
        'Short circuit exact equality -- needed to catch two infinities of
        ' the same sign. And perhaps speeds things up a bit sometimes.
        IsClose = True
    ElseIf IsInfinity(a) Or IsInfinity(b) Then
        'This catches the case of two infinities of opposite sign, or
        ' one infinity and one finite number. Two infinities of opposite
        ' sign would otherwise have an infinite relative tolerance.
        'Two infinities of the same sign are caught by the equality check
        ' above.
        IsClose = False
    Else
        'Now do the regular computation on finite arguments. Here an
        ' infinite tolerance will always result in the function returning True,
        ' since an infinite difference will be <= to the infinite tolerance.
        'NaN has already been filtered out in the equality checks earlier.

        On Error Resume Next 'This is to suppress overflow errors as we deal with infinity.
        Dim diff As Double: diff = Abs(b - a)
        
        If diff <= absTol Then
            IsClose = True
        ElseIf diff <= CDbl(Abs(relTol * b)) Then
            IsClose = True
        ElseIf diff <= CDbl(Abs(relTol * a)) Then
            IsClose = True
        End If
        On Error GoTo 0
    End If
End Function

'@Description "Checks if Number is IEEE754 +/- inf, won't raise an error"
Public Function IsInfinity(ByVal Number As Double) As Boolean
    On Error Resume Next                         'in case of NaN
    IsInfinity = Abs(Number) = PosInf
    On Error GoTo 0
End Function

'@Description "IEEE754 -inf"
Public Property Get NegInf() As Double
    On Error Resume Next
    NegInf = -1 / 0
    On Error GoTo 0
End Property

'@Description "IEEE754 +inf"
Public Property Get PosInf() As Double
    On Error Resume Next
    PosInf = 1 / 0
    On Error GoTo 0
End Property

'@Description "IEEE754 signaling NaN (sNaN)"
Public Property Get NaN() As Double
    On Error Resume Next
    NaN = 0 / 0
    On Error GoTo 0
End Property

'@Description "IEEE754 quiet NaN (qNaN)"
Public Property Get QNaN() As Double
    QNaN = -NaN
End Property

更新以纳入来自 Cristian Buse关于代码审查的出色反馈

示例

IsClose
功能可用于检查绝对差异:

assert(IsClose(0, 0.0001233, absTol:= 0.001)) 'same to 3 d.p.?

...或相对差异:

assert(IsClose(1234.5, 1234.6, relTol:= 0.0001)) '0.01% relative difference?

...但通常您指定两者,如果满足任一容差,则数字被认为接近。它对仅接近自身的 +-无穷大和几乎为零的 NaN 有特殊处理(请参阅 PEP 以获取完整的理由,或我的代码审查帖子,我希望对此代码提供反馈:)


2
投票

正如已经指出的,许多十进制数无法精确表示为传统的浮点类型。根据问题空间的性质,您可能最好使用 Decimal VBA 类型,它可以以精确到特定小数点的完美精度表示十进制数(以 10 为基数)。这通常是为了表示金钱,例如通常需要 2 位小数精度。

Dim a as Decimal
Dim b as Decimal
a = 0.15
b = 0.01

1
投票

货币数据类型可能是一个不错的选择。它以固定的四位精度处理相对较大的数字。


1
投票

根据您的情况和数据,如果您对默认显示的精度级别感到满意,您可以尝试将数字的字符串转换作为非常简单的编码解决方案进行比较:

if cstr(a) = cstr(b)

这将包括默认显示的尽可能多的精度,这通常足以认为数字相等。

这对于非常大的数据集来说效率很低,但对我来说,在协调导入的数据时非常有用,这些数据相同但在将数据存储在 VBA 数组中后不匹配。


0
投票

工作一轮? 不确定这是否能回答所有情况,但我遇到了在 VBA 中比较舍入双精度值的问题。当我比较四舍五入后看起来相同的数字时,VBA 将在 if-then 比较语句中触发 false。 我的解决方法是运行两次转换,首先将双精度转换为字符串,然后将字符串转换为双精度,然后进行比较。

模拟示例 我没有记录导致本文中提到的错误的确切数字,并且我的示例中的金额当前不会触发问题,旨在代表问题的类型。

 Sub Test_Rounded_Numbers()

      Dim Num1 As Double

      Dim Num2 As Double

      Let Num1 = 123.123456789

      Let Num2 = 123.123467891

      Let Num1 = Round(Num1, 4) '123.1235


      Let Num2 = Round(Num2, 4) '123.1235

      If Num1 = Num2 Then

           MsgBox "Correct Match, " & Num1 & " does equal " & Num2
      Else
           MsgBox "Inccorrect Match, " & Num1 & " does not equal " & Num2
      End If

      'Here it would say that "Inccorrect Match, 123.1235 does not equal 123.1235."

 End Sub

 Sub Fixed_Double_Value_Type_Compare_Issue()

      Dim Num1 As Double

      Dim Num2 As Double

      Let Num1 = 123.123456789

      Let Num2 = 123.123467891

      Let Num1 = Round(Num1, 4) '123.1235


      Let Num2 = Round(Num2, 4) '123.1235

      'Add CDbl(CStr(Double_Value))
      'By doing this step the numbers
      'would trigger if they matched
      '100% of the time

      If CDbl(CStr(Num1)) = CDbl(CStr(Num2)) Then

           MsgBox "Correct Match"
      Else
           MsgBox "Inccorrect Match"

      End If

      'Now it says Here it would say that "Correct Match, 123.1235 does equal 123.1235."
 End Sub

-1
投票

如果可能,尝试使用单一值。 转换为 Double 值会产生随机错误。

Public Sub Test()
Dim D01 As Double
Dim D02 As Double
Dim S01 As Single
Dim S02 As Single
S01 = 45.678 / 12
S02 = 45.678
D01 = S01
D02 = S02
Debug.Print S01 * 12
Debug.Print S02
Debug.Print D01 * 12
Debug.Print D02
End Sub

    45,678 
    45,678 
    45,67799949646 
    45,6780014038086 
© www.soinside.com 2019 - 2024. All rights reserved.