【问题标题】:IsDate function returns unexpected resultsIsDate 函数返回意外结果
【发布时间】:2011-05-19 06:58:55
【问题描述】:

为什么IsDate("13.50") 返回TrueIsDate("12.25.2010") 返回False

【问题讨论】:

    标签: vba vb6


    【解决方案1】:

    我最近被这个小“功能”迷住了,想提高对 VB 和 VBA 中 IsDate 函数的一些问题的认识。

    简单案例

    如您所料,IsDate 在传递 Date 数据类型时返回 True,对于除字符串以外的所有其他数据类型返回 False。对于字符串,IsDate 根据字符串的内容返回TrueFalse

    IsDate(CDate("1/1/1980"))  --> True
    IsDate(#12/31/2000#)       --> True
    IsDate(12/24)              --> False  '12/24 evaluates to a Double: 0.5'
    IsDate("Foo")              --> False
    IsDate("12/24")            --> True
    

    IsDateTime?

    IsDate 应该更准确地命名为 IsDateTime,因为它会为格式化为时间的字符串返回 True

    IsDate("10:55 AM")   --> True
    IsDate("23:30")      --> True  'CDate("23:30")   --> 11:30:00 PM'
    IsDate("1:30:59")    --> True  'CDate("1:30:59") --> 1:30:59 AM'
    IsDate("13:55 AM")   --> True  'CDate("13:55 AM")--> 1:55:00 PM'
    IsDate("13:55 PM")   --> True  'CDate("13:55 PM")--> 1:55:00 PM'
    

    请注意,从上面的最后两个示例中,IsDate 并不是一个完美的验证器次。

    问题!

    IsDate 不仅接受时间,还接受多种格式的时间。其中一个使用句点 (.) 作为分隔符。这会导致一些混乱,因为句点可以用作时间分隔符,但不能用作日期分隔符:

    IsDate("13.50")     --> True  'CDate("13.50")    --> 1:50:00 PM'
    IsDate("12.25")     --> True  'CDate("12.25")    --> 12:25:00 PM'
    IsDate("12.25.10")  --> True  'CDate("12.25.10") --> 12:25:10 PM'
    IsDate("12.25.2010")--> False '2010 > 59 (number of seconds in a minute - 1)'
    IsDate("24.12")     --> False '24 > 23 (number of hours in a day - 1)'
    IsDate("0.12")      --> True  'CDate("0.12")     --> 12:12:00 AM
    

    如果您正在解析字符串并根据其明显类型对其进行操作,这可能会出现问题。例如:

    Function Bar(Var As Variant)
        If IsDate(Var) Then
            Bar = "This is a date"
        ElseIf IsNumeric(Var) Then
            Bar = "This is numeric"
        Else
            Bar = "This is something else"
        End If
    End Function
    
    ?Bar("12.75")   --> This is numeric
    ?Bar("12.50")   --> This is a date
    

    解决方法

    如果您正在测试其基础数据类型的变体,您应该使用TypeName(Var) = "Date" 而不是IsDate(Var)

    TypeName(#12/25/2010#)  --> Date
    TypeName("12/25/2010")  --> String
    
    Function Bar(Var As Variant)
        Select Case TypeName(Var)
        Case "Date"
            Bar = "This is a date type"
        Case "Long", "Double", "Single", "Integer", "Currency", "Decimal", "Byte"
            Bar = "This is a numeric type"
        Case "String"
            Bar = "This is a string type"
        Case "Boolean"
            Bar = "This is a boolean type"
        Case Else
            Bar = "This is some other type"
        End Select
    End Function
    
    ?Bar("12.25")   --> This is a string type
    ?Bar(#12/25#)   --> This is a date type
    ?Bar(12.25)     --> This is a numeric type
    

    但是,如果您正在处理可能是日期或数字的字符串(例如,解析文本文件),您应该在检查它是否是日期之前检查它是否是数字:

    Function Bar(Var As Variant)
        If IsNumeric(Var) Then
            Bar = "This is numeric"
        ElseIf IsDate(Var) Then
            Bar = "This is a date"
        Else
            Bar = "This is something else"
        End If
    End Function
    
    ?Bar("12.75")   --> This is numeric
    ?Bar("12.50")   --> This is numeric
    ?Bar("12:50")   --> This is a date
    

    即使你只关心它是否是一个日期,你也应该确保它不是一个数字:

    Function Bar(Var As Variant)
        If IsDate(Var) And Not IsNumeric(Var) Then
            Bar = "This is a date"
        Else
            Bar = "This is something else"
        End If
    End Function
    
    ?Bar("12:50")   --> This is a date
    ?Bar("12.50")   --> This is something else
    

    CDate 的特点

    正如@Deanna 在下面的 cmets 中指出的那样,CDate() 的行为也是不可靠的。它的结果根据传递的是字符串还是数字而有所不同:

    ?CDate(0.5)     -->  12:00:00 PM
    ?CDate("0.5")   -->  12:05:00 AM
    

    如果数字作为字符串传递,则尾随 前导零很重要:

    ?CDate(".5")    -->  12:00:00 PM 
    ?CDate("0.5")   -->  12:05:00 AM 
    ?CDate("0.50")  -->  12:50:00 AM 
    ?CDate("0.500") -->  12:00:00 PM 
    

    随着字符串的小数部分接近 60 分钟标记,该行为也会发生变化:

    ?CDate("0.59")  -->  12:59:00 AM 
    ?CDate("0.60")  -->   2:24:00 PM 
    

    底线是,如果您需要将字符串转换为日期/时间,您需要了解您希望它们采用什么格式,然后在依赖CDate() 转换它们之前适当地重新格式化它们。

    【讨论】:

    • 那...非常完整。
    • 如果我可以多次紫外线,我会 UP'd 这 10 倍:D
    • 另请注意,“X.YY”表示时间可能不明确,尤其是数值的小数部分是一天中的小数部分。 ?cdate("0.59") = 00:59:00?cdate("0.60") = 14:24:00
    • @Deanna:感谢您的评论;我不知道这个问题。我对其进行了充实,并将其添加到我的答案中。
    【解决方案2】:

    在这里游戏迟到了(mwolfe02 在一年前回答了这个问题!)但问题仍然存在,还有其他值得研究的方法,而 StackOverflow 是找到它们的地方:所以这是我自己的答案...

    几年前我在这个问题上被 VBA.IsDate() 绊倒了,并编写了一个扩展函数来覆盖 VBA.IsDate() 处理不好的情况。最糟糕的是浮点数和整数从 IsDate 返回 FALSE,即使日期序列经常作为双精度数(对于 DateTime)和长整数(对于日期)传递。

    需要注意的一点:您的实现可能不需要检查数组变体的能力。如果没有,请随意删除Else ' Comment this out if you don't need to check array variants 后面的缩进块中的代码。但是,您应该知道,一些第三方系统(包括实时市场数据客户端)以数组形式返回其数据,甚至是单个数据点。

    更多信息在代码 cmets 中。

    代码如下:

    Public Function IsDateEx(TestDate As Variant, Optional LimitPastDays As Long = 7305, Optional LimitFutureDays As Long = 7305, Optional FirstColumnOnly As Boolean = False) As Boolean
    'Attribute IsDateEx.VB_Description = "Returns TRUE if TestDate is a date, and is within ± 20 years of the system date.
    'Attribute IsDateEx.VB_ProcData.VB_Invoke_Func = "w\n9"
    Application.Volatile False
    On Error Resume Next
    
    ' Returns TRUE if TestDate is a date, and is within ± 20 years of the system date.
    
    ' This extends VBA.IsDate(), which returns FALSE for floating-point numbers and integers
    ' even though the VBA Serial Date is a Double. IsDateEx() returns TRUE for variants that
    ' can be parsed into string dates, and numeric values with equivalent date serials.  All
    ' values must still be ±20 years from SysDate. Note: locale and language settings affect
    ' the validity of day- and month names; and partial date strings (eg: '01 January') will
    ' be parsed with the missing components filled-in with system defaults.
    
    ' Optional parameters LimitPastDays/LimitFutureDays vary the default ± 20 years boundary
    
    ' Note that an array variant is an acceptable input parameter: IsDateEx will return TRUE
    ' if all the values in the array are valid dates: set  FirstColumnOnly:=TRUE if you only
    ' need to check the leftmost column of a 2-dimensional array.
    
    
    ' *     THIS CODE IS IN THE PUBLIC DOMAIN
    ' *
    ' *     Author: Nigel Heffernan, May 2005
    ' *     http://excellerando.blogspot.com/
    ' *
    ' *
    ' *     *********************************
    
    Dim i As Long
    Dim j As Long
    Dim k As Long
    
    Dim jStart As Long
    Dim jEnd   As Long
    
    Dim dateFirst As Date
    Dim dateLast As Date
    
    Dim varDate As Variant
    
    dateFirst = VBA.Date - LimitPastDays
    dateLast = VBA.Date + LimitFutureDays
    
    IsDateEx = False
    
    If TypeOf TestDate Is Excel.Range Then
        TestDate = TestDate.Value2
    End If
    
    If VarType(TestDate) < vbArray Then
    
        If IsDate(TestDate) Or IsNumeric(TestDate) Then
            If (dateLast > TestDate) And (TestDate > dateFirst) Then
                IsDateEx = True
            End If
        End If
    
    Else   ' Comment this out if you don't need to check array variants
    
        k = ArrayDimensions(TestDate)
        Select Case k
        Case 1
    
            IsDateEx = True
            For i = LBound(TestDate) To UBound(TestDate)
                If IsDate(TestDate(i)) Or IsNumeric(TestDate(i)) Then
                    If Not ((dateLast > CVDate(TestDate(i))) And (CVDate(TestDate(i)) > dateFirst)) Then
                        IsDateEx = False
                        Exit For
                    End If
                Else
                    IsDateEx = False
                    Exit For
                End If
            Next i
    
        Case 2
    
            IsDateEx = True
            jStart = LBound(TestDate, 2)
    
            If FirstColumnOnly Then
                jEnd = LBound(TestDate, 2)
            Else
                jEnd = UBound(TestDate, 2)
            End If
    
            For i = LBound(TestDate, 1) To UBound(TestDate, 1)
                For j = jStart To jEnd
                    If IsDate(TestDate(i, j)) Or IsNumeric(TestDate(i, j)) Then
                        If Not ((dateLast > CVDate(TestDate(i, j))) And (CVDate(TestDate(i, j)) > dateFirst)) Then
                            IsDateEx = False
                            Exit For
                        End If
                    Else
                        IsDateEx = False
                        Exit For
                    End If
                Next j
            Next i
    
        Case Is > 2
    
            ' Warning: For... Each enumerations are SLOW
            For Each varDate In TestDate
    
                If IsDate(varDate) Or IsNumeric(varDate) Then
                    If Not ((dateLast > CVDate(varDate)) And (CVDate(varDate) > dateFirst)) Then
                        IsDateEx = False
                        Exit For
                    End If
                Else
                    IsDateEx = False
                    Exit For
                End If
    
            Next varDate
    
        End Select
    
    End If
    
    End Function
    

    给仍在使用 Excel 2003 的人的提示:

    如果您(或您的用户)要从工作表中调用 IsDateEx(),请将这两行放在函数标题下方,在导出的 .bas 文件中使用文本编辑器并重新导入文件,因为 VB 属性很有用,但是 Excel 的 VBA IDE 中的代码编辑器无法访问它们

    Attribute IsDateEx.VB_Description = "Returns TRUE if TestDate is a date, and is within ± 20 years of the system date.\r\nChange the defaulte default ± 20 years boundaries by setting values for LimitPastDays and LimitFutureDays\r\nIf you are checking an array of dates, ALL the values will be tested: set FirstColumnOnly TRUE to check the leftmost column only."
    

    只有一行:注意浏览器插入的换行符! ...还有这一行,它将 isDateEX 与 ISNUMBER()、ISERR()、ISTEXT() 等一起放入“信息”类别的函数向导中:

    Attribute IsDateEx.VB_ProcData.VB_Invoke_Func = "w\n9"
    

    如果您希望在日期和时间函数下看到它,请使用“w\n2”:在您自己的代码和所有那些第三方加载项的“已使用定义”函数的泥沼中击败地狱失去它由那些相当不足以帮助临时用户的人开发。

    我不知道这在 Office 2010 中是否仍然有效。

    另外,您可能需要 ArrayDimensions 的来源:

    模块头中需要此 API 声明:

    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
                       (Destination As Any, _
                        Source As Any, _
                        ByVal Length As Long)
    

    …这是函数本身:

    Private Function ArrayDimensions(arr As Variant) As Integer
      '-----------------------------------------------------------------
      ' will return:
      ' -1 if not an array
      ' 0  if an un-dimmed array
      ' 1  or more indicating the number of dimensions of a dimmed array
      '-----------------------------------------------------------------
    
    
      ' Retrieved from Chris Rae's VBA Code Archive - http://chrisrae.com/vba
      ' Code written by Chris Rae, 25/5/00
    
      ' Originally published by R. B. Smissaert.
      ' Additional credits to Bob Phillips, Rick Rothstein, and Thomas Eyde on VB2TheMax
    
      Dim ptr As Long
      Dim vType As Integer
    
      Const VT_BYREF = &H4000&
    
      'get the real VarType of the argument
      'this is similar to VarType(), but returns also the VT_BYREF bit
      CopyMemory vType, arr, 2
    
      'exit if not an array
      If (vType And vbArray) = 0 Then
        ArrayDimensions = -1
        Exit Function
      End If
    
      'get the address of the SAFEARRAY descriptor
      'this is stored in the second half of the
      'Variant parameter that has received the array
      CopyMemory ptr, ByVal VarPtr(arr) + 8, 4
    
      'see whether the routine was passed a Variant
      'that contains an array, rather than directly an array
      'in the former case ptr already points to the SA structure.
      'Thanks to Monte Hansen for this fix
    
      If (vType And VT_BYREF) Then
        ' ptr is a pointer to a pointer
        CopyMemory ptr, ByVal ptr, 4
      End If
    
      'get the address of the SAFEARRAY structure
      'this is stored in the descriptor
    
      'get the first word of the SAFEARRAY structure
      'which holds the number of dimensions
      '...but first check that saAddr is non-zero, otherwise
      'this routine bombs when the array is uninitialized
    
      If ptr Then
        CopyMemory ArrayDimensions, ByVal ptr, 2
      End If
    
    End Function
    

    请在您的源代码中保留致谢:随着您作为开发人员的职业发展,您会感谢自己的贡献得到承认。

    另外:我建议您将该声明保密。如果您必须将其设为另一个模块中的公共 Sub,请在模块标题中插入 Option Private Module 语句。您真的不希望您的用户使用 CopyMemory 操作和指针算法调用任何函数。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-12-02
    • 2017-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多