web-dev-qa-db-fra.com

La fonction IsDate renvoie des résultats inattendus

Comment se fait-il que IsDate("13.50") renvoie True mais IsDate("12.25.2010") renvoie False?

28
mwolfe02

J'ai été trompé récemment par cette petite "fonctionnalité" et je voulais sensibiliser à certains des problèmes entourant la fonction IsDate dans VB et VBA.

Le cas simple

Comme vous vous en doutez, IsDate renvoie True lorsque vous passez un type de données Date et False pour tous les autres types de données sauf Strings. Pour les chaînes, IsDate renvoie True ou False en fonction du contenu de la chaîne:

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 devrait être plus précisément nommé IsDateTime car il renvoie True pour les chaînes formatées en temps:

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'

Notez à partir des deux derniers exemples ci-dessus que IsDate n'est pas parfait validateur de fois.

Le Gotcha!

Non seulement IsDate accepte les heures, il accepte les heures dans de nombreux formats. L'un d'eux utilise un point (.) Comme séparateur. Cela conduit à une certaine confusion, car la période peut être utilisée comme séparateur d'heure mais pas comme séparateur de date:

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

Cela peut être un problème si vous analysez une chaîne et que vous l'utilisez en fonction de son type apparent. Par exemple:

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

Les contournements

Si vous testez une variante pour son type de données sous-jacent, vous devez utiliser TypeName(Var) = "Date" plutôt que 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

Si, cependant, vous avez affaire à des chaînes qui peuvent être des dates ou des nombres (par exemple, analyser un fichier texte), vous devez vérifier s'il s'agit d'un nombre avant de vérifier si c'est une 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

Même si vous ne vous souciez que de savoir s'il s'agit d'une date, vous devez probablement vous assurer qu'il ne s'agit pas d'un nombre:

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

Particularités de CDate

Comme @Deanna l'a souligné dans les commentaires ci-dessous, le comportement de CDate() n'est pas fiable non plus. Ses résultats varient en fonction de la transmission d'une chaîne ou d'un nombre:

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

Les zéros de début et sont significatifs si un nombre est passé sous forme de chaîne:

?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 

Le comportement change également lorsque la partie décimale d'une chaîne se rapproche de la marque des 60 minutes:

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

L'essentiel est que si vous devez convertir des chaînes en date/heure, vous devez être conscient du format dans lequel vous vous attendez à ce qu'elles soient, puis les reformater de manière appropriée avant de vous fier à CDate() pour les convertir.

64
mwolfe02

Tard dans le jeu ici (mwolfe02 a répondu il y a un an!) Mais le problème est toujours réel, il existe des approches alternatives qui méritent d'être étudiées, et StackOverflow est l'endroit idéal pour les trouver: voici donc ma propre réponse ...

J'ai été déclenché par VBA.IsDate () sur ce problème il y a quelques années et j'ai codé une fonction étendue pour couvrir les cas que VBA.IsDate () gère mal. Le pire est que les flottants et les entiers retournent FALSE à partir de IsDate, même si les séries de dates sont souvent transmises en double (pour DateTime) et en entiers longs (pour les dates).

Un point à noter: votre implémentation peut ne pas nécessiter la possibilité de vérifier les variantes de tableau. Sinon, n'hésitez pas à supprimer le code dans le bloc en retrait qui suit Else ' Comment this out if you don't need to check array variants. Cependant, vous devez savoir que certains systèmes tiers (y compris les clients de données de marché en temps réel) renvoient leurs données dans des tableaux, même des points de données uniques.

Plus d'informations sont dans les commentaires de code.

Voici le 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

Un conseil pour les personnes qui utilisent encore Excel 2003:

Si vous (ou vos utilisateurs) allez appeler IsDateEx () à partir d'une feuille de calcul, placez ces deux lignes, immédiatement sous l'en-tête de la fonction, à l'aide d'un éditeur de texte dans un fichier .bas exporté et réimportation du fichier, car VB Les attributs sont utiles, mais ils ne sont pas accessibles à l'éditeur de code dans l'IDE VBA d'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."

C'est tout une ligne: attention aux sauts de ligne insérés par le navigateur! ... Et cette ligne, qui place isDateEX dans la fonction Wizard dans la catégorie 'Information', à côté de ISNUMBER (), ISERR (), ISTEXT () et ainsi de suite:

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

Utilisez "w\n2" si vous préférez le voir sous les fonctions de date et d'heure: bat l'enfer pour le perdre dans le bourbier des fonctions "utilisées définies" de votre propre code, et tous ces compléments tiers développés par des personnes qui ne font pas assez assez pour aider les utilisateurs occasionnels.

Je ne sais pas si cela fonctionne toujours dans Office 2010.

En outre, vous pourriez avoir besoin de la source pour ArrayDimensions:

Cette déclaration d'API est requise dans l'en-tête du module:

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

… Et voici la fonction elle-même:

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

Veuillez conserver les remerciements dans votre code source: à mesure que vous progressez dans votre carrière de développeur, vous apprécierez que vos propres contributions soient reconnues.

Aussi: Je vous conseille de garder cette déclaration privée. Si vous devez en faire un Sub public dans un autre module, insérez le Option Private Module instruction dans l'en-tête du module. Vous ne voulez vraiment pas que vos utilisateurs appellent une fonction avec CopyMemoryoperations et l'arithmétique des pointeurs.

7
Nigel Heffernan