Validatie van een geboortedatum heeft de reputatie makkelijk te zijn, en die reputatie is onverdiend. Het veld neemt één waarde, de waarde is een datum, en een datum is een opgelost probleem — behalve dat het dat niet is, want een geboortedatum is ook de invoer voor een leeftijd, een leeftijd wordt vergeleken met een drempel, en zowel de leeftijd als de drempel hangen af van welke dag het is waar de vergelijking plaatsvindt.
Dit artikel verzamelt de randen waar correct uitziende implementaties verkeerde antwoorden geven, en legt uit welke ervan in een testsuite horen vóór een release in plaats van erna na een klacht.
Wat maakt een geboortedatum anders dan andere datums?
Een boekingsdatum, een factuurdatum en een bezorgdatum liggen allemaal dicht bij het heden en geen ervan hoeft te worden omgerekend naar een aantal jaren. Een geboortedatum verschilt op drie manieren tegelijk. Hij ligt ver in het verleden, dus hij overspant tientallen schrikkeljaarcycli en minstens één regelwijziging. Hij wordt gebruikt om iemands leeftijd te berekenen, wat rekenkunde betekent in plaats van opmaak. En hij wordt vaak vergeleken met een wettelijke drempel, wat betekent dat het antwoord consequenties heeft.
De drie manieren waarop een geboortedatum van die datums verschilt:
- Hij ligt ver in het verleden, dus hij overspant tientallen schrikkeljaarcycli en minstens één regelwijziging
- Hij wordt gebruikt om iemands leeftijd te berekenen, wat rekenkunde betekent in plaats van opmaak
- Hij wordt vaak vergeleken met een wettelijke drempel, wat betekent dat het antwoord consequenties heeft
De consequentie van een fout is asymmetrisch. Een geldige geboortedatum afwijzen houdt een legitieme gebruiker tegen; een geboortedatum accepteren die had moeten falen, laat iemand door een poort die bestaat om hem te beschermen. Beide zijn echte fouten, en ze komen via verschillende codepaden binnen.
Welke datums bestaan helemaal niet?
Elke datum die niet op de kalender bestaat, moet worden afgewezen, wat vanzelfsprekend klinkt totdat de schrikkeljaarregel wordt uitgeschreven. Een jaar is een schrikkeljaar wanneer het deelbaar is door vier, behalve dat eeuwjaren deelbaar moeten zijn door vierhonderd, dus één eeuwjaar dat deelbaar lijkt door vier is geen schrikkeljaar en een ander is dat wel.
De praktische gevallen die ertoe doen, zijn de twee eeuwjaren aan weerszijden van het heden. Een ervan is geen schrikkeljaar, dus de negenentwintigste van zijn tweede maand is geen geldige datum; de andere is een schrikkeljaar, dus dezelfde dag is geldig. Implementaties die alleen de deelbaar-door-vier-regel toepassen, accepteren één onmogelijke datum en wijzen één echte af, en de fout is het grootste deel van het jaar onzichtbaar.
Dezelfde redenering strekt zich uit tot de rest van de kalender: maanden hebben verschillende lengtes, sommige invoer komt in dag-eerst-volgorde en sommige in maand-eerst-volgorde, en een tweecijferig jaar is per constructie dubbelzinnig. Een veld dat een kaal tweecijferig jaar accepteert, plaatst sommige mensen een eeuw naast hun echte leeftijd.
Hoe wordt leeftijd eigenlijk berekend?
Leeftijd is een verschil in hele jaren, berekend door de geboortedatum met de huidige datum component voor component te vergelijken: trek de geboortejaren af en trek er nog één af als de verjaardag dit jaar nog niet is geweest. De verjaardag zelf is de grens, en de gangbare conventie is dat iemand op die dag een jaar ouder is, niet de dag erna.
Die definitie heeft een subtiliteit die het vermelden waard is, want daar leven de meeste off-by-one-fouten. De vergelijking is tussen twee kalenderdatums, niet tussen twee tijdstippen. Twee mensen die op dezelfde kalenderdag zijn geboren, zijn op die dag even oud, zelfs als ze op verschillende tijden zijn geboren, en iemand die laat op de avond is geboren, wordt niet op dat tijdstip een jaar ouder.
Ofwel datum omzetten naar een aantal seconden en delen is de gebruikelijke verkeerde implementatie. Het wijkt af over schrikkeljaren, het laat het antwoord afhangen van het tijdstip van de dag, en het levert een leeftijd op die op een willekeurig uur verandert in plaats van om middernacht.
Welke “vandaag” gebruikt de controle?
Welke klok de server toevallig ook gebruikt, tenzij iemand anders heeft besloten. Dat is de wortel van een klasse bugs die alleen rond middernacht verschijnen en alleen voor gebruikers in sommige tijdzones: een geboortedatum die een leeftijdsdrempel haalt voor de lokale dag van de gebruiker, faalt voor de dag van de server, of omgekeerd.
De nette manier om erover te denken is dat de geboortedatum een datum is en helemaal geen tijd draagt. Sla hem op als een gewone datum zonder tijdzone eraan, en voer de leeftijdsvergelijking uit tegen een datum die is afgeleid van een expliciet beleid — meestal de lokale datum van de gebruiker voor een interactieve controle, en een vaste referentiedatum voor alles wat reproduceerbaar moet zijn. Een tijdzoneloze geboortedatum mengen met een tijdzonebewuste huidige tijd is hoe de dubbelzinnigheid binnenkomt.
Er is een gerelateerde valkuil voor tests. Een test die zijn verwachting berekent uit de systeemklok, slaagt vandaag en faalt op iemands verjaardag, of in een schrikkeljaar, of na een beleidswijziging. Tests die op leeftijd asserten, hebben de huidige datum geïnjecteerd nodig, niet gelezen.
Zijn alle kalenders het eens over dezelfde dag?
Nee. De jaartelling die de wereldwijde standaardkalender gebruikt, is niet de enige in gebruik, en verschillende regio’s onderhouden hun eigen systemen voor civiele doeleinden. Een datum geschreven in de ene kalender komt niet overeen met dezelfde geschreven datum in een andere, en hetzelfde tijdstip kan met een ander jaar, andere maand en andere dag verschijnen, afhankelijk van welk systeem het formulier verwacht.
Voor een formulierbouwer is de praktische regel expliciet zijn in plaats van slim: vermeld welke kalender het veld verwacht, accepteer de componenten in een vermelde volgorde, en als de lokale kalender van een regio belangrijk is voor uw gebruikers, behandel de conversie dan als een productbeslissing met een eigen veld in plaats van een automatische transformatie die niemand kan zien. Stilletjes converteren is erger dan niet converteren, want de verkeerde waarde is niet te onderscheiden van een getypte.
Waar veroorzaken plaatshouderdatums problemen?
Drie standaardwaarden richten meetbare schade aan. De eerste januari van een rond jaar is de meest voorkomende plaatshouder ter wereld, en een formulier dat die stilletjes als echte geboortedatum behandelt, krijgt een cluster gebruikers die allemaal dezelfde verjaardag blijken te hebben. Een nul- of lege waarde die zich voordoet als datum is erger, want die kan uitkomen op een leeftijd van meerdere eeuwen en een ouder-dan-dertien-controle halen terwijl hij elke andere controle stilletjes faalt.
De derde is de plaatshouder die technisch geldig en duidelijk verkeerd is, zoals de vroegste datum die een datumkiezer toestaat. Alle drie delen een symptoom: ze laten een slecht record compleet lijken, zodat stroomafwaartse code nooit de kans krijgt het af te wijzen.
Dat is het argument voor gegenereerde records in tests. Waarden gebouwd door de generator voor identiteits- en testgegevens zijn gespreid in plaats van geclusterd, wat betekent dat een formulier onder test een reeks echt gevormde datums ziet in plaats van steeds dezelfde plaatshouder. Het zijn synthetische waarden voor softwaretesten en niets meer — niet iemands echte geboortedatum, en geen document dat als iemands identiteit kan worden gebruikt.
Voor ontwikkelaars: grenzen die het asserten waard zijn
Zet de referentiedatum vast in de test en assert dan de exacte grens in plaats van iets in de buurt. Vier waarden dekken het meeste risico voor elke drempel: de geboortedatum die de persoon vandaag precies de drempelleeftijd maakt, degene die één dag tekortschiet, degene die één dag te ver is, en één geboren op een schrikkeldag.
Houd daarbuiten de opslag- en vergelijkingsregels nauw: sla een kalenderdatum op zonder tijdzone, leid leeftijd op aanvraag af in plaats van die op te slaan, en laat een plaatshouderdatum nooit niet te onderscheiden worden van een echte. Een schildwachtwaarde buiten het plausibele bereik, afgewezen door validatie, verdient de voorkeur boven een plausibele standaardwaarde die door de controles glipt. En houd beleidsgetallen in configuratie, want drempels veranderen en een hardgecodeerde is één release verwijderd van onjuist zijn.
Volgende stappen
Neem de leeftijdscontrole in uw product en voer die vier grenswaarden erdoor met een vaste referentiedatum; als een ervan het verkeerde antwoord teruggeeft, zit de bug in de vergelijking en niet in het formulier. Haal dan een spreiding van gegenereerde geboortedatums uit de identiteitsgenerator en bevestig dat het veld ze ongewijzigd opslaat, inclusief één aan het begin van een maand en één in een niet-standaard kalendervolgorde, zodat een locale-wijziging ze niet stilletjes kan herordenen. Het artikel over leeftijdsverificatie testen behandelt de drempels waarmee die datums gewoonlijk worden vergeleken.