Å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]; // row 1, col 1 (1-based)
// Value may arrive as varDate or as a plain numeric serial;
// the format index is the signal that tells them apart.
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 surfaces it as the Date1904 property of the workbook.
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, et bibliotek, der kortlægger Excel-serienumre to TDateTime has to offset by 1462 in both directions whenever the workbook is flagged 1904. Reading a 1904 serial, subtract 1462 before treating it as a TDateTime; writing a TDateTime into a 1904 workbook, subtract 1462 from the serial so Excel renders the day you meant. HotXLS applies this shift internally when it serializes date values for a workbook whose Date1904 is set, so the value you assign as a TDateTime round-trips to the same calendar day on the screen.
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. Correcting it would put every modern date one day off from what Excel shows, which is a worse outcome than carrying a forty-thousand-day-old off-by-one that no real date in business use ever touches. 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. Two further bands, 27 through 36 and 50 through 58, are the locale-specific date and time formats used for CJK calendars, defined in ECMA-376 18.8.30. A cell whose number format id falls in any of these ranges is a date or time cell.
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. The public side of that is the cell's NumberFormat string and its NumberFormatIndex, which give you both the resolved format code and the id to test.
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. A naive search for the letters that spell dates, the d, m, y, h, and s of day, month, year, hour, and second, will misfire on two structures that are not date tokens at all.
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. A scanner that counts the letters inside the quotes as month tokens would wrongly flag that currency format as a date. The second is the bracket section. Number formats carry directives in square brackets, color names such as [Red], comparison conditions such as [>1000], locale tags, and the elapsed-time markers [h] and [mm]. Some bracket content holds date letters and some does not, and treating bracketed text the same as the body of the format leads to both false positives and missed cases.
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 is the single piece of evidence that the number was meant as a date in the first place. Drop any one of the three and you get a plausible wrong answer rather than a visible error.
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');
// The 1904 flag is workbook-wide: read it once, apply it to
// every serial the workbook hands back.
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];
// A date is only a date when its format says so; the same numeric
// value with a plain format is just a quantity.
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.
Praktisk kontekst
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, lagrer tildeling af en TDateTime to a cell stores the 1900-based serial, and HotXLS applies the same 1462-day shift on save when the workbook is flagged 1904, so the saved file shows the date you intended rather than one four years adrift.
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, is consistency: pick the epoch once per workbook, write every date under it, and read every serial back under the flag the file actually carries.
Dates are one column in a greater story about what a cell really holds. The neighboring metadata layer, the title and author and timestamps that ride alongside the grid, is covered in our article on workbook metadata and document properties, where the same Created and Modified values are stored as TDateTime with the same unset-equals-zero convention. When a date is the result of a calculation rather than a stored value, the evaluation rules in our article on the formula engine and custom functions determine the serial that the format then renders. Both work over the same date model that ships in the HotXLS Component for Delphi and C++Builder, which reads and writes XLS and XLSX dates without Excel automation.