Как получилось IsDate("13.50")
возвращает True
, но IsDate("12.25.2010")
возвращает False
?
Функция IsDate возвращает неожиданные результаты
Ответ 1
В последнее время я немного расстроился этой маленькой "особенностью" и хотел повысить осведомленность о некоторых проблемах, связанных с функцией IsDate
в VB и VBA.
Простой случай
Как и следовало ожидать, IsDate
возвращает True
при передаче типа данных даты и 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
не является идеальным валидатором раз.
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()
для их преобразования.
Ответ 2
Поздно к игре здесь (mwolfe02 ответил на это год назад!), но проблема по-прежнему реальна, есть альтернативные подходы, которые стоит исследовать, а StackOverflow - это место, где их можно найти: вот мой собственный ответ...
Я сработал VBA.IsDate() по этой самой проблеме несколько лет назад и закодировал расширенную функцию, чтобы покрыть случаи, которые VBA.IsDate() плохо обрабатывает. Хуже всего то, что float и integers возвращают FALSE из IsDate, хотя сериальные даты часто передаются как Doubles (для 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 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 и арифметики указателей.