Åpne et regneark, klikk på en celle som viser 2026-06-19, og formellinjen viser fortsatt en dato. Les den samme cellen fra Delphi, og du får tallet 46192. Begge bildene er riktige, for Excel lagret aldri en dato i den cellen. Det lagret et serienummer, et antall dager, og festet på et tallformat som forteller skjermen at antallet skal vises som en kalenderdato. Det finnes ingen datotype i celleverdien. Det finnes et tall og en visningsregel, og visningsregelen er det eneste som skiller en dato fra en vanlig mengde
Den adskillelsen er roten til hver eneste datofeil et regnearkbibliotek må styre unna. En serieverdi alene sier ikke hvilken dag det er, fordi den ikke sier hva dag null var. Det samme tallet betyr to datoer fire år fra hverandre avhengig av ett enkelt arbeidsbokflagg. Og et tall som skal leses tilbake som en dato, vil leses tilbake som en naken mengde med mindre noe inspiserer formatet og gjenkjenner et datomønster. Slik er datomodellen i HotXLS bygget, og dette er grunnen til at den må være slik
En datocelle er et tall pluss et format
Excel lagrer en dato som antall dager siden en epoke, med klokkeslettet i desimaldelen. Midt på dagen bærer en serieverdi .5. Heltallsdelen er dagantallet. Ingenting i den lagrede verdien merker den som tidsmessig. Det som merker den, er cellens tallformat: ECMA-376 kaller dette en numFmt, og en celle hvis formatkode staver ut et dato- eller klokkeslettmønster, vises som en dato. Fjern formatet, og den samme cellen viser et tall; den underliggende verdien endret seg aldri
Dette er grunnen til at det å lese en celleverdi gir deg en Variant som kan være en varDate eller kan være en vanlig Double, og grunnen til at tallformatet på den samme cellen er signalet som avgjør hva en tredjepart mente. Når HotXLS åpner en XLSX-fil, bærer en celle både sin Value og sin NumberFormatIndex inn i TXLSXCell, og formatindeksen er det du rådfører deg med for å finne ut 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]; // rad 1, kolonne 1 (1-basert)
// Value kan komme som varDate eller som en vanlig numerisk serieverdi;
// formatindeksen er signalet som skiller dem fra hverandre.
Writeln('raw value : ', VarToStr(Cell.Value));
Writeln('numFmt idx: ', Cell.NumberFormatIndex);
Writeln('format : ', Cell.NumberFormat);
finally
Book.Free;
end;
end;
To epoker, 1462 dager fra hverandre
Standard datosystem, det hver eneste Windows-arbeidsbok bruker, teller fra helt på tampen av 1899, slik at serieverdien 1 faller på den første dagen i 1900. Det andre systemet stammer fra de tidlige Macintosh-maskinene og teller fra begynnelsen av 1904, så dets serieverdi 1 ligger fire år og en dag senere. En arbeidsbok registrerer hvilket system den bruker, i ett flagg. I en OOXML-pakke er det flagget date1904 på arbeidsbokdelen; HotXLS eksponerer det som arbeidsbokens Date1904-egenskap
Avstanden mellom de to epokene er nøyaktig 1462 dager. Det er fire kalenderår, tre på 365 dager og ett på 366, til sammen 1461, pluss én til for forskyvningen på litt over en dag mellom de to dag-null-konvensjonene. Tallet er fast, og du kan bære det i hodet. Betydningen ligger i at det ikke er null. En serieverdi kopiert ut av en 1904-arbeidsbok og tolket etter 1900-regler, eller omvendt, lander hver dato 1462 dager feil, noe som viser seg som datoer som bommer med litt over fire år, og som lett kan forveksles med ødelagte data
Fordi Delphis egen TDateTime er forankret i 1900-konvensjonen, må et bibliotek som avbilder Excel-serieverdier på TDateTime, forskyve med 1462 i begge retninger når arbeidsboken er merket 1904. Leser du en 1904-serieverdi, trekk fra 1462 før du behandler den som en TDateTime; skriver du en TDateTime inn i en 1904-arbeidsbok, trekk 1462 fra serieverdien slik at Excel viser dagen du mente. HotXLS bruker denne forskyvningen internt når det serialiserer datoverdier for en arbeidsbok der Date1904 er satt, slik at verdien du tilordner som en TDateTime, kommer tilbake til den samme kalenderdagen på skjermen
Den bevisste skuddårsrariteten i 1900
Det finnes en berømt krøll i 1900-systemet. Excel behandler 1900 som et skuddår og godtar 29. februar 1900 som en virkelig dato, serieverdi 60. Året 1900 var ikke et skuddår, fordi århundreår er skuddår bare når de er delelige med 400, og 1900 er ikke det. Fantomdagen er en bevisst kompatibilitetsatferd arvet fra et tidlig regneark som ble levert med feilen, beholdt siden den gang slik at seriearitmetikken forblir identisk gjennom tiår med filer
Den praktiske konsekvensen er liten, men reell: for enhver dato på eller etter 1. mars 1900 er serieverdien én høyere enn et strengt korrekt dagantall ville gitt, fordi den ikke-eksisterende 29. februar spiste opp et tall. Et regnearkbibliotek gjenskaper rariteten i stedet for å rette den, fordi det å treffe Excels aritmetikk nøyaktig er hele jobben. Å rette den ville lagt hver moderne dato én dag feil i forhold til det Excel viser, som er et dårligere utfall enn å bære med seg en førti tusen dager gammel av-med-én som ingen reell dato i forretningsbruk noen gang berører. 1904-systemet har ingen tilsvarende fantomdag, som er én grunn til at noen få miljøer historisk foretrakk det
Oppdage en dato ut fra numFmt
Når et tall kommer fra en fil noen andre skrev, er formatet det eneste beviset på at det er en dato. ECMA-376 tildeler en blokk med innebygde format-id-er hvis betydning er fastsatt av spesifikasjonen, og dato- og klokkeslettformatene opptar kjente områder. Id-ene 14 til 22 er dato- og klokkeslettformatene for generell lokalitet, de velkjente m/d/yyyy, h:mm og slektningene deres. Id-ene 45 til 47 er formatene for medgått tid. To ytterligere bånd, 27 til 36 og 50 til 58, er de lokalitetsspesifikke dato- og klokkeslettformatene som brukes for CJK-kalendere, definert i ECMA-376 18.8.30. En celle hvis tallformat-id faller innenfor noen av disse områdene, er en dato- eller klokkeslettcelle
Innebygde id-er dekker de vanlige tilfellene, men ikke de egendefinerte. Når en arbeidsbok definerer sin egen formatkode, si en ikke-standard rekkefølge eller et lokalisert månedsnavn, ligger id-en over det innebygde området og peker inn i arbeidsbokens tallformattabell. For disse betyr det å gjenkjenne en dato at man leser formatkodestrengen og ser etter datotokener. HotXLS folder begge kontrollene inn i ett internt predikat, XlsxNumFmtIsDate, som umiddelbart returnerer sant for de innebygde datoområdene og ellers parser den egendefinerte formatkoden gjennom XlsxFormatCodeIsDate. Den offentlige siden av det er cellens NumberFormat-streng og dens NumberFormatIndex, som gir deg både den oppløste formatkoden og id-en du kan teste
Hvorfor formatparseren ikke bare kan lete etter d og m
Å parse en formatkode etter datotokener ser trivielt ut helt til du husker hva annet som bor i et tallformat. Et naivt søk etter bokstavene som staver datoer, d, m, y, h og s for dag, måned, år, time og sekund, vil slå feil på to strukturer som slett ikke er datotokener
Den første er den siterte strenglitteralen. Et tallformat kan bygge inn bokstavelig tekst i doble anførselstegn, så et finansformat som #,##0 "MM" føyer tegnene M og M til et tall uten noen som helst tidsmessig betydning. En skanner som teller bokstavene inne i anførselstegnene som månedstokener, ville feilaktig flagget det valutaformatet som en dato. Den andre er klammeseksjonen. Tallformater bærer direktiver i hakeparenteser, fargenavn som [Red], sammenligningsbetingelser som [>1000], lokalitetstagger og markørene for medgått tid, [h] og [mm]. Noe klammeinnhold rommer datobokstaver og noe gjør det ikke, og å behandle tekst i hakeparenteser likt med formatets brødtekst fører til både falske positive og oversette tilfeller
Den riktige parseren går gjennom formatkoden tegn for tegn, holder styr på om den er inne i en sitert litteral og hvor dypt den er inne i klammenesting, og den respekterer også omvendt skråstrek-escapen som siterer ett enkelt etterfølgende tegn. Bare en uescapet datobokstav funnet utenfor enhver strenglitteral og utenfor enhver klammeseksjon teller som et ekte datotoken. Det er nøyaktig slik XlsxFormatCodeIsDate skanner: et anførselstegn vender en i-litteral-tilstand som undertrykker tokengjenkjenning frem til det avsluttende anførselstegnet, en omvendt skråstrek hopper over neste tegn, og en teller for klammedybde undertrykker gjenkjenning inne i [...]-strekninger. Gevinsten er at #,##0 "MM" korrekt leses som et tallformat, mens en knapp egendefinert kode som ikke inneholder annet enn en enslig m eller d utenfor anførselstegn, fortsatt gjenkjennes korrekt som en dato
Lese datoer ut av tredjepartsfiler
Alt det ovenstående konvergerer mot én arbeidsflyt: å gjøre et tall som et annet program skrev, tilbake til en dato du kan stole på. Serieverdien gir deg dagantallet, arbeidsbokens Date1904-flagg forteller deg hvilken epoke antallet måles fra, og cellens tallformat-id eller egendefinerte kode er det eneste beviset på at tallet i det hele tatt var ment som en dato. Dropp én av de tre, og du får et plausibelt galt svar i stedet for en synlig feil
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-flagget gjelder hele arbeidsboken: les det én gang, bruk det på
// hver serieverdi arbeidsboken leverer tilbake.
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 bare en dato når formatet sier det; den samme numeriske
// verdien med et vanlig format er bare en mengde.
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 eldre BIFF-siden har én ekstra felle verdt å nevne. I en eldre .xls-strøm kan en rekke av tilstøtende numeriske celler pakkes inn i én enkelt flercellepost, MULRK-en, som lagrer flere verdier med sine formatreferanser i én struktur. Datoceller lagret slik er ikke mindre datoer av å være pakket, så den samme format-id-testen må nå inn i flercelleposten og anvendes per celle, og 1904-forskyvningen styrer fortsatt hver serieverdi den gir. En leser som bare inspiserer frittstående tallposter, og hopper over de pakkede, vil stille gjøre en kolonne med datoer om til en kolonne med heltall
Avbilde serieverdier på TDateTime i praksis
Når formatkontrollen har bekreftet en dato og Date1904-flagget er kjent, er omregningen mekanisk. En verdi som HotXLS allerede leverer tilbake som en varDate, er en TDateTime du kan bruke direkte. En verdi som kommer som en naken Double, noe som skjer når kilden skrev en serieverdi uten et gjenkjent datoformat, omregnes ved å lese den som et dagantall på 1900-aksen og, for en 1904-arbeidsbok, først trekke fra forskyvningen på 1462 dager slik at epokene stemmer overens. Den andre veien lagrer det å tilordne en TDateTime til en celle den 1900-baserte serieverdien, og HotXLS bruker den samme forskyvningen på 1462 dager ved lagring når arbeidsboken er merket 1904, slik at den lagrede filen viser datoen du hadde tenkt, i stedet for en som driver fire år av
Sett flagget bevisst når du genererer en arbeidsbok. Standarden lar Date1904 stå usann, noe som samsvarer med Excel for Windows og nesten alltid er det du vil ha; sett det sant bare når du gjenskaper en arbeidsbok med Mac-opphav eller et nedstrømssystem uttrykkelig venter 1904-aksen. Den ene regelen som forebygger hele denne klassen av fireårsfeil, er konsistens: velg epoken én gang per arbeidsbok, skriv hver dato under den, og les hver serieverdi tilbake under det flagget filen faktisk bærer
Datoer er én kolonne i en videre historie om hva en celle egentlig rommer. Det tilgrensende metadatalaget, tittelen og forfatteren og tidsstemplene som følger rutenettet, er dekket i artikkelen vår om arbeidsbokmetadata og dokumentegenskaper, der de samme Created- og Modified-verdiene lagres som TDateTime med den samme konvensjonen om at usatt er lik null. Når en dato er resultatet av en beregning i stedet for en lagret verdi, bestemmer evalueringsreglene i artikkelen vår om formelmotoren og egendefinerte funksjoner serieverdien som formatet så gjengir. Begge arbeider over den samme datomodellen som følger med HotXLS Delphi Component for Delphi og C++Builder, som leser og skriver XLS- og XLSX-datoer uten Excel-automatisering