Technisch artikel

Excel-datumserials in Delphi: 1900 vs 1904 en numFmt

Open een spreadsheet, klik op een cel die 2026-06-19 toont, en de formulebalk leest nog steeds een datum. Lees diezelfde cel vanuit Delphi en u krijgt het getal 46192. Beide beelden kloppen, want Excel heeft nooit een datum in die cel opgeslagen. Het sloeg een serieel getal op, een telling van dagen, en koppelde er een getalnotatie aan die het scherm vertelt de telling als een kalenderdatum te renderen. Er is geen datumtype in de celwaarde. Er is een getal en een weergaveregel, en de weergaveregel is het enige wat een datum van een gewone hoeveelheid onderscheidt

Die scheiding is de wortel van elke datumbug die een spreadsheetbibliotheek moet ontwijken. Een serieel getal alleen zegt niet welke dag het is, want het zegt niet wat dag nul was. Hetzelfde getal betekent twee data die vier jaar uit elkaar liggen, afhankelijk van één enkele werkmapvlag. En een getal dat als datum terug zou moeten lezen, leest terug als een kale hoeveelheid tenzij iets zijn notatie inspecteert en een datumpatroon herkent. Zo is het datummodel in HotXLS opgebouwd, en zo moet het ook

Een datumcel is een getal plus een notatie

Excel slaat een datum op als het aantal dagen sinds een tijdperk, met de tijd van de dag in het gebroken deel. Midden op de dag draagt een serieel getal .5. Het gehele deel is de dagtelling. Niets in de opgeslagen waarde markeert haar als tijdgebonden. Wat haar markeert is de getalnotatie van de cel: ECMA-376 noemt dit een numFmt, en een cel waarvan de notatiecode een datum- of tijdpatroon uitspelt wordt als datum getoond. Haal de notatie eraf en dezelfde cel toont een getal; de onderliggende waarde is nooit veranderd

Daarom levert het lezen van een celwaarde u een Variant op die een varDate kan zijn of een gewone Double, en daarom is de getalnotatie op dezelfde cel het signaal dat bepaalt welke van beide een derde partij bedoelde. Wanneer HotXLS een XLSX-bestand opent, draagt een cel zowel haar Value als haar NumberFormatIndex mee in TXLSXCell, en de notatie-index is wat u raadpleegt om te leren of het getal een datum is

var
  Book: TXLSXWorkbook;
  Cell: TXLSXCell;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('timesheet.xlsx') <> 1 then
      raise Exception.Create('Cannot open workbook');

    Cell := Book.Sheets[0].Cells[1, 1];   // rij 1, kolom 1 (vanaf 1)
    // Value kan als varDate of als kaal numeriek serieel getal binnenkomen;
    // de notatie-index is het signaal dat ze uit elkaar houdt.
    Writeln('raw value : ', VarToStr(Cell.Value));
    Writeln('numFmt idx: ', Cell.NumberFormatIndex);
    Writeln('format    : ', Cell.NumberFormat);
  finally
    Book.Free;
  end;
end;

Twee tijdperken, 1462 dagen uit elkaar

Het standaarddatumsysteem, dat elke Windows-werkmap gebruikt, telt vanaf het uiterste einde van 1899, zodat serieel getal 1 op de eerste dag van 1900 valt. Het andere systeem stamt van de vroege Macintosh en telt vanaf het begin van 1904, dus zijn serieel getal 1 ligt vier jaar en een dag later. Een werkmap legt in één vlag vast welk systeem zij gebruikt. In een OOXML-pakket is die vlag date1904 op het werkmaponderdeel; HotXLS stelt haar beschikbaar als de eigenschap Date1904 van de werkmap

Het gat tussen de twee tijdperken is precies 1462 dagen. Dat zijn vier kalenderjaren, drie van 365 dagen en een van 366, samen 1461, plus er nog een voor de verschuiving van iets meer dan een dag tussen de twee conventies voor dag nul. Het getal ligt vast en u kunt het uit uw hoofd onthouden. Waar het om gaat is dat het niet nul is. Een serieel getal dat uit een 1904-werkmap wordt gekopieerd en onder 1900-regels wordt geïnterpreteerd, of andersom, zet elke datum 1462 dagen mis, wat zich toont als data die er ruim vier jaar naast zitten en makkelijk voor beschadigde data worden aangezien

Tijdlijn die de Excel-datumsystemen 1900 en 1904 vergelijkt, met het vaste gat van 1462 dagen tussen de tijdperken en de spookschrikkeldag 29 februari 1900 zoals HotXLS die in Delphi afhandelt
De systemen 1900 en 1904 tellen vanaf verschillende punten voor dag nul, dus dezelfde kalenderdag ligt precies 1462 seriële getallen uit elkaar; alleen de 1900-as draagt de spookdatum 29 februari op serieel getal 60

Omdat Delphis eigen TDateTime aan de 1900-conventie is verankerd, moet een bibliotheek die Excel-seriële getallen op TDateTime afbeeldt in beide richtingen met 1462 verschuiven zodra de werkmap als 1904 is gemarkeerd. Leest u een 1904-serieel getal, trek dan 1462 af voordat u het als een TDateTime behandelt; schrijft u een TDateTime in een 1904-werkmap, trek dan 1462 van het seriële getal af zodat Excel de dag rendert die u bedoelde. HotXLS past deze verschuiving intern toe wanneer het datumwaarden serialiseert voor een werkmap waarvan Date1904 is gezet, zodat de waarde die u als TDateTime toewijst op het scherm op dezelfde kalenderdag terugkomt

De opzettelijke schrikkeljaargril van 1900

Er zit een beroemde rimpel in het 1900-systeem. Excel behandelt 1900 als een schrikkeljaar en accepteert 29 februari 1900 als echte datum, serieel getal 60. Het jaar 1900 was geen schrikkeljaar, want eeuwjaren zijn alleen schrikkeljaren wanneer ze deelbaar zijn door 400, en 1900 is dat niet. De spookdag is een opzettelijk compatibiliteitsgedrag, geërfd van een vroege spreadsheet die met de bug werd geleverd, sindsdien behouden zodat de rekenkunde met seriële getallen over tientallen jaren aan bestanden identiek blijft

Het praktische gevolg is klein maar echt: voor elke datum op of na 1 maart 1900 ligt het seriële getal één hoger dan een strikt correcte dagtelling zou geven, omdat de niet-bestaande 29 februari een nummer opsoupeerde. Een spreadsheetbibliotheek reproduceert de gril in plaats van haar te repareren, omdat de rekenkunde van Excel exact evenaren de hele opdracht is. Haar corrigeren zou elke moderne datum een dag laten afwijken van wat Excel toont, wat een slechtere uitkomst is dan een off-by-one van veertigduizend dagen oud die geen enkele datum in zakelijk gebruik ooit raakt. Het 1904-systeem heeft geen equivalente spookdag, wat een reden is waarom een paar bedrijven het historisch verkozen

Een datum herkennen aan numFmt

Wanneer een getal binnenkomt uit een bestand dat iemand anders schreef, is zijn notatie het enige bewijs dat het een datum is. ECMA-376 kent een blok ingebouwde notatie-ids toe waarvan de betekenis door de specificatie vastligt, en de datum- en tijdnotaties bezetten bekende bereiken. De ids 14 tot en met 22 zijn de datum- en tijdnotaties voor de algemene locale, de vertrouwde m/d/yyyy, h:mm en hun verwanten. De ids 45 tot en met 47 zijn de notaties voor verstreken tijd. Twee verdere banden, 27 tot en met 36 en 50 tot en met 58, zijn de locale-specifieke datum- en tijdnotaties voor CJK-kalenders, gedefinieerd in ECMA-376 18.8.30. Een cel waarvan de getalnotatie-id in een van deze bereiken valt is een datum- of tijdcel

Ingebouwde ids dekken de gangbare gevallen maar niet de aangepaste. Wanneer een werkmap haar eigen notatiecode definieert, bijvoorbeeld een niet-standaard volgorde of een gelokaliseerde maandnaam, ligt de id boven het ingebouwde bereik en wijst zij naar de getalnotatietabel van de werkmap. Voor die gevallen betekent een datum herkennen dat u de tekenreeks van de notatiecode leest en naar datumtokens zoekt. HotXLS vouwt beide controles in één interne predicaatfunctie, XlsxNumFmtIsDate, die direct waar retourneert voor de ingebouwde datumbereiken en anders de aangepaste notatiecode via XlsxFormatCodeIsDate parseert. De publieke kant daarvan is de NumberFormat-tekenreeks van de cel en haar NumberFormatIndex, die u zowel de opgeloste notatiecode als de te testen id geven

Waarom de notatieparser niet zomaar op d en m kan scannen

Een notatiecode op datumtokens parseren lijkt triviaal totdat u zich herinnert wat er nog meer in een getalnotatie woont. Een naïeve zoektocht naar de letters die data spellen, de d, m, y, h en s van dag, maand, jaar, uur en seconde, gaat de mist in bij twee structuren die helemaal geen datumtokens zijn

De eerste is de geciteerde tekenreeksliteral. Een getalnotatie kan letterlijke tekst tussen dubbele aanhalingstekens insluiten, dus een financiële notatie als #,##0 "MM" hangt de tekens M en M achter een getal zonder enige tijdgebonden betekenis. Een scanner die de letters binnen de aanhalingstekens als maandtokens telt, zou die valutanotatie ten onrechte als datum markeren. De tweede is de haakjessectie. Getalnotaties dragen directieven tussen vierkante haken, kleurnamen zoals [Red], vergelijkingscondities zoals [>1000], localetags, en de markeringen voor verstreken tijd [h] en [mm]. Sommige haakjesinhoud bevat datumletters en andere niet, en tekst tussen haken hetzelfde behandelen als de romp van de notatie leidt tot zowel valse positieven als gemiste gevallen

De juiste parser loopt de notatiecode teken voor teken door, houdt bij of hij zich binnen een geciteerde literal bevindt en hoe diep hij in haakjesnesting zit, en respecteert ook de backslash-escape die één volgend teken citeert. Alleen een niet-geëscapete datumletter die buiten elke tekenreeksliteral en buiten elke haakjessectie wordt gevonden telt als een echt datumtoken. Precies zo scant XlsxFormatCodeIsDate: een aanhalingsteken kantelt een in-literal-toestand die tokendetectie onderdrukt tot het sluitende aanhalingsteken, een backslash slaat het volgende teken over, en een teller voor haakjesdiepte onderdrukt detectie binnen [...]-stukken. De opbrengst is dat #,##0 "MM" correct als getalnotatie wordt gelezen, terwijl een bondige aangepaste code die niets bevat dan één enkele m of d buiten aanhalingstekens nog steeds correct als datum wordt herkend

Datumdetectie in twee fasen voor HotXLS-cellen: eerst de bereiken van ingebouwde getalnotatie-ids, daarna een scan van aangepaste notatiecodes in Delphi die aanhalingstekens en haken respecteert
Ingebouwde notatie-ids beslechten de meeste cellen meteen; aangepaste codes vallen door naar een tekenwandeling die geciteerde literals, haakjessecties en backslash-escapes negeert

Data uit bestanden van derden lezen

Al het bovenstaande komt samen in één workflow: een getal dat een andere applicatie schreef terugbrengen tot een datum die u kunt vertrouwen. Het seriële getal geeft u de dagtelling, de vlag Date1904 van de werkmap vertelt u vanaf welk tijdperk de telling wordt gemeten, en de getalnotatie-id of aangepaste code van de cel is het enige bewijs dat het getal überhaupt als datum bedoeld was. Laat er één van de drie vallen en u krijgt een plausibel fout antwoord in plaats van een zichtbare fout

Workflow voor het lezen van data uit spreadsheets van derden met HotXLS: de getalnotatie moet een datum declareren en de vlag Date1904 bepaalt of het seriële getal de verschuiving van 1462 dagen nodig heeft voordat het een Delphi TDateTime wordt
Drie feiten bepalen elke conversie: de notatie moet een datum claimen, de vlag Date1904 legt het tijdperk vast, en een kale Double krijgt de correctie van 1462 dagen terwijl een varDate ongewijzigd wordt gebruikt
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Cell: TXLSXCell;
  r: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('vendor-export.xlsx') <> 1 then
      raise Exception.Create('Cannot open export');

    // De 1904-vlag geldt voor de hele werkmap: lees haar eenmaal en pas haar toe
    // op elk serieel getal dat de werkmap teruggeeft.
    if Book.Date1904 then
      Writeln('workbook uses the 1904 date system')
    else
      Writeln('workbook uses the 1900 date system');

    Sheet := Book.Sheets[0];
    for r := 1 to 10 do
    begin
      Cell := Sheet.Cells[r, 1];
      // Een datum is pas een datum als haar notatie dat zegt; dezelfde numerieke
      // waarde met een gewone notatie is slechts een hoeveelheid.
      Writeln(Format('row %d  value=%s  numFmt=%d  code="%s"',
        [r, VarToStr(Cell.Value), Cell.NumberFormatIndex, Cell.NumberFormat]));
    end;
  finally
    Book.Free;
  end;
end;

De verouderde BIFF-kant heeft nog een extra valstrik die het benoemen waard is. In een oudere .xls-stream kan een reeks aangrenzende numerieke cellen in één record voor meerdere cellen worden gepakt, de MULRK, die meerdere waarden met hun notatieverwijzingen in één structuur opslaat. Datumcellen die zo zijn opgeslagen zijn er niet minder datum om, dus dezelfde notatie-id-test moet in het multicelrecord reiken en per cel worden toegepast, en de 1904-verschuiving beheerst nog steeds elk serieel getal dat het oplevert. Een lezer die alleen losstaande getalrecords inspecteert en de gepakte overslaat, verandert een kolom data stilzwijgend in een kolom gehele getallen

Seriële getallen in de praktijk op TDateTime afbeelden

Zodra de notatiecontrole een datum bevestigt en de vlag Date1904 bekend is, is de conversie mechanisch. Een waarde die HotXLS al als varDate teruggeeft is een TDateTime die u direct kunt gebruiken. Een waarde die als kale Double binnenkomt, wat gebeurt wanneer de bron een serieel getal zonder herkende datumnotatie schreef, wordt omgezet door haar als dagtelling op de 1900-as te lezen en, voor een 1904-werkmap, eerst de verschuiving van 1462 dagen af te trekken zodat de tijdperken samenvallen. Andersom slaat het toewijzen van een TDateTime aan een cel het op 1900 gebaseerde seriële getal op, en past HotXLS bij het opslaan dezelfde verschuiving van 1462 dagen toe wanneer de werkmap als 1904 is gemarkeerd, zodat het opgeslagen bestand de datum toont die u bedoelde in plaats van een die vier jaar afdrijft

Zet de vlag bewust wanneer u een werkmap genereert. De standaard laat Date1904 op onwaar, wat overeenkomt met Excel voor Windows en vrijwel altijd is wat u wilt; zet haar alleen op waar wanneer u een werkmap van Mac-origine reproduceert of wanneer een systeem verderop specifiek de 1904-as verwacht. De ene regel die de hele klasse van vierjaarsfouten voorkomt is consistentie: kies het tijdperk één keer per werkmap, schrijf elke datum eronder, en lees elk serieel getal terug onder de vlag die het bestand daadwerkelijk draagt

Data zijn één kolom in een breder verhaal over wat een cel werkelijk bevat. De naburige metadatalaag, de titel en auteur en tijdstempels die naast het raster meereizen, wordt behandeld in ons artikel over werkmapmetadata en documenteigenschappen, waar dezelfde waarden Created en Modified als TDateTime worden opgeslagen met dezelfde conventie dat niet-ingesteld gelijk is aan nul. Wanneer een datum het resultaat is van een berekening in plaats van een opgeslagen waarde, bepalen de evaluatieregels in ons artikel over de formulemotor en aangepaste functies het seriële getal dat de notatie vervolgens rendert. Beide werken op hetzelfde datummodel dat wordt geleverd in het HotXLS Delphi Component voor Delphi en C++Builder, dat XLS- en XLSX-data leest en schrijft zonder Excel-automatisering