La validation de la date de naissance a la réputation d’être facile, et cette réputation est imméritée. Le champ prend une valeur, cette valeur est une date, et une date est un problème résolu — sauf qu’il ne l’est pas, car une date de naissance est aussi l’entrée d’un âge, un âge est comparé à un seuil, et tant l’âge que le seuil dépendent du jour où l’on se trouve à l’endroit où la comparaison a lieu.
Cet article rassemble les cas limites où des implémentations d’apparence correcte donnent de mauvaises réponses, et explique lesquels ont leur place dans une suite de tests avant une publication plutôt qu’après une réclamation.
Qu’est-ce qui distingue une date de naissance des autres dates ?
Une date de réservation, une date de facture et une date de livraison se situent toutes près du présent et aucune n’a besoin d’être convertie en un nombre d’années. Une date de naissance diffère sur trois plans à la fois. Elle est loin dans le passé, elle couvre donc des dizaines de cycles bissextiles et au moins un changement de règle. Elle sert à calculer l’âge d’une personne, ce qui implique de l’arithmétique plutôt que de la mise en forme. Et elle est souvent comparée à un seuil légal, ce qui signifie que la réponse a des conséquences.
Les trois façons dont une date de naissance diffère de ces dates :
- Elle est loin dans le passé, elle couvre donc des dizaines de cycles bissextiles et au moins un changement de règle
- Elle sert à calculer l’âge d’une personne, ce qui implique de l’arithmétique plutôt que de la mise en forme
- Elle est souvent comparée à un seuil légal, ce qui signifie que la réponse a des conséquences
La conséquence d’une erreur est asymétrique. Rejeter une date de naissance valide éconduit un utilisateur légitime ; accepter une date de naissance qui aurait dû échouer laisse quelqu’un franchir une barrière qui existe pour le protéger. Ce sont deux échecs réels, et ils arrivent par des chemins de code différents.
Quelles dates n’existent pas du tout ?
Toute date qui n’existe pas dans le calendrier devrait être rejetée, ce qui semble évident jusqu’à ce que la règle des années bissextiles soit écrite. Une année est bissextile lorsqu’elle est divisible par quatre, sauf que les années séculaires doivent être divisibles par quatre cents, si bien qu’une année séculaire qui semble divisible par quatre ne l’est pas et qu’une autre l’est.
Les cas pratiques qui comptent sont les deux années séculaires de part et d’autre du présent. L’une n’est pas bissextile, le vingt-neuvième jour de son deuxième mois n’est donc pas une date valide ; l’autre est bissextile, le même jour est donc valide. Les implémentations qui n’appliquent que la règle de divisibilité par quatre acceptent une date impossible et rejettent une date réelle, et l’erreur est invisible pendant la majeure partie de l’année.
Le même raisonnement s’étend au reste du calendrier : les mois ont des longueurs différentes, certaines entrées arrivent au format jour-mois et d’autres au format mois-jour, et une année à deux chiffres est ambiguë par construction. Un champ qui accepte une année nue à deux chiffres placera certaines personnes à un siècle de leur âge réel.
Comment l’âge est-il réellement calculé ?
L’âge est une différence en années entières, calculée en comparant la date de naissance à la date courante composant par composant : soustrayez les années de naissance, puis soustrayez un de plus si l’anniversaire n’est pas encore arrivé cette année. L’anniversaire lui-même est la frontière, et la convention courante veut qu’une personne ait un an de plus le jour même, et non le lendemain.
Cette définition comporte une subtilité qui mérite d’être énoncée, car c’est là que vivent la plupart des erreurs de décalage d’une unité. La comparaison porte sur deux dates de calendrier, et non sur deux instants. Deux personnes nées le même jour calendaire ont le même âge ce jour-là même si elles sont nées à des heures différentes, et quelqu’un né tard le soir ne prend pas un an de plus à cette heure de la journée.
Convertir l’une ou l’autre date en un nombre de secondes puis diviser est l’implémentation erronée habituelle. Elle dérive à travers les années bissextiles, elle fait dépendre la réponse de l’heure de la journée, et elle produit un âge qui change à une heure arbitraire plutôt qu’à minuit.
Quel « aujourd’hui » le contrôle utilise-t-il ?
Celui de l’horloge sur laquelle se trouve le serveur, à moins que quelqu’un en ait décidé autrement. C’est la racine d’une classe de bugs qui n’apparaissent qu’autour de minuit et uniquement pour les utilisateurs de certains fuseaux horaires : une date de naissance qui satisfait un seuil d’âge pour le jour local de l’utilisateur échoue pour le jour du serveur, ou l’inverse.
La manière propre de l’aborder est de considérer que la date de naissance est une date et ne porte aucune heure. Stockez-la comme une date simple sans fuseau horaire attaché, et effectuez la comparaison d’âge par rapport à une date dérivée d’une politique explicite — généralement la date locale de l’utilisateur pour un contrôle interactif, et une date de référence fixe pour tout ce qui doit être reproductible. Mélanger une date de naissance sans fuseau horaire avec un instant courant sensible au fuseau est la manière dont l’ambiguïté s’introduit.
Il existe un piège connexe pour les tests. Un test qui calcule son attendu à partir de l’horloge système passe aujourd’hui et échoue le jour de l’anniversaire de quelqu’un, ou une année bissextile, ou après un changement de politique. Les tests qui affirment un âge ont besoin que la date courante soit injectée, et non lue.
Tous les calendriers s’accordent-ils sur le même jour ?
Non. Le décompte utilisé par le calendrier mondial par défaut n’est pas le seul en usage, et plusieurs régions maintiennent leurs propres systèmes à des fins civiles. Une date écrite dans un calendrier ne correspond pas à la même date écrite dans un autre, et le même instant peut apparaître avec une année, un mois et un jour différents selon le système que le formulaire attend.
Pour un concepteur de formulaire, la règle pratique est d’être explicite plutôt que malin : indiquez quel calendrier le champ attend, acceptez les composants dans un ordre énoncé, et si le calendrier local d’une région compte pour vos utilisateurs, traitez la conversion comme une décision produit dotée de son propre champ plutôt que comme une transformation automatique que personne ne voit. Convertir en silence est pire que ne pas convertir, car la valeur erronée est indiscernable d’une valeur saisie.
Où les dates de remplacement causent-elles des ennuis ?
Trois valeurs par défaut font des dégâts mesurables. Le premier janvier d’une année ronde est la valeur de remplacement la plus courante au monde, et un formulaire qui la traite discrètement comme une date de naissance réelle aura un groupe d’utilisateurs qui s’avèrent tous partager le même anniversaire. Une valeur nulle ou vide déguisée en date est pire, car elle peut se calculer en un âge de plusieurs siècles et passer un contrôle de plus de treize ans tout en échouant silencieusement à tous les autres.
La troisième est la valeur de remplacement qui est techniquement valide et manifestement fausse, comme la date la plus ancienne qu’un sélecteur autorise. Toutes les trois partagent un symptôme : elles font paraître un mauvais enregistrement complet, de sorte que le code en aval n’a jamais la possibilité de le rejeter.
C’est l’argument en faveur des enregistrements générés dans les tests. Les valeurs construites par le générateur d’identités et de données de test sont réparties plutôt que regroupées, ce qui signifie qu’un formulaire en cours de test voit une gamme de dates de forme réelle au lieu de la même valeur de remplacement encore et encore. Ce sont des valeurs synthétiques pour les tests logiciels et rien de plus — pas la date de naissance réelle de quiconque, et pas un document qui pourrait servir d’identité à quelqu’un.
Pour les développeurs : les frontières qui valent une assertion
Figez la date de référence dans le test, puis affirmez la frontière exacte plutôt que quelque chose à proximité. Quatre valeurs couvrent l’essentiel du risque pour n’importe quel seuil : la date de naissance qui rend la personne exactement à l’âge seuil aujourd’hui, celle qui s’en écarte d’un seul jour en moins, celle qui s’en écarte d’un seul jour en plus, et une personne née un jour bissextile.
Au-delà, gardez les règles de stockage et de comparaison étroites : stockez une date de calendrier sans fuseau horaire, dérivez l’âge à la demande plutôt que de le stocker, et ne laissez jamais une date de remplacement devenir indiscernable d’une date réelle. Une valeur sentinelle hors de la plage plausible, rejetée par la validation, est préférable à une valeur par défaut plausible qui passe à travers les contrôles. Et gardez les nombres de politique dans la configuration, car les seuils changent et un seuil codé en dur est à une publication de devenir faux.
Étapes suivantes
Prenez le contrôle d’âge de votre produit et faites-y passer ces quatre valeurs frontières avec une date de référence fixe ; si l’une d’elles renvoie une mauvaise réponse, le bug est dans la comparaison et non dans le formulaire. Récupérez ensuite un éventail de dates de naissance générées dans le générateur d’identités et confirmez que le champ les stocke sans modification, y compris une en début de mois et une dans un ordre de calendrier non par défaut, afin qu’un changement de locale ne puisse pas les réordonner en silence. L’article sur les tests de vérification d’âge couvre les seuils auxquels ces dates sont habituellement comparées.