IsDate function returns unexpected results

后端 未结 2 2046
迷失自我
迷失自我 2020-11-27 03:48

How come IsDate(\"13.50\") returns True but IsDate(\"12.25.2010\") returns False?

相关标签:
2条回答
  • 2020-11-27 04:25

    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...

    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).

    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.

    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:

    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:

    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:

    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 quite enough to help occasional users.

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

    Also, you might need the source for ArrayDimensions:

    This API declaration is required in the module header:

    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 Module statement in the module header. You really don't want your users calling any function with CopyMemoryoperations and pointer arithmetic.

    0 讨论(0)
  • 2020-11-27 04:42

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

    The Simple Case

    As you'd expect, IsDate returns True when passed a Date data type and False for all other data types except Strings. For Strings, IsDate returns True or False based on the contents of the string:

    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 should be more precisely named IsDateTime because it returns True for strings formatted as times:

    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 IsDate is not a perfect validator of times.

    The Gotcha!

    Not only does IsDate accept 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("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(#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

    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:

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

    Trailing and leading 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:

    ?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.

    0 讨论(0)
提交回复
热议问题