Функция IsDate возвращает неожиданные результаты

Как так IsDate("13.50") возвращается True но IsDate("12.25.2010") возвращается False?

2 ответа

Решение

Недавно меня подхватила эта маленькая "особенность", и я хотел повысить осведомленность о некоторых проблемах, связанных с IsDate функция в VB и VBA.

Простой случай

Как и следовало ожидать, IsDate возвращается True когда передан тип данных Date и False для всех других типов данных, кроме строк. Для строк, IsDate возвращается True или же False на основании содержимого строки:

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 указал в комментариях ниже, поведение 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() конвертировать их.

Позже к игре здесь (mwolfe02 ответил на это год назад!), Но проблема все еще актуальна, есть альтернативные подходы, которые стоит изучить, и Stackru - место, где их можно найти: так вот мой собственный ответ...

Несколько лет назад VBA.IsDate() подвела меня к этой проблеме, и я написала расширенную функцию, охватывающую случаи, которые VBA.IsDate() плохо обрабатывает. Хуже всего то, что числа с плавающей запятой и целые числа возвращают FALSE из IsDate, хотя сериалы даты часто передаются как Double (для DateTime) и Long Integer (для дат).

Обратите внимание: вашей реализации может не потребоваться возможность проверять варианты массивов. Если нет, не стесняйтесь вырезать код из отступа блока, который следует 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", если вы предпочитаете видеть его в функциях "Дата и время": это, черт побери, потеряно в потоке функций "Используется определено" из вашего собственного кода и всех сторонних надстроек, разработанных людьми. которые не делают достаточно, чтобы помочь случайным пользователям.

Я понятия не имею, работает ли это все еще в 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 утверждение в заголовке модуля. Вы действительно не хотите, чтобы ваши пользователи вызывали какие-либо функции с CopyMemoryoperations и арифметикой указателей.

Другие вопросы по тегам