La validazione della data di nascita ha la reputazione di essere facile, e quella reputazione è immeritata. Il campo prende un solo valore, il valore è una data e una data è un problema risolto — tranne che non lo è, perché una data di nascita è anche l’input di un’età, un’età viene confrontata con una soglia, e sia l’età sia la soglia dipendono da che giorno è nel luogo in cui avviene il confronto.
Questo articolo raccoglie i casi limite in cui implementazioni dall’aspetto corretto danno risposte sbagliate, e spiega quali di essi appartengono a una suite di test prima di una release anziché dopo un reclamo.
Cosa rende una data di nascita diversa dalle altre date?
Una data di prenotazione, una data di fattura e una data di consegna stanno tutte vicino al presente e nessuna di esse deve essere convertita in un numero di anni. Una data di nascita è diversa in tre modi contemporaneamente. È lontana nel passato, quindi attraversa decine di cicli di anni bisestili e almeno un cambiamento di regola. Serve a calcolare l’età di una persona, il che significa aritmetica anziché formattazione. Ed è spesso confrontata con una soglia legale, il che significa che la risposta ha conseguenze.
I tre modi in cui una data di nascita differisce da quelle date:
- È lontana nel passato, quindi attraversa decine di cicli di anni bisestili e almeno un cambiamento di regola
- Serve a calcolare l’età di una persona, il che significa aritmetica anziché formattazione
- È spesso confrontata con una soglia legale, il che significa che la risposta ha conseguenze
La conseguenza di sbagliare è asimmetrica. Rifiutare una data di nascita valida allontana un utente legittimo; accettare una data di nascita che avrebbe dovuto fallire lascia passare qualcuno oltre un cancello che esiste per proteggerlo. Entrambe sono veri guasti, e arrivano attraverso percorsi di codice diversi.
Quali date non esistono affatto?
Qualunque data che non esista sul calendario dovrebbe essere rifiutata, il che sembra ovvio finché non si scrive per esteso la regola dell’anno bisestile. Un anno è bisestile quando è divisibile per quattro, tranne che gli anni di fine secolo devono essere divisibili per quattrocento, quindi un anno di fine secolo che sembra divisibile per quattro non è bisestile e un altro lo è.
I casi pratici che contano sono i due anni di fine secolo ai due lati del presente. Uno dei due non è bisestile, quindi il ventinove del suo secondo mese non è una data valida; l’altro è bisestile, quindi lo stesso giorno è valido. Le implementazioni che applicano solo la regola della divisibilità per quattro accettano una data impossibile e ne rifiutano una reale, e l’errore è invisibile per la maggior parte dell’anno.
Lo stesso ragionamento si estende al resto del calendario: i mesi hanno lunghezze diverse, alcuni input arrivano nell’ordine giorno-prima e altri nell’ordine mese-prima, e un anno a due cifre è ambiguo per costruzione. Un campo che accetta un anno a due cifre nudo collocherà alcune persone a un secolo di distanza dalla loro età reale.
Come si calcola davvero l’età?
L’età è una differenza in anni interi, calcolata confrontando la data di nascita con la data corrente componente per componente: sottrai gli anni di nascita, poi sottrai ancora uno se il compleanno non è ancora arrivato quest’anno. Il compleanno stesso è il confine, e la convenzione comune è che una persona compia un anno in più il giorno stesso, non il giorno dopo.
Quella definizione ha una sottigliezza che vale la pena enunciare, perché è lì che vivono la maggior parte degli errori di scarto di uno. Il confronto è tra due date di calendario, non tra due istanti. Due persone nate nello stesso giorno di calendario hanno la stessa età in quel giorno anche se sono nate in momenti diversi, e chi è nato a tarda sera non compie un anno in più a quell’ora del giorno.
Convertire l’una o l’altra data in un conteggio di secondi e dividere è la solita implementazione sbagliata. Deriva attraverso gli anni bisestili, rende la risposta dipendente dall’ora del giorno e produce un’età che cambia a un’ora arbitraria anziché a mezzanotte.
Quale “oggi” usa il controllo?
Qualunque orologio su cui si trovi il server, a meno che qualcuno non abbia deciso diversamente. È la radice di una classe di bug che compaiono solo intorno a mezzanotte e solo per gli utenti di alcuni fusi orari: una data di nascita che soddisfa una soglia di età per il giorno locale dell’utente fallisce per il giorno del server, o viceversa.
Il modo pulito di pensarci è che la data di nascita è una data e non porta affatto un orario. Memorizzala come data semplice senza alcun fuso orario allegato, e fai il confronto dell’età contro una data derivata da una politica esplicita — di solito la data locale dell’utente per un controllo interattivo, e una data di riferimento fissa per tutto ciò che deve essere riproducibile. Mescolare una data di nascita priva di fuso orario con un istante corrente consapevole del fuso orario è il modo in cui entra l’ambiguità.
C’è una trappola correlata per i test. Un test che calcola la sua aspettativa dall’orologio di sistema passa oggi e fallisce il compleanno di qualcuno, o in un anno bisestile, o dopo un cambiamento di politica. I test che asseriscono sull’età hanno bisogno che la data corrente sia iniettata, non letta.
Tutti i calendari concordano sullo stesso giorno?
No. Il computo usato dal calendario predefinito globale non è l’unico in uso, e diverse regioni mantengono sistemi propri per scopi civili. Una data scritta in un calendario non corrisponde alla stessa data scritta in un altro, e lo stesso istante può apparire con anno, mese e giorno diversi a seconda di quale sistema il modulo si aspetta.
Per chi costruisce moduli la regola pratica è essere espliciti anziché furbi: dichiara quale calendario il campo si aspetta, accetta le componenti in un ordine dichiarato e, se il calendario locale di una regione conta per i tuoi utenti, tratta la conversione come una decisione di prodotto con un suo campo anziché come una trasformazione automatica che nessuno vede. Convertire in silenzio è peggio che non convertire, perché il valore sbagliato è indistinguibile da uno digitato.
Dove causano problemi le date segnaposto?
Tre valori predefiniti fanno danni misurabili. Il primo gennaio di un anno tondo è il segnaposto più comune al mondo, e un modulo che lo tratta in silenzio come una vera data di nascita avrà un grappolo di utenti che scoprono tutti di condividere il compleanno. Un valore zero o vuoto che si maschera da data è peggio, perché può calcolare un’età di diversi secoli e superare un controllo sui maggiori di tredici anni fallendo in silenzio ogni altro controllo.
Il terzo è il segnaposto tecnicamente valido e ovviamente sbagliato, come la prima data che un selettore consente. Tutti e tre condividono un sintomo: fanno sembrare completo un record sbagliato, così il codice a valle non ha mai la possibilità di rifiutarlo.
Questo è l’argomento a favore dei record generati nei test. I valori costruiti dal generatore di identità e dati di test sono distribuiti anziché raggruppati, il che significa che un modulo sotto test vede una gamma di date dalla forma reale invece dello stesso segnaposto ripetuto. Sono valori sintetici per i test software e nulla più — non la vera data di nascita di qualcuno, e non un documento che possa essere usato come identità di qualcuno.
Per gli sviluppatori: confini che vale la pena asserire
Fissa la data di riferimento nel test, poi asserisci il confine esatto anziché qualcosa vicino. Quattro valori coprono la maggior parte del rischio per qualunque soglia: la data di nascita che rende la persona esattamente dell’età soglia oggi, quella che è di un solo giorno al di sotto, quella di un solo giorno al di sopra e una nata in un giorno bisestile.
Oltre a quelli, mantieni strette le regole di memorizzazione e confronto: memorizza una data di calendario senza fuso orario, deriva l’età su richiesta anziché memorizzarla e non lasciare mai che una data segnaposto diventi indistinguibile da una reale. Un valore sentinella fuori dall’intervallo plausibile, rifiutato dalla validazione, è preferibile a un valore predefinito plausibile che scivola attraverso i controlli. E tieni i numeri delle politiche nella configurazione, perché le soglie cambiano e una codificata a mano è a una release di distanza dall’essere sbagliata.
Passi successivi
Prendi il controllo dell’età nel tuo prodotto e fai passare quei quattro valori di confine con una data di riferimento fissa; se uno di essi restituisce la risposta sbagliata, il bug è nel confronto e non nel modulo. Poi estrai una varietà di date di nascita generate dal generatore di identità e conferma che il campo le memorizzi immutate, inclusa una all’inizio di un mese e una in un ordine di calendario non predefinito, così che un cambio di locale non possa riordinarle in silenzio. L’articolo sul test di verifica dell’età tratta le soglie con cui quelle date vengono di solito confrontate.