vba - tutorial - La función IsDate devuelve resultados inesperados
visual basic for applications download (2)
Esta pequeña "función" me hizo tropezar recientemente y quería concienciar sobre algunos de los problemas que rodean la función IsDate
en VB y VBA.
El caso simple
Como era de esperar, IsDate
devuelve True
cuando pasó un tipo de datos de fecha y False
para todos los demás tipos de datos, excepto Strings. Para Strings, IsDate
devuelve True
o False
según el contenido de la cadena:
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
debe IsDateTime
mayor precisión IsDateTime
porque devuelve True
para cadenas formateadas como veces:
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''
Nota de los dos últimos ejemplos anteriores que IsDate
no es un validador perfecto de los tiempos.
¡El Gotcha!
IsDate
no solo acepta tiempos, acepta tiempos en muchos formatos. Uno de los cuales usa un punto ( .
) Como separador. Esto lleva a cierta confusión, porque el período se puede usar como un separador de tiempo pero no como un separador de fecha:
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
Esto puede ser un problema si está analizando una cadena y operando en ella en función de su tipo aparente. Por ejemplo:
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
Las soluciones
Si está probando una variante para su tipo de datos subyacente, debe usar TypeName(Var) = "Date"
lugar de 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
Sin embargo, si se trata de cadenas que pueden ser fechas o números (por ejemplo, analizar un archivo de texto), debe verificar si se trata de un número antes de verificar para ver si se trata de una fecha:
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
Incluso si lo único que te importa es si se trata de una fecha, probablemente deberías asegurarte de que no sea un número:
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
Peculiaridades de CDate
Como señaló @Deanna en los comentarios a continuación, el comportamiento de CDate()
es confiable. Sus resultados varían según si se le pasa una cadena o un número:
?CDate(0.5) --> 12:00:00 PM
?CDate("0.5") --> 12:05:00 AM
Los ceros finales y iniciales son significativos si un número se pasa como una cadena:
?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
El comportamiento también cambia a medida que la parte decimal de una cadena se acerca a la marca de los 60 minutos:
?CDate("0.59") --> 12:59:00 AM
?CDate("0.60") --> 2:24:00 PM
La conclusión es que si necesita convertir cadenas a la fecha / hora, debe conocer el formato en el que espera que estén y, luego, formatearlas adecuadamente antes de confiar en CDate()
para convertirlas.
¿Cómo es que IsDate("13.50")
devuelve True
pero IsDate("12.25.2010")
devuelve False
?
Tarde en el juego aquí (mwolfe02 respondió esto hace un año!) Pero el problema sigue siendo real, hay enfoques alternativos que vale la pena investigar, y es el lugar para encontrarlos: así que aquí está mi propia respuesta ...
Me tropecé con VBA.IsDate () en este mismo tema hace unos años, y codifiqué una función extendida para cubrir casos que VBA.IsDate () maneja mal. El peor es que los flotantes y enteros devuelven FALSE desde IsDate, aunque las publicaciones seriadas de fechas se pasan con frecuencia como Dobles (para DateTime) y Long Integers (para fechas).
Un punto a tener en cuenta: es posible que su implementación no requiera la capacidad de verificar variantes de matriz. De lo contrario, no dude en quitar el código en el bloque sangrado que sigue a Else '' Comment this out if you don''t need to check array variants
. Sin embargo, debe tener en cuenta que algunos sistemas de terceros (incluidos clientes de datos de mercado en tiempo real) devuelven sus datos en matrices, incluso puntos de datos únicos.
Más información está en los comentarios del código.
Aquí está el Código:
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
Un consejo para las personas que todavía usan Excel 2003:
Si usted (o sus usuarios) van a llamar a IsDateEx () desde una hoja de trabajo, coloque estas dos líneas, inmediatamente debajo del encabezado de la función, usando un editor de texto en un archivo .bas exportado y reimportar el archivo, porque los atributos de VB son útiles , pero no son accesibles para el editor de código en el IDE de VBA de Excel :
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."
Eso es una sola línea: ¡cuidado con los saltos de línea insertados por el navegador! ... Y esta línea, que pone isDateEX en el asistente de función en la categoría ''Información'', junto con ISNUMBER (), ISERR (), ISTEXT () y así sucesivamente:
Attribute IsDateEx.VB_ProcData.VB_Invoke_Func = "w/n9"
Utilice "w / n2" si prefiere verlo en las funciones de Fecha y hora: supera al perderlo en el marasmo de las funciones "Definidas usadas" de su propio código, y todos los complementos de terceros desarrollados por personas que no hacen lo suficiente para ayudar a los usuarios ocasionales.
No tengo idea si esto todavía funciona en Office 2010.
Además, es posible que necesite la fuente para ArrayDimensions:
Esta declaración de API es obligatoria en el encabezado del módulo:
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(Destination As Any, _
Source As Any, _
ByVal Length As Long)
... Y aquí está la función en sí:
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
Guarde los agradecimientos en su código fuente: a medida que avance en su carrera como desarrollador, llegará a apreciar que sus propias contribuciones sean reconocidas.
Además: le aconsejo que mantenga esa declaración privada. Si debe convertirlo en un Sub público en otro módulo, inserte la declaración de Option Private Module
en el encabezado del módulo. Realmente no desea que sus usuarios llamen a ninguna función con CopyMemoryoperations y aritmética de punteros.