Åbn et regneark, klik på en celle, der viser 2026-06-19, og formellinjen viser stadig en dato. Læs den samme celle fra Delphi, og du får tallet 46192. Begge visninger er korrekte, fordi Excel aldrig gemte en dato i denne celle. Den gemte et serienummer, en optælling af dage, og tilknyttede et talformat, der fortæller skærmen, at den skal rendere optællingen som en kalenderdato. Der er ingen datotype i celleværdien. Der er et tal og en visningsregel, og visningsreglen er det eneste, der adskiller en dato fra en almindelig mængde
Denne adskillelse er roden til enhver datofejl, et regnearksbibliotek skal undgå. Et serienummer alene fortæller ikke, hvilken dag det er, fordi det ikke fortæller, hvad dag nul var. Det samme tal betyder to datoer med fire års mellemrum afhængigt af et enkelt projektmappeflag. Og et tal, der skal læses som en dato, vil blive læst som en ren mængde, medmindre noget inspicerer dets format og genkender et datomønster. Det er sådan, datomodellen i HotXLS er bygget, og hvorfor den skal være det
En datocelle er et tal plus et format
Excel gemmer en dato som antallet af dage siden en epoke, med tidspunktet på dagen i den brøkdelte del. Middag på en serie bærer .5. Heltalsdelen er dagsoptællingen. Intet i den gemte værdi markerer den som tidsmæssig. Det, der markerer den, er cellens talformat: ECMA-376 kalder dette en numFmt, og en celle, hvis formatkode staver et dato- eller tidsmønster, vises som en dato. Fjern formatet, og den samme celle viser et tal; den underliggende værdi ændrede sig aldrig
Dette er grunden til, at læsning af en celleværdi giver dig en Variant, som kan være en varDate eller en almindelig Double, og hvorfor talformatet på den samme celle er det signal, der afgør, hvad en tredjepart mente. Når HotXLS åbner en XLSX-fil, en celle bærer både sin Value og sin NumberFormatIndex ind i TXLSXCell, og formatindekset er det, du konsulterer for at finde ud af, om tallet er en dato
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]; // række 1, kolonne 1 (1-baseret)
// Value kan ankomme som varDate eller som et almindeligt numerisk serienummer;
// formatindekset er det signal, der adskiller dem.
Writeln('raw value : ', VarToStr(Cell.Value));
Writeln('numFmt idx: ', Cell.NumberFormatIndex);
Writeln('format : ', Cell.NumberFormat);
finally
Book.Free;
end;
end;
To epoker, 1462 dage fra hinanden
Standarddatosystemet, det som alle Windows-projektmapper bruger, tæller fra slutningen af 1899, så serienummeret 1 falder på den første dag i 1900. Det andet system sporer tilbage til den tidlige Macintosh og tæller fra starten af 1904, så dets serienummer 1 er fire år og en dag senere. En projektmappe registrerer, hvilket system den bruger, i ét flag. I en OOXML-pakke er dette flag date1904 på projektmappedelen; HotXLS eksponerer det som projektmappens egenskab Date1904
Gabet mellem de to epoker er præcis 1462 dage. Det er fire kalenderår, tre på 365 dage og et på 366, i alt 1461, plus et mere for afvigelsen på en dags tid mellem de to dag-nul-konventioner. Tallet er fast, og du kan have det i hovedet. Dets vigtighed er, at det ikke er nul. Et serienummer kopieret ud af en 1904-projektmappe og fortolket under 1900-regler, eller omvendt, lander enhver dato 1462 dage forkert, hvilket præsenterer sig som datoer, der er forkerte med lige over fire år, og er let at forveksle med beskadigede data
Fordi Delphis egen TDateTime er forankret til 1900-konventionen, skal et bibliotek, der kortlægger Excel-serienumre til TDateTime, forskyde med 1462 i begge retninger, hver gang projektmappen er markeret 1904. Når man læser et 1904-serienummer, trækker man 1462 fra, før det behandles som en TDateTime; når man skriver en TDateTime ind i en 1904-projektmappe, trækker man 1462 fra serienummeret, så Excel viser den dag, man mente. HotXLS anvender denne forskydning internt, når den serialiserer datoværdier for en projektmappe, hvis Date1904 er sat, så den værdi, du tildeler som en TDateTime, ruller frem og tilbage til den samme kalenderdag på skærmen
Den bevidste 1900-skudårs-særhed
Der er en berømt krølle i 1900-systemet. Excel behandler 1900 som et skudår og accepterer 29. februar 1900 som en reel dato, serienummer 60. Året 1900 var ikke et skudår, fordi århundreder kun er skudår, når de kan deles med 400, og 1900 kan ikke. Fantomdagen er en bevidst kompatibilitetsadfærd, der er arvet fra et tidligt regneark, der blev leveret med fejlen, bevaret lige siden, så seriel aritmetik forbliver identisk på tværs af årtiers filer
Den praktiske konsekvens er lille, men reel: for enhver dato på eller efter 1. marts 1900, serienummeret er én højere, end en strengt korrekt dagsoptælling ville give, fordi den ikke-eksisterende 29. februar forbrugte et tal. Et regnearksbibliotek reproducerer denne særhed frem for at rette den, fordi det at matche Excels aritmetik præcist er hele opgaven. At rette den ville sætte hver eneste moderne dato en dag forkert i forhold til, hvad Excel viser, hvilket er et værre udfald end at bære en fyrre tusind dage gammel off-by-one-fejl, som ingen reel dato i forretningsbrug nogensinde rører ved. 1904-systemet har intet tilsvarende fantomdag, hvilket er en af grundene til, at nogle få virksomheder historisk set foretrak det
Registrering af en dato fra numFmt
Når et tal ankommer fra en fil, en anden har skrevet, dets format er det eneste bevis på, at det er en dato. ECMA-376 tildeler en blok af indbyggede format-id'er, hvis betydning er fastlagt i specifikationen, og dato- og tidsformaterne optager kendte områder. Id'er 14 til 22 er de generiske dato- og tidsformater, de velkendte m/d/yyyy, h:mm og deres slægtninge. Id'er 45 til 47 er formaterne for forløbet tid. To yderligere bånd, 27 til 36 og 50 til 58, er de lokalitetsspecifikke dato- og tidsformater, der bruges til CJK-kalendere, defineret i ECMA-376 18.8.30. En celle, hvis talformat-id falder i et hvilket som helst af disse områder, er en dato- eller tidscelle
Indbyggede id'er dækker de almindelige tilfælde, men ikke brugerdefinerede. Når en projektmappe definerer sin egen formatkode, f.eks. en ikke-standardiseret rækkefølge eller et lokaliseret månedsnavn, det id er over det indbyggede område og peger ind i projektmappens talformattabel. For disse betyder genkendelse af en dato at læse formatkoderen og lede efter datotokens. HotXLS folder begge kontroller ind i ét internt prædikat, XlsxNumFmtIsDate, som returnerer sandt med det samme for de indbyggede datoområder, og ellers fortolker den brugerdefinerede formatkode via XlsxFormatCodeIsDate. Den offentlige side af dette er cellens NumberFormat-streng og dens NumberFormatIndex, som giver dig både den løste formatkode og det id, du kan teste
Hvorfor formatfortolkeren ikke bare kan scanne efter d og m
Fortolkning af en formatkode for datotokens ser triviel ud, indtil du husker, hvad der ellers lever i et talformat. En naiv søgning efter de bogstaver, der staver datoer, d, m, y, h og s for dag, måned, år, time og sekund, vil fejlfyre på to strukturer, der slet ikke er datotokens
Den første er den citerede strengkonstant. Et talformat kan indlejre bogstavelig tekst i dobbelte anførselstegn, så et finansielt format som #,##0 "MM" tilføjer tegnene M og M til et tal uden nogen som helst tidsmæssig betydning. En scanner, der tæller bogstaverne inde i anførselstegnene som månedstokens, ville forkert markere det valutaformat som en dato. Den anden er parentessektionen. Talformater bærer direktiver i firkantede parenteser, farvenavne som [Red], sammenligningsbetingelser som [>1000], lokalitetstags og markørerne for forløbet tid [h] og [mm]. Noget parentesindhold indeholder datobogstaver, og noget gør ikke, og at behandle indrammet tekst på samme måde som formatets brødtekst fører til både falske positiver og oversete tilfælde
Den korrekte fortolker gennemgår formatkoden tegn for tegn, sporer om den er inde i en citeret konstant, og hvor dybt den er i indlejrede parenteser, og den respekterer også backslash-escapen, der citerer et enkelt efterfølgende tegn. Kun et ikke-escapet datobogstav fundet uden for en strengkonstant og uden for enhver parentessektion tæller som et reelt datotoken. Det er præcis sådan, XlsxFormatCodeIsDate scanner: et anførselstegn skifter en i-konstant-tilstand, der undertrykker token-registrering indtil det afsluttende anførselstegn, en backslash springer det næste tegn over, og en parentes-dybdetæller undertrykker registrering inde i [...]-kørsler. Gevinsten er, at #,##0 "MM" læses korrekt som et talformat, mens en kort custom kode, der intet andet indeholder end et enkelt m eller d uden for anførselstegn, stadig genkendes korrekt som en dato
Læsning af datoer ud af tredjepartsfiler
Alt ovenstående konvergerer mod én arbejdsgang: at omdanne et tal, som en anden applikation skrev, tilbage til en dato, du kan stole på. Serienummeret giver dig dagsoptællingen, projektmappens Date1904-flag fortæller dig, hvilken epoke optællingen måles fra, og cellens talformat-id eller brugerdefinerede kode er det eneste bevis på, at tallet oprindeligt var ment som en dato. Dropper man et eneste af de tre, får man et plausibelt forkert svar i stedet for en synlig fejl
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');
// 1904-flaget gælder for hele projektmappen: læs det én gang, anvend det på
// hvert serienummer, projektmappen afleverer.
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];
// En dato er kun en dato, når dens format siger det; den samme numeriske
// værdi med et almindeligt format er blot en mængde.
Writeln(Format('row %d value=%s numFmt=%d code="%s"',
[r, VarToStr(Cell.Value), Cell.NumberFormatIndex, Cell.NumberFormat]));
end;
finally
Book.Free;
end;
end;
Den ældre BIFF-side har en ekstra trap, der er værd at nævne. I en ældre .xls-strøm kan en række tilstødende numeriske celler pakkes ind i en enkelt multi-celle record, MULRK, der gemmer flere værdier med deres formatreferencer i én struktur. Datoceller, der gemmes på den måde, er ikke mindre datoer for at være pakket, så den samme talformat-id-test skal nå ind i multi-celle-recorden og gælde pr. celle, og 1904-afvigelsen styrer stadig ethvert serienummer, den giver. En læser, der kun inspicerer selvstændige talrecords og springer de pakkede over, vil lydløst gøre en kolonne med datoer til en kolonne med heltal
Omsætning af serier til TDateTime i praksis
Når formatkontrollen bekræfter en dato, og Date1904-flaget er kendt, er konverteringen mekanisk. En værdi, som HotXLS allerede afleverer som en varDate, er en TDateTime, du kan bruge direkte. En værdi, der ankommer som en bar Double, hvilket sker, når kilden skrev et serienummer uden et anerkendt datoformat, konverteres ved at læse den som en dagsoptælling på 1900-aksen og for en 1904-projektmappe trække den 1462-dages afvigelse fra først, så epokerne passer. Går man den anden vej, gemmer tildeling af en TDateTime til en celle det 1900-baserede serienummer, og HotXLS anvender den samme 1462-dages forskydning ved gem, når projektmappen er markeret 1904, så den gemte fil viser den dato, du mente, i stedet for en, der er fire år forskudt
Indstil flaget bevidst, når du genererer en projektmappe. Standarden efterlader Date1904 falsk, hvilket matcher Excel for Windows og næsten altid er det, du ønsker; indstil det kun til sandt, når du reproducerer en projektmappe med oprindelse på Mac, eller et efterfølgende system specifikt forventer 1904-aksen. Den eneste regel, der forhindrer hele klassen af fire-års-fejl, er konsistens: vælg epoken én gang pr. projektmappe, skriv hver dato under den, og læs hvert serienummer tilbage under det flag, som filen faktisk bærer
Datoer er én kolonne i en større historie om, hvad en celle egentlig indeholder. Det nærliggende metadatalag, titlen og forfatteren og de tidsstempler, der følger med gitteret, er dækket i vores artikel om projektmappe-metadata og dokumentegenskaber, hvor de samme Created- og Modified-værdier gemmes som TDateTime med den samme konvention om, at ikke-sat betyder nul. Når en dato er resultatet af en beregning frem for en gemt værdi, bestemmer evalueringsreglerne i vores artikel om formelmotoren og brugerdefinerede funktioner det serienummer, som formatet derefter renderer. Begge arbejder over den samme datomodel, der leveres i HotXLS Delphi Component til Delphi og C++Builder, som læser og skriver XLS- og XLSX-datoer uden Excel-automation