Tijdstempels, tijdzones en het uur dat twee keer voorkomt

Wat Unix-tijd werkelijk telt, hoe offsets in ISO 8601 verschillen van IANA-tijdzones, wat de zomertijd doet met alarmen en tijdsduren, en waar UTC opslaan het verkeerde antwoord is.

Een tijdstempel beantwoordt een van twee vragen, en de meeste datumbugs komen doordat die twee door elkaar gehaald worden. Hij noemt ofwel een moment, een punt op de tijdlijn waarover elke waarnemer het eens is, ofwel een kloktijd, die niets betekent tot je weet van wiens klok. Een betaling vond op een moment plaats; een tandartsafspraak is een kloktijd. Sla de een op als de ander en het breekt later, met precies één uur, op een zondagochtend.

Unix-tijd telt, hij vertelt niet hoe laat het is

Unix-tijd is het aantal seconden verstreken sinds 1970-01-01 00:00:00 UTC. Die UTC zegt waar het tellen begon; de waarde zelf draagt geen tijdzone. Hetzelfde moment is in Amsterdam, Auckland en Lima hetzelfde getal.

Het probleem zit in de eenheid, dus tel de cijfers.

CijfersVoorbeeldEenheid
101756080000seconden
131756080000000milliseconden

Elke hedendaagse datum heeft 10 cijfers in seconden en 13 in milliseconden, en dat blijft zo tot 2286. Beide misinterpretaties zijn luidruchtig: milliseconden gelezen als seconden komen in het jaar 57626 terecht, seconden gelezen als milliseconden in januari 1970. Een tijdstempelconverter die de geïnterpreteerde datum toont maakt het meteen duidelijk.

De andere beperking is de container. Een telling van seconden in 32 bits met teken eindigt bij 2147483647, oftewel 03:14:07 UTC op 19 januari 2038, en slaat dan om naar negatief en leest als december 1901. Alles wat een vervaldatum over 30 jaar opslaat, rekent daar nu al voorbij. Zonder teken haalt 32 bits het jaar 2106 maar verliest elke datum vóór 1970; 64 bits kent geen praktisch plafond.

ISO 8601 en RFC 3339

De vorm die het onthouden waard is, is 2026-08-25T14:30:00Z: datum, een T, tijd, en dan een zone-aanduiding. RFC 3339 is een striktere variant van ISO 8601 voor internetprotocollen, en vereist die aanduiding. ISO 8601 zelf staat ook weekdatums toe, een vorm zonder scheidingstekens zoals 20260825T143000Z, en tijdstempels zonder offset, dus een API die "ISO 8601" belooft bedoelt meestal de deelverzameling van RFC 3339.

Z betekent een offset van nul. +02:00 betekent dat de lokale klok twee uur vóór loopt op UTC, dus UTC vind je door af te trekken; dat omdraaien is een veelgemaakte fout van twee uur.

StringWat hij benoemt
2026-08-25T14:30:00ZEen moment, ondubbelzinnig
2026-08-25T16:30:00+02:00Hetzelfde moment, op een klok die twee uur voorloopt
2026-08-25T14:30:00Een kloktijd, geen moment
2026-08-25Een datum, 23, 24 of 25 uur lang

Een string zonder Z en zonder offset is dubbelzinnig, en parsers zijn het oneens: sommige nemen UTC aan, andere de zone van het systeem. Daarom verschijnt een waarde die alleen een datum is, zoals een verjaardag, ten westen van Greenwich vaak een dag te vroeg.

Een offset is geen tijdzone

+02:00 is een feit over één moment op één plek. Europe/Amsterdam is een verzameling regels: +01:00 in de winter, +02:00 in de zomer, en in eerdere decennia weer anders. De offset opslaan behoudt het antwoord voor het moment waarop hij gemeten werd en gooit de mogelijkheid weg om enig ander antwoord te berekenen.

Offsets zijn ook niet uniek. Op een zomermiddag dekt +02:00 Amsterdam, Lagos en Johannesburg, dus uit de offset alleen is er geen weg terug naar de regels en geen manier om te weten wat die klok komende december aanwijst. Afkortingen zijn nog erger, want CST benoemt drie verschillende zones. Sla de IANA-identifier op in de vorm Area/Location.

Het uur dat niet bestaat en het uur dat twee keer voorkomt

In de EU gaat de klok vooruit op de laatste zondag van maart en terug op de laatste zondag van oktober. In Amsterdam wordt 02:00 in het voorjaar 03:00, dus elke lokale tijd van 02:00:00 tot 02:59:59 komt die dag niet voor. In het najaar wordt 03:00 weer 02:00, dus 02:30 komt twee keer voor, één keer op +02:00 en een uur later nog eens op +01:00.

  • Terugkerende alarmen. Een taak die op 02:30 lokale tijd staat, heeft op de voorjaarszondag geen geldig tijdstip. Planners verschillen: overslaan, verschuiven naar 03:00, of verschuiven naar 01:30. Op de najaarszondag kan dezelfde taak één of twee keer afgaan, dus controleer de eerstvolgende paar uitvoeringen met een cron-parser.
  • Logbestanden. Een uur aan lokale tijdstempels zonder offset is niet te sorteren, en twee gebeurtenissen met een uur ertussen dragen dezelfde tekst. Log in UTC, of zet de offset op elke regel.
  • Tijdsduren. Van 20:00 op 24 oktober 2026 tot 20:00 de volgende dag, lokale tijd in Amsterdam, is 25 uur. "Een dag optellen" en "24 uur optellen" zijn verschillende bewerkingen, en een datumverschil in kalenderdagen beantwoordt een andere vraag dan een duurconverter die in uren rekent.

De regels veranderen, dus de opslag moet dat overleven

De IANA-tijdzonedatabase, tzdata, bevat deze regels en wordt meerdere keren per jaar uitgebracht, omdat de regels wetgeving zijn: overheden schaffen de zomertijd af, voeren hem in, of verplaatsen de overgangsdata, soms met weken aankondiging. Een vastgezette containerimage, een browser en een taalruntime dragen elk hun eigen kopie en kunnen het op één machine met elkaar oneens zijn. Een toekomstige lokale tijd naar een moment omrekenen is een voorspelling, die pas beslecht wordt wanneer hij aanbreekt.

Soort waardeOpslaan alsReden
Iets dat gebeurd isMoment in UTCHet verleden kan niet opnieuw in de wet worden vastgelegd
Toekomstige afspraak op een kloktijdLokale datum en tijd plus IANA-zone-idDe bedoeling is de kloktijd
Toekomstig moment, vast over zones heenUTCDe bedoeling is het moment
Datum zonder tijdKale datum, geen zone, geen middernachtEen zone eraan hangen laat hem verschuiven

Daarom is "sla gewoon UTC op" verkeerd voor een vergadering volgend jaar. Zet vandaag 09:00 op 30 maart 2027 in Europe/Amsterdam om naar UTC en het antwoord is 07:00Z. Verschuift de overgangsdatum voor die tijd, dan is de vergadering nog steeds om 09:00 lokale tijd maar leest 07:00Z nu als 08:00: de opgeslagen waarde bevroor een voorspelling en gooide de bedoeling weg. Houd lokale tijd plus zone als bron van waarheid; elke UTC-kolom ernaast is een cache.

Datums tonen aan mensen

03/04 is uit de string niet eenduidig te maken. Het is 3 april voor het grootste deel van de wereld en 4 maart in de Verenigde Staten; 03/04/05 voegt een derde onbekende toe. Schrijf de maand als woord voor menselijke uitvoer (4 Apr 2026) en gebruik ISO voor uitwisseling tussen machines. Een Discord-tijdstempeltag omzeilt dit in de chat door weer te geven in de zone en landinstelling van wie hem leest: sla een ondubbelzinnige waarde op en maak hem pas aan de rand op.

Weeknummers hebben hun eigen regel. ISO-weken beginnen op maandag, en week 1 is de week met de eerste donderdag van januari erin, wat gelijkstaat aan de week met 4 januari erin. Zo kan 1 januari in week 52 of 53 van het vorige ISO-jaar vallen, en 29 december in week 1 van het volgende. Een gangbare Noord-Amerikaanse conventie nummert in plaats daarvan vanaf de week met 1 januari erin die op zondag begint, dus dezelfde datum krijgt twee nummers, en het is de moeite waard om de ISO-week van een weekdagcalculator naast een spreadsheet te leggen.

Tijdsduren, maanden en schrikkeljaren

"Een maand later" is geen vast aantal dagen; het is 28, 29, 30 of 31. Een maand optellen bij 31 januari heeft geen goed antwoord, en bibliotheken klemmen het meestal vast op het einde van februari, waardoor de bewerking niet omkeerbaar is: tel een maand op, trek hem er weer af, en 31 januari is nu 28 januari. Afronden is dezelfde valstrik in het klein: een kloktijd afkappen naar het dichtstbijzijnde kwartier en ernaartoe afronden verschillen tot een heel interval.

De schrikkeljaarregel voluit: een jaar is een schrikkeljaar als het deelbaar is door 4, behalve dat jaren die deelbaar zijn door 100 dat niet zijn, tenzij ze ook deelbaar zijn door 400. Dus 1900 was er geen, 2000 wel, en 2100 wordt er geen, wat een gemiddeld jaar van 365,2425 dagen oplevert. Code die alleen op deelbaarheid door 4 test klopt voor elk jaar dat iemand zich herinnert, en daarom houdt hij het tot 2100 vol. Een verjaardag op 29 februari is de verwante lastigheid: het volgende jaar biedt 28 februari en 1 maart, en geen enkele regel kiest daartussen, dus kies er bewust een.