Extraire les anniversaires sur une période de l’année

Un internaute souhaitait une requête SQL paramétrée pour afficher l’Id et la date de naissance des animaux dont l’anniversaire tombe sur une période de l’année.

Création de la table et insertion des données

Public Function CreationTableNaissance()
   Const cInsert As String = "INSERT INTO tNaissance (DateNaissance) VALUES "
 
   With DoCmd
      .SetWarnings False
 
      .RunSQL "CREATE TABLE tNaissance (Id AUTOINCREMENT CONSTRAINT PrimaryKey PRIMARY KEY, DateNaissance DATE);"
 
      .RunSQL cInsert & "(#1/1/2011#);"
      .RunSQL cInsert & "(#15/12/2010#);"
      .RunSQL cInsert & "(#05/07/2009#);"
      .RunSQL cInsert & "(#29/02/2008#);"
      .RunSQL cInsert & "(#26/10/2008#);"
      .RunSQL cInsert & "(#11/01/2009#);"
      .RunSQL cInsert & "(NULL);"
 
      .SetWarnings True
   End With
   MsgBox "Création terminée"
End Function

Analyse du problème
A priori, la requête semble aisée : Il suffit d’extraire les dates de naissance dont le mois et le jour sont dans la période de l’année définie.
Exemple pour la période entre le 15/05 et le 10/07 :
Une erreur souvent commise est de filtrer indépendamment les mois et les jours des dates de naissance
... WHERE (MONTH([DateNaissance]) BETWEEN 5 AND 7) AND (DAY([DateNaissance]) BETWEEN 10 AND 15)
On obtient que les dates de naissance entre le 10 et le 15 des mois de mai, juin et juillet ce qui n’est pas l’objectif recherché.

Pour récupérer toutes les dates sur cette période on peut écrire en condition de filtre de la requête :
...WHERE (MONTH([DateNaissance])*100+DAY([DateNaissance])) BETWEEN 515 AND 710
515 et 710 correspondent au [n° du mois] * 100 + [le jour du mois] de la période étudiée.

Et voilà le tour est joué… mais pas complétement !
En effet, que se passe t’il sur une période entre le 10/07 et le 15/05?
...WHERE (MONTH([DateNaissance])*100+DAY([DateNaissance])) BETWEEN 710 AND 515
Access retourne toutes les dates de naissances entre 515 et 710 soit exactement l’inverse de ce que l’on souhaite mis à part nos deux bornes.

Une solution universelle
Graphiquement, la période du 10/07 au 15/05 est

       /
       | Début période : 10/07
       |
       |
 01/01 |
       |
       |
       | Fin période : 15/05
       /

Ce qui est équivalent sur l’échelle d’une année :

 01/01 |
       |
       | Fin période : 15/05
       /
       /
       | Début période : 10/07
       |
 31/12 |

A l’échelle d’une année, on remarque que la date de fin période est antérieure à la date de début de période lorsque la période s’étale sur ‘deux années’
En résumé, si D:MMJJ du début de période est plus grand que F:MMJJ de fin de période il suffit d’écrire la clause WHERE suivante :
...(MONTH([DateNaissance])*100+DAY([DateNaissance])) NOT BETWEEN 515 + 1 AND 710 - 1
On obtient donc toutes les dates qui ne sont pas entre le 16/05 et le 09/07

Finalement, si Début période <= à Fin période alors on veut les dates entre Début et Fin sinon on veut les dates qui ne sont pas entre Fin + 1 et Début – 1.

La requête paramétrée

PARAMETERS [Debut Periode (MMJJ)] LONG, [Fin Periode (MMJJ)] LONG;

SELECT tnaissance.id,
       tnaissance.datenaissance
FROM   tnaissance
WHERE  (
         [debut periode (mmjj)]  <= [fin periode (mmjj)]
         AND
         MONTH([datenaissance]) * 100 + DAY([datenaissance]) BETWEEN
               [debut periode (mmjj)] AND [fin periode (mmjj)]  
       ) OR (
         [debut periode (mmjj)]  > [fin periode (mmjj)]
         AND
         MONTH([datenaissance]) * 100 + DAY([datenaissance])  NOT BETWEEN
               [fin periode (mmjj)] + 1 AND [debut periode (mmjj)] - 1
       )
ORDER  BY tnaissance.datenaissance;

Lorsque la requete demande les paramètres, écrire 515 pour le 15 mai et 710 pour le 10 juillet.

Autres requêtes paramétrées sur les anniversaires
On souhaite une requete qui nous retourne les dates de naissance dans x jours et sur une période de y jours.
Potentiellement, on peut regretter que la solution précédente teste deux fois le début période par rapport à la fin de période :
(Si debut <= fin Et ... ) OU (Si debut > fin Et ...)
Pour ne tester qu’une fois, on peut s’appuyer sur la fonction IIF() de VBA qui correspond à Si,alors,sinon.

De plus la formule avec MONTH()x100 + DAY() pourrait être simplifieé en utilisant la fonction format(Date,’mmdd’). Il faut utiliser « mmdd » et non « mdd » car la fonction format retourne un string et non un numérique et ‘0515′ (15 mai) est bien < ‘1015’ (15 octobre) mais ‘515’ est > ‘1015’ !

Voici la requête avec IIf() seulement :

PARAMETERS [Dans x Jours] SHORT, [Sur y Jours] SHORT;

SELECT      tnaissance.id,
            tnaissance.datenaissance,
            MONTH(DATE() + [dans x jours]) * 100 + DAY(DATE() +
                  [dans x jours]) AS [Début période],
            MONTH(DATE() + [dans x jours] + [sur y jours] - 1) * 100 +
                  DAY(DATE() + [dans x jours] + [sur y jours] - 1) AS [Fin période]
FROM        tnaissance
WHERE       (  
              ( Iif(MONTH(DATE() + [dans x jours]) * 100 + DAY(DATE() + [dans x jours]) <=
                    MONTH(DATE() + [dans x jours] + [sur y jours] - 1) * 100 +
                          DAY(DATE() + [dans x jours] + [sur y jours] - 1)
                    ,
                    (MONTH([datenaissance]) * 100 + DAY([datenaissance]) ) BETWEEN
                          MONTH(DATE() + [dans x jours]) * 100 + DAY(DATE() + [dans x jours])
                          AND
                          MONTH(DATE() + [dans x jours] + [sur y jours] - 1) * 100 +
                                DAY(DATE() + [dans x jours] + [sur y jours] - 1
                     )
                     ,
                     (MONTH([datenaissance]) * 100 + DAY([datenaissance])) NOT BETWEEN
                            MONTH(DATE() + [dans x jours] + [sur y jours]) * 100 +
                                  DAY(DATE() + [dans x jours] + [sur y jours])
                            AND
                            MONTH(DATE() + [dans x jours]) * 100 + DAY(DATE() + [dans x jours]) - 1
                     )
                ) <> FALSE
              )
ORDER BY tnaissance.datenaissance;

Et la requête avec IIF() et Format() :

PARAMETERS  [Dans x Jours] SHORT, [Sur y Jours] SHORT;
 
SELECT      tnaissance.id,
            tnaissance.datenaissance,
            Format(DATE() + [dans x jours], "mmdd") AS [Début période],
            Format(DATE() + [dans x jours] + [sur y jours] - 1, "mmdd") AS [Fin période]
FROM        tnaissance
WHERE       (
                ( Iif(Format(DATE() + [dans x jours], "mmdd") <=
                      Format(DATE() + [dans x jours] + [sur y jours] - 1, "mmdd")
                      ,
                      Format([datenaissance], "mmdd") BETWEEN
                            Format(DATE() + [dans x jours], "mmdd")
                            AND
                            Format(DATE() + [dans x jours] + [sur y jours] - 1, "mmdd")
                      ,
                      Format([datenaissance], "mmdd") NOT BETWEEN
                             Format(DATE() + [dans x jours] + [sur y jours],"mmdd")
                             AND
                             Format(DATE() + [dans x jours] - 1,"mmdd")
                    )
                ) = TRUE
            )
ORDER BY    tnaissance.datenaissance;

Remarques
La période s’étale du jour Date() + X à Date() + X + Y – 1 car on inclut les bornes et on limite la longueur de la période à seulement Y jours.

Donc, ‘Dans x jours’ doit être >= à 0 et ‘Sur y jours’ doit être >= à 1

On remarquera que si une date de naissance est NULL, la deuxième requête avec Format() la retourne si la période passe par le 1er janvier (car une chaine vide n’est pas dans la période entre « F:mmdd » et « D:mmdd »). On peut s’affranchir de cet artefact en ajoutant dans la clause where une condition supplémentaire qui écarte les dates de naissance NULL.

Performance
La performance des différentes requêtes est excellente.
Pour la tester, voici une fonction qui ajoute 100 000 lignes à la table :

Public Function GenereNaissances()
   Const clMaxNaissances As Long = 100000
   Dim odb As DAO.Database
   Dim ors As DAO.Recordset
   Dim i As Long
 
   Set odb = CurrentDb
   Set ors = odb.OpenRecordset("tNaissance", dbOpenDynaset)
   Randomize
 
   With ors
      For i = 1 To clMaxNaissances
         .AddNew
         !DateNaissance = DateAdd("d", Int((#1/1/2012# - #1/1/1900# + 1) * Rnd()), #1/1/1900#)
         .Update
      Next i
      .Close
   End With
 
   Set ors = Nothing
   Set odb = Nothing
   MsgBox clMaxNaissances & " naissance(s) générée(s)"
End Function

Bon anniversaire !

Philippe

Laisser un commentaire