Technical Article

Excel-Datumsserien in Delphi: 1900 vs. 1904 und numFmt

Öffnen Sie eine Tabellenkalkulation, klicken Sie auf eine Zelle, die 2026-06-19 anzeigt, und die Bearbeitungsleiste zeigt immer noch ein Datum an. Lesen Sie dieselbe Zelle aus Delphi aus, erhalten Sie die Zahl 46192. Beide Ansichten sind korrekt, da Excel nie ein Datum in dieser Zelle gespeichert hat. Es hat eine Seriennummer (eine Anzahl von Tagen) gespeichert und ein Zahlenformat angehängt, das dem Bildschirm mitteilt, diese Anzahl als Kalenderdatum darzustellen. Es gibt keinen Datums-Typ im Zellenwert. Es gibt eine Zahl und eine Darstellungsregel, und diese Darstellungsregel ist das Einzige, was ein Datum von einer einfachen Menge unterscheidet

Diese Trennung ist die Ursache für jeden Datumsfehler, dem eine Tabellenkalkulationsbibliothek ausweichen muss. Eine Serie allein sagt nicht aus, welcher Tag es ist, da sie nicht angibt, welcher Tag Null war. Dieselbe Zahl bedeutet zwei Datumsangaben im Abstand von vier Jahren, abhängig von einem einzigen Workbook-Flag. Und eine Zahl, die als Datum zurückgelesen werden sollte, wird als bloße Menge zurückgelesen, es sei denn, etwas überprüft ihr Format und erkennt ein Datumsmuster. So ist das Datumsmodell in HotXLS aufgebaut, und aus diesem Grund muss es so sein

Eine Datumszelle ist eine Zahl plus ein Format

Excel speichert ein Datum als Anzahl der Tage seit einer Epoche, wobei die Tageszeit im Nachkommateil liegt. Mittag auf einer Serie trägt .5. Der ganzzahlige Teil ist die Tagesanzahl. Nichts im gespeicherten Wert kennzeichnet ihn als zeitlich. Was ihn kennzeichnet, ist das Zahlenformat der Zelle: ECMA-376 nennt dies ein numFmt, und eine Zelle, deren Formatcode ein Datums- oder Zeitmuster beschreibt, wird als Datum angezeigt. Entfernt man das Format, zeigt dieselbe Zelle eine Zahl; der zugrundeliegende Wert hat sich nie geändert

Deshalb liefert das Lesen eines Zellenwerts eine Variant, die ein varDate oder ein einfaches Double sein kann, und deshalb ist das Zahlenformat auf derselben Zelle das Signal, das entscheidet, was ein Drittanbieter gemeint hat. Wenn HotXLS eine XLSX-Datei öffnet, eine Zelle trägt sowohl ihren Value als auch ihren NumberFormatIndex in TXLSXCell ein, und der Formatindex ist das, was Sie konsultieren, um zu erfahren, ob die Zahl ein Datum ist

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;

Zwei Epochen, 1462 Tage voneinander entfernt

Das Standarddatumssystem, das jedes Windows-Workbook verwendet, zählt ab dem Ende des Jahres 1899, sodass die Serie 1 auf den ersten Tag des Jahres 1900 fällt. Das andere System geht auf den frühen Macintosh zurück und zählt ab Beginn des Jahres 1904, sodass seine Serie 1 vier Jahre und einen Tag später liegt. Ein Workbook zeichnet in einem Flag auf, welches System es verwendet. In einem OOXML-Paket ist dieses Flag date1904 im Workbook-Teil; HotXLS macht es als Date1904-Eigenschaft des Workbooks zugänglich

Der Abstand zwischen den beiden Epochen beträgt genau 1462 Tage. Das sind vier Kalenderjahre (drei mit 365 Tagen und eines mit 366 Tagen, insgesamt 1461 Tage) plus ein weiterer Tag für den Offset zwischen den beiden Tag-Null-Konventionen. Die Zahl ist fest und man kann sie sich leicht merken. Ihre Bedeutung liegt darin, dass sie nicht Null ist. Eine Serie, die aus einem 1904-Workbook kopiert und nach 1900-Regeln interpretiert wird (oder umgekehrt), verschiebt jedes Datum um 1462 Tage. Dies äußert sich in Datumsangaben, die um etwas mehr als vier Jahre falsch sind, und lässt sich leicht mit beschädigten Daten verwechseln

Da Delphis eigenes TDateTime an die 1899/1900er-Konvention verankert ist, muss eine Bibliothek, die Excel-Serien auf TDateTime abbildet, in beide Richtungen einen Offset von 1462 anwenden, wann immer das Workbook als 1904 gekennzeichnet ist. Beim Lesen einer 1904-Serie ziehen Sie 1462 ab, bevor Sie sie als TDateTime behandeln; beim Schreiben eines TDateTime in ein 1904-Workbook ziehen Sie 1462 von der Serie ab, damit Excel den von Ihnen gemeinten Tag darstellt. HotXLS wendet diese Verschiebung intern an, wenn es Datumswerte für ein Workbook serialisiert, dessen Date1904 gesetzt ist, sodass der von Ihnen als TDateTime zugewiesene Wert auf dem Bildschirm zum selben Kalendertag führt

Die bewusste Eigenheit des Schaltjahres 1900

Es gibt eine berühmte Besonderheit im 1900er-System. Excel behandelt 1900 als Schaltjahr und akzeptiert den 29. Februar 1900 als reales Datum, Serie 60. Das Jahr 1900 war kein Schaltjahr, da Jahrhundertjahre nur dann Schaltjahre sind, wenn sie durch 400 teilbar sind, und 1900 ist dies nicht. Der Phantomtag ist ein bewusstes Kompatibilitätsverhalten, das von einer frühen Tabellenkalkulation übernommen wurde, die mit diesem Fehler ausgeliefert wurde, und das seitdem beibehalten wird, damit die Serienarithmetik über Jahrzehnte von Dateien hinweg identisch bleibt

Die praktische Auswirkung ist gering, aber real: Für jedes Datum am oder nach dem 1. März 1900 ist die Serie um eins höher, als es eine streng korrekte Tageszählung ergeben würde, da der nicht existierende 29. Februar eine Nummer verbraucht hat. Eine Tabellenkalkulationsbibliothek bildet diese Eigenheit nach, anstatt sie zu korrigieren, da die exakte Übereinstimmung mit Excels Arithmetik die eigentliche Aufgabe ist. Eine Korrektur würde jedes moderne Datum um einen Tag von dem abweichen lassen, was Excel anzeigt, was ein schlechteres Ergebnis wäre, als einen vierzigtausend Tage alten Off-by-One-Fehler mitzuschleppen, den kein reales Datum im geschäftlichen Gebrauch jemals berührt. Das 1904er-System hat keinen entsprechenden Phantomtag, was ein Grund ist, warum einige Unternehmen dies in der Vergangenheit bevorzugten

Erkennen eines Datums anhand von numFmt

Wenn eine Zahl aus einer Datei stammt, die jemand anderes geschrieben hat, ist ihr Format der einzige Beweis dafür, dass es sich um ein Datum handelt. ECMA-376 weist einen Block integrierter Format-IDs zu, deren Bedeutung durch die Spezifikation festgelegt ist, und die Datums- und Zeitformate belegen bekannte Bereiche. Die IDs 14 bis 22 sind die länderspezifischen Datums- und Zeitformate, die bekannten Formate wie m/d/yyyy, h:mm und deren Verwandte. Die IDs 45 bis 47 sind die Formate für abgelaufene Zeit. Zwei weitere Bänder, 27 bis 36 und 50 bis 58, sind die gebietsschemaspezifischen Datums- und Zeitformate, die für CJK-Kalender verwendet werden und in ECMA-376 18.8.30 definiert sind. Eine Zelle, deren Zahlenformat-ID in einen dieser Bereiche fällt, ist eine Datums- oder Zeitzelle

Integrierte IDs decken die häufigsten Fälle ab, jedoch nicht benutzerdefinierte Formate. Wenn ein Workbook seinen eigenen Formatcode definiert (z. B. eine vom Standard abweichende Reihenfolge oder einen lokalisierten Monatsnamen), liegt die ID über dem integrierten Bereich und verweist auf die Zahlenformattabelle des Workbooks. Um bei diesen ein Datum zu erkennen, muss der Formatcode-String gelesen und nach Datums-Token gesucht werden. HotXLS fasst beide Prüfungen in einem internen Prädikat namens XlsxNumFmtIsDate zusammen, das für die integrierten Datumsbereiche sofort True zurückgibt und andernfalls den benutzerdefinierten Formatcode über XlsxFormatCodeIsDate parst. Die öffentliche Seite davon sind der NumberFormat-String der Zelle und ihr NumberFormatIndex, die Ihnen sowohl den aufgelösten Formatcode als auch die zu prüfende ID liefern

Warum der Format-Parser nicht einfach nach d und m suchen kann

Das erste ist das in Anführungszeichen gesetzte String-Literal. Ein Zahlenformat kann literalen Text in doppelten Anführungszeichen einbetten, sodass ein Finanzformat wie #,##0 "MM" die Zeichen M und M an eine Zahl anhängt, ohne dass dies irgendeine zeitliche Bedeutung hat. Ein Scanner, der die Buchstaben innerhalb der Anführungszeichen als Monats-Token zählt, würde dieses Währungsformat fälschlicherweise als Datum kennzeichnen. Das zweite ist der Klammerabschnitt. Zahlenformate tragen Anweisungen in eckigen Klammern: Farbnamen wie [Red], Vergleichsbedingungen wie [>1000], Gebietsschema-Tags und die Marker für abgelaufene Zeit wie [h] und [mm]. Einige Klammerinhalte enthalten Datumsbuchstaben und andere nicht. Die Gleichbehandlung von Text in Klammern mit dem Hauptteil des Formats führt sowohl zu Fehlalarmen als auch zu übersehenen Fällen

Der korrekte Parser durchläuft den Formatcode Zeichen für Zeichen und verfolgt dabei, ob er sich innerhalb eines Literals in Anführungszeichen befindet und wie tief er sich innerhalb von eckigen Klammern befindet. Er berücksichtigt auch den Backslash-Escape, der ein einzelnes folgendes Zeichen maskiert. Nur ein unmaskierter Datumsbuchstabe, der außerhalb eines String-Literals und außerhalb eines Klammerabschnitts gefunden wird, zählt als echtes Datums-Token. Genau so scannt XlsxFormatCodeIsDate: Ein Anführungszeichen schaltet einen In-Literal-Zustand um, der die Token-Erkennung bis zum schließenden Anführungszeichen unterdrückt, ein Backslash überspringt das nächste Zeichen, und ein Klammertiefenzähler unterdrückt die Erkennung innerhalb von [...]-Abschnitten. Das Ergebnis ist, dass #,##0 "MM" korrekt als Zahlenformat gelesen wird, während ein knapper benutzerdefinierter Code, der nichts außer einem einzelnen m oder d außerhalb von Anführungszeichen enthält, immer noch korrekt als Datum erkannt wird

Lesen von Datumsangaben aus Drittanbieter-Dateien

Alles oben Genannte läuft auf einen einzigen Workflow hinaus: Das Umwandeln einer Zahl, die eine andere Anwendung geschrieben hat, zurück in ein Datum, dem Sie vertrauen können. Die Serie liefert Ihnen die Tageszählung, das Date1904-Flag des Workbooks teilt Ihnen mit, ab welcher Epoche gezählt wird, und die Zahlenformat-ID der Zelle oder der benutzerdefinierte Code ist der einzige Beleg dafür, dass die Zahl überhaupt als Datum gedacht war. Lassen Sie einen der drei Faktoren weg, erhalten Sie eine plausible falsche Antwort anstelle eines sichtbaren Fehlers

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;

Die veraltete BIFF-Seite birgt eine zusätzliche Falle, die es wert is, benannt zu werden. In einem älteren .xls-Stream kann eine Reihe benachbarter numerischer Zellen in einen einzigen Multi-Zellen-Datensatz, den MULRK, gepackt werden, der mehrere Werte mit ihren Format-Referenzen in einer Struktur speichert. Auf diese Weise gespeicherte Datumszellen sind nicht weniger Datumsangaben, nur weil sie gepackt sind. Dieselbe Format-ID-Prüfung muss also in den Multi-Zellen-Datensatz hineinreichen und pro Zelle angewendet werden, und der 1904er-Offset steuert weiterhin jede Serie, die er liefert. Ein Lader, der nur eigenständige Zahlendatensätze prüft und die gepackten überspringt, verwandelt stillschweigend eine Spalte mit Datumsangaben in eine Spalte mit Ganzzahlen

Abbildung von Serien auf TDateTime in der Praxis

Sobald die Formatprüfung ein Datum bestätigt und das Date1904-Flag bekannt ist, die Konvertierung erfolgt mechanisch. Ein Wert, den HotXLS bereits als varDate zurückgibt, ist ein TDateTime, das Sie direkt verwenden können. Ein Wert, der als einfaches Double ankommt (was vorkommt, wenn die Quelle eine Serie ohne erkanntes Datumsformat geschrieben hat), wird konvertiert, indem er als Tagesanzahl auf der 1900er-Achse gelesen wird, wobei bei einem 1904-Workbook zuerst der 1462-Tage-Offset abgezogen wird, damit die Epochen übereinstimmen. In der Gegenrichtung speichert das Zuweisen eines TDateTime an eine Zelle die 1900-basierte Serie, und HotXLS wendet beim Speichern dieselbe Verschiebung um 1462 Tage an, wenn das Workbook als 1904 gekennzeichnet ist. So zeigt die gespeicherte Datei das von Ihnen beabsichtigte Datum anstelle eines um vier Jahre verschobenen Datums

Setzen Sie das Flag bewusst, wenn Sie ein Workbook erstellen. Der Standardwert belässt Date1904 auf False, was Excel für Windows entspricht und fast immer das ist, was Sie wollen. Setzen Sie es nur dann auf True, wenn Sie ein vom Mac stammendes Workbook reproduzieren oder ein nachgelagertes System speziell die 1904er-Achse erwartet. Die einzige Regel, die die gesamte Klasse von Vier-Jahres-Fehlern verhindert, is Konsistenz: Wählen Sie die Epoche einmal pro Workbook, schreiben Sie jedes Datum darunter und lesen Sie jede Serie unter dem Flag zurück, das die Datei tatsächlich trägt

Datumsangaben sind nur eine Spalte in einer größeren Geschichte darüber, was eine Zelle wirklich enthält. Die benachbarte Metadatenschicht (Titel, Autor und Zeitstempel, die neben dem Raster existieren) wird in our article on workbook metadata and document properties behandelt, in dem dieselben Created- und Modified-Werte als TDateTime mit derselben Konvention (nicht gesetzt entspricht Null) gespeichert werden. Wenn ein Datum das Ergebnis einer Berechnung und kein gespeicherter Wert ist, bestimmen die Berechnungsregeln in our article on the formula engine and custom functions die Serie, die das Format dann rendert. Beide arbeiten über dasselbe Datumsmodell, das in der HotXLS Component für Delphi und C++Builder ausgeliefert wird, die XLS- und XLSX-Daten ohne Excel-Automatisierung liest und schreibt