IsDate函数返回意外结果

时间:2010-12-02 17:41:03

标签: vba vb6

IsDate("13.50")如何返回TrueIsDate("12.25.2010")会返回False

2 个答案:

答案 0 :(得分:63)

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

简单案例

正如您所期望的那样,IsDate在传递Date数据类型时返回True,为除Strings之外的所有其他数据类型返回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不是一个完美的验证器

The Gotcha!

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在下面的评论中指出的那样,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()转换它们之前重新格式化它们

答案 1 :(得分:6)

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

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

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

更多信息在代码注释中。

以下是代码:

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”。时间函数:在你自己的代码中使用'Used Defined'功能的混乱中失去它,以及那些没有相当足够帮助的人开发的所有第三方加载项偶尔的用户。

我不知道这是否仍适用于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

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

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