Technical Article

Excel-datumserier i Delphi: 1900 mot 1904 och numFmt

Öppna ett kalkylblad, klicka på en cell som visar 2026-06-19 och formelfältet läser fortfarande ett datum. Läs samma cell från Delphi och du får talet 46192. Båda vyerna är korrekta, eftersom Excel aldrig lagrade ett datum i den cellen. Det lagrade ett serienummer, ett antal dagar, och kopplade ett talformat som talar om för skärmen att rendera antalet som ett kalenderdatum. Det finns ingen datumtyp i cellvärdet. Det finns ett tal och en visningsregel, och visningsregeln är det enda som skiljer ett datum från en vanlig mängd

Den separationen är roten till varje datumbugg som ett kalkylbladsbibliotek måste undvika. Ett serienummer ensamt säger inte vilken dag det är, eftersom det inte säger vad dag noll var. Samma tal betyder två datum med fyra års mellanrum beroende på en enda arbetsboksflagga. Och ett tal som borde läsas av som ett datum kommer att läsas av som enbart en mängd om inte något inspekterar dess format och känner igen ett datummönster. Detta är hur datummodellen i HotXLS är uppbyggd, och varför den måste vara det

En datumcell är ett tal plus ett format

Excel lagrar ett datum som antalet dagar sedan en epok, med klockslaget i decimaldelen. Mitt på dagen på en serie bär på .5. Heltalsdelen är dagantalet. Ingenting i det lagrade värdet markerar det som tidsmässigt. Det som markerar det är cellens talformat: ECMA-376 kallar detta numFmt, och en cell vars formatkod visar ett datum- eller tidsmönster visas som ett datum. Ta bort formatet och samma cell visar ett tal; det underliggande värdet ändrades aldrig

Detta är varför avläsning av ett cellvärde ger dig en Variant som kan vara en varDate eller kan vara en vanlig Double, och varför talformatet på samma cell är signalen som avgör vilket av dem en tredje part avsåg. När HotXLS öppnar en XLSX-fil, en cell bär med sig både sitt Value och sitt NumberFormatIndex till TXLSXCell, och formatindexet är vad du konsulterar för att ta reda på om talet är ett datum

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;

Praktiskt sammanhang

Standarddatumsystemet, det som varje Windows-arbetsbok använder, räknar från slutet av 1899, så att serie 1 infaller på den första dagen 1900. Det andra systemet spåras till tidiga Macintosh och räknar från början av 1904, så dess serie 1 är fyra år och en dag senare. En arbetsbok registrerar vilket system den använder i en flagga. I ett OOXML-paket är den flaggan date1904 på arbetsboksdelen; HotXLS lyfter fram det som egenskapen Date1904 i arbetsboken

Gapet mellan de två epokerna är exakt 1462 dagar. Det är fyra kalenderår, tre på 365 dagar och ett på 366, totalt 1461, plus en till för dag-och-lite-offset mellan de två dag-noll-konventionerna. Talet är fast och du kan bära det i huvudet. Dess betydelse är att det inte är noll. En serie kopierad från en 1904-arbetsbok och tolkad under 1900-regler, eller tvärtom, landar varje datum 1462 dagar fel, vilket visar sig som datum som är fel med drygt fyra år och är lätt att missta för korrupta data

Eftersom Delphis egen TDateTime är förankrad i 1900-konventionen, måste ett bibliotek som mappar Excel-serier till TDateTime justera med 1462 i båda riktningarna när arbetsboken är flaggad med 1904. Vid avläsning av en 1904-serie, subtrahera 1462 innan du behandlar den som en TDateTime; vid skrivning av en TDateTime till en 1904-arbetsbok, subtrahera 1462 från serien så att Excel återger dagen du avsåg. HotXLS tillämpar detta skift internt när det serialiserar datumvärden för en arbetsbok vars Date1904 är inställd, så att värdet du tilldelar som en TDateTime går fram och tillbaka till samma kalenderdag på skärmen

Det avsiktliga skottårsfelet 1900

Det finns en berömd rynka i 1900-systemet. Excel behandlar 1900 som ett skottår och accepterar den 29 februari 1900 som ett verkligt datum, serie 60. Året 1900 var inte ett skottår, eftersom sekelskiftesår är skottår endast när de är delbara med 400, och 1900 är inte det. Fantomdagen är ett avsiktligt kompatibilitetsbeteende som ärvts från ett tidigt kalkylprogram som levererades med buggen, och har behållits sedan dess så att serie-aritmetiken förblir identisk över decennier av filer

Den praktiska konsekvensen är liten men verklig: för alla datum på eller efter 1 mars 1900 är serien en enhet högre än vad en strikt korrekt dagräkning skulle ge, eftersom den icke-existerande 29 februari förbrukade ett nummer. Ett kalkylbladsbibliotek reproducerar felet snarare än att korrigera det, eftersom att matcha Excels aritmetik exakt är hela jobbet. Att korrigera det skulle placera varje modernt datum en dag fel från vad Excel visar, vilket är ett sämre resultat än att bära med sig en fyrtiotusen dagar gammal avvikelse som inget verkligt datum i affärsverksamhet någonsin berör. 1904-systemet har ingen motsvarande fantomdag, vilket är en anledning till att vissa företag historiskt föredrog det

Att upptäcka ett datum från numFmt

När ett tal kommer från en fil som någon annan har skrivit, är dess format det enda beviset på att det är ett datum. ECMA-376 tilldelar ett block av inbyggda format-id vars innebörd är fastställd av specifikationen, och datum- och tidsformaten upptar kända intervall. Id 14 till 22 är datum- och tidsformaten för allmänna språk, det bekanta m/d/yyyy, h:mm och deras släktingar. Id 45 till 47 är formaten för förfluten tid. Ytterligare två band, 27 till 36 och 50 till 58, är de språkspecifika datum- och tidsformaten som används för CJK-kalendrar, definierade i ECMA-376 18.8.30. En cell vars talformat-id faller inom något av dessa intervall är een datum- eller tidscell

Inbyggda id:n täcker de vanliga fallen men inte anpassade. När en arbetsbok definierar sin egen formatkod, till exempel en icke-standardiserad ordning eller ett lokaliserat månadsnamn, ligger id:t ovanför det inbyggda intervallet och pekar på arbetsbokens talformattabell. För dessa innebär identifiering av ett datum att läsa formatkodsträngen och leta efter datum-tokens. HotXLS fäller ihop båda kontrollerna till ett internt predikat, XlsxNumFmtIsDate, vilket returnerar sant direkt för de inbyggda datumintervallen och annars tolkar den anpassade formatkoden via XlsxFormatCodeIsDate. Den offentliga sidan av detta är cellens NumberFormat-sträng och dess NumberFormatIndex, vilket ger dig både den lösta formatkoden och id:t att testa

Varför formattolken inte bara kan söka efter d och m

Att tolka en formatkod för datum-tokens verkar enkelt tills du kommer ihåg vad mer som finns i ett talformat. En naiv sökning efter bokstäverna som stavar datum, d, m, y, h och s för dag, månad, år, timme och sekund, kommer att slå fel på två strukturer som inte alls är datum-tokens

Det första är det citerade strängliteralen. En talformatkod kan bädda in bokstavlig text i dubbla citattecken, så ett finansiellt format som #,##0 "MM" lägger till tecknen M och M till ett nummer utan någon temporal betydelse alls. En skanner som räknar bokstäverna inom citattecknen som månadstokens skulle felaktigt flagga det valutaformatet som ett datum. Det andra är parentesavsnittet. Talformat bär direktiv i hakparenteser, färgnamn som [Red], jämförelsevillkor som [>1000], språktaggar och markörer för förfluten tid [h] och [mm]. Viss hakparentesinnehåll innehåller datumbokstäver och vissa inte, och att behandla hakparentestext på samma sätt som formatets kropp leder till både falska positiva resultat och missade fall

Den korrekta tolken går igenom formatkoden tecken för tecken och spårar om den är inuti en citerad literal och hur djupt den är inuti hakparenteser, och den respekterar även backslash-escape som citerar ett enskilt efterföljande tecken. Endast en oescaperad datumbokstav som hittas utanför valfri strängliteral och utanför hakparenteser räknas som en verklig datumtoken. Det är exakt hur XlsxFormatCodeIsDate skannar: ett citattecken växlar ett in-literal-tillstånd som förhindrar tokendetektering tills det stängande citattecknet, ett bakåtvänt snedstreck hoppar över nästa tecken, och en hakparentesdjup-räknare förhindrar detektering inuti [...]. Resultatet är att #,##0 "MM" tolkas korrekt som ett talformat, medan en kortfattad anpassad kod som inte innehåller annat än ett enskilt m eller d utanför citattecken fortfarande känns igen korrekt som ett datum

Att läsa datum ur filer från tredje part

Allt ovanstående sammanstrålar i ett arbetsflöde: att förvandla ett tal som ett annat program har skrivit till ett datum som du kan lita på. Serienumret ger dig dagantalet, arbetsbokens Date1904-flagga talar om för dig vilken epok räkningen mäts från, och cellens talformat-id eller anpassade kod är det enda beviset på att talet var avsett som ett datum från början. Släpp någon av de tre och du får ett rimligt felaktigt svar snarare än ett synligt fel

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-sidan har ytterligare en fälla värd att nämna. I en äldre .xls-ström kan en sekvens av intilliggande numeriska celler packas in i en enda flercellig post, MULRK, som lagrar flera värden med deras formatreferenser i en struktur. Datumceller lagrade på det sättet är inte mindre datum för att de är packade, så samma format-id-test måste gälla per cell, och 1904-justeringen styr fortfarande varje serienummer det ger. En läsare som endast inspekterar fristående nummerposter, och hoppar över de packade, kommer i tysthet att förvandla en kolumn med datum till en kolumn med heltal

Att mappa serier till TDateTime i praktiken

När formatkontrollen väl har bekräftat ett datum och Date1904-flaggan är känd, är konverteringen mekanisk. Ett värde som HotXLS redan levererar som en varDate är en TDateTime du kan använda direkt. Ett värde som anländer som en ren Double, vilket händer när källan skrev ett serienummer utan ett känt datumformat, konverteras genom att läsa det som ett dagantal på 1900-axeln och, för en 1904-arbetsbok, subtrahera 1462-dagars offset först så att epokerna ligger i linje. Går man åt andra hållet, sparar tilldelning av en TDateTime till en cell det 1900-baserade serienumret, och HotXLS tillämpar samma 1462-dagars skift vid sparning när arbetsboken är flaggad med 1904, så att kalkylbladsfilen visar datumet du avsåg

Ställ in flaggan avsiktligt när du genererar en arbetsbok. Standardinställningen lämnar Date1904 falsk, vilket matchar Excel för Windows och är nästan alltid vad du vill ha; ställ in den till sant endast när du återskapar en arbetsbok med Mac-ursprung eller när ett mottagande system specifikt förväntar sig 1904-axeln. Regeln som förhindrar hela klassen av fyraårsfel är konsekvens: välj epoken en gång per arbetsbok, skriv varje datum under det och läs varje serie tillbaka under den flagga som filen faktiskt bär

Datum är en kolumn i en bredare historia om vad en cell faktiskt innehåller. Det intilliggande metadatalagret, titeln och författaren samt tidsstämplarna som följer med rutnätet, beskrivs i vår artikel om arbetsboksmetadata och dokumentegenskaper, där samma värden för Created och Modified lagras som TDateTime med samma konvention för oinställt-lika-med-noll. När ett datum är resultatet av en beräkning snarare än ett lagrat värde, bestämmer utvärderingsreglerna i vår artikel om formelmotorn och anpassade funktioner serienumret som formatet sedan renderar. Båda arbetar över samma datummodell som levereras i HotXLS Component för Delphi och C++Builder, som läser och skriver XLS- och XLSX-datum utan Excel-automatisering