vba IsDate 函数返回意外结果

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4338025/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 10:58:44  来源:igfitidea点击:

IsDate function returns unexpected results

vbavb6

提问by mwolfe02

How come IsDate("13.50")returns Truebut IsDate("12.25.2010")returns False?

怎么来IsDate("13.50")的回报True,但IsDate("12.25.2010")回报False

回答by mwolfe02

I got tripped up by this little "feature" recently and wanted to raise awareness of some of the issues surrounding the IsDatefunction in VB and VBA.

我最近被这个小“功能”绊倒了,想提高人们对IsDateVB 和 VBA 中围绕该功能的一些问题的认识。

The Simple Case

简单案例

As you'd expect, IsDatereturns Truewhen passed a Date data type and Falsefor all other data types except Strings. For Strings, IsDatereturns Trueor Falsebased on the contents of the string:

正如您所期望的,在传递日期数据类型和除字符串之外的所有其他数据类型时IsDate返回。对于字符串,返回或基于字符串的内容:TrueFalseIsDateTrueFalse

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?

是日期时间?

IsDateshould be more precisely named IsDateTimebecause it returns Truefor strings formatted as times:

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'

Note from the last two examples above that IsDateis not a perfect validatorof times.

请注意,上面最后两个示例IsDate并不是完美的时间验证器

The Gotcha!

陷阱!

Not only does IsDateaccept times, it accepts times in many formats. One of which uses a period (.) as a separator. This leads to some confusion, because the period can be used as a time separator but not a date separator:

不仅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

This can be a problem if you are parsing a string and operating on it based on its apparent type. For example:

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

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

The Workarounds

解决方法

If you are testing a variant for its underlying data type, you should use TypeName(Var) = "Date"rather than IsDate(Var):

如果您正在测试其基础数据类型的变体,则应使用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

If, however, you are dealing with strings that may be dates or numbers (eg, parsing a text file), you should check if it's a number before checking to see if it's a date:

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

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

Even if all you care about is whether it is a date, you should probably make sure it's not a number:

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

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

Peculiarities of CDate

CDate 的特点

As @Deanna pointed out in the comments below, the behavior of CDate()is unreliable as well. Its results vary based on whether it is passed a string or a number:

正如@Deanna 在下面的评论中指出的那样, 的行为CDate()也不可靠。其结果因传递的是字符串还是数字而异:

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

Trailing andleading zeroes are significant if a number is passed as a string:

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

?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 

The behavior also changes as the decimal part of a string approaches the 60-minute mark:

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

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

The bottom line is that if you need to convert strings to date/time you need to be aware of what format you expect them to be in and then re-format them appropriately before relying on CDate()to convert them.

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

回答by Nigel Heffernan

Late to the game here (mwolfe02 answered this a year ago!) but the issue is still real, there are alternative approaches worth investigating, and StackOverflow is the place to find them: so here's my own answer...

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

I got tripped up by VBA.IsDate() on this very issue a few years ago, and coded up an extended function to cover cases that VBA.IsDate() handles badly. The worst one is that floats and integers return FALSE from IsDate, even though date serials are frequently passed as Doubles (for DateTime) and Long Integers (for dates).

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

A point to note: your implementation might not require the ability to check array variants. If not, feel free to strip out the code in the indented block that follows Else ' Comment this out if you don't need to check array variants. However, you should be aware that some third-party systems (including realtime market data clients) return their data in arrays, even single data points.

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

More information is in the code comments.

更多信息在代码注释中。

Here's the Code:

这是代码:

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

A Tip for people still using Excel 2003:

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

If you (or your users) are going to call IsDateEx() from a worksheet, put these two lines in, immediately below the function header, using a text editor in an exported .bas file and reimporting the file, because VB Attributes are useful, but they are not accessible to the code editor in Excel's VBA IDE:

如果您(或您的用户)要从工作表调用 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."

That's all one line: watch out for line-breaks inserted by the browser! ...And this line, which puts isDateEX into the function Wizard in the 'Information' category, alongside ISNUMBER(), ISERR(), ISTEXT() and so on:

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

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

Use "w\n2" if you prefer to see it under the Date & Time functions: beats hell outta losing it in the morass of 'Used Defined' functions from your own code, and all those third-party add-ins developed by people who don't do quiteenough to help occasional users.

如果您更喜欢在日期和时间函数下看到它,请使用“w\n2”:在您自己的代码中的“使用定义”函数以及人们开发的所有第三方加载项的泥沼中失去它谁不这样做相当足够的帮助临时用户。

I have no idea whether this still works in Office 2010.

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

Also, you might need the source for ArrayDimensions:

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

This API declaration is required in the module header:

模块标头中需要此 API 声明:

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

…And here's the function itself:

……这是函数本身:

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

Please keep the acknowledgements in your source code: as you progress in your career as a developer, you will come to appreciate your own contributions being acknowledged.

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

Also: I would advise you to keep that declaration private. If you must make it a public Sub in another module, insert the Option Private Modulestatement in the module header. You really don't want your users calling any function with CopyMemoryoperations and pointer arithmetic.

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