HotXLS speichert Excel-Dokument-Property-Zeitstempel in der Datei als UTC und stellt sie über die API als lokale Zeit bereit: TXLSWorkbook.CreatedDate und LastSavedDate für .xls, TXLSXWorkbook.Created und Modified für .xlsx. Seit v2.384.48 rechnen beide Engines beim Schreiben von lokaler Zeit nach UTC um und beim Lesen zurück, jeweils mit den Sommerzeit-Regeln, die am Datum des Stempels selbst gelten. Bis dahin waren zwei Fixes nötig, und beide Bugs hatten aus demselben peinlichen Grund überlebt: Jeder automatisierte Roundtrip bestand, während Excels Datei-Info-Anzeige den falschen Tag oder die falsche Stunde zeigte. Wenn Sie unseren Überblick über Setzen von Excel-Dokumenteigenschaften in Delphi gelesen haben – hier ist die Stelle, an der die Datumswerte aufhören, simple Werte zu sein
Warum versteckte ein Speichern-und-erneut-öffnen-Test einen Tagesfehler?
Ein symmetrischer Roundtrip versteckte den Fehler, weil Writer und Reader dieselbe falsche Konstante teilten – der Irrtum hob sich selbst auf. Ein OLE-Property-Set-Datum ist eine FILETIME, ein 64-Bit-Zähler von 100-Nanosekunden-Ticks seit 1601-01-01 UTC ([MS-DTYP] §2.3.3), während ein Delphi-TDateTime Tage ab dem 1899-12-30 zählt, derselbe Serial-Ursprung wie in Excel-Datumsserials in Delphi und 1900 gegen 1904 behandelt. Die Lücke zwischen den beiden Epochen beträgt 109205 Tage, was Sie ohne Kalender prüfen können: 25569 (die Unix-Epoche als TDateTime) plus 109205 ergibt 134774, die Unix-Epoche in FILETIME-Tagen. HotXLS-Builds vor v2.384.17 nutzten 109206, also wurde jeder Erstellungs- und Speicherstempel einen Tag zu spät geschrieben und einen Tag zu früh gelesen. Die Testsuite sah den Wert, den sie zugewiesen hatte; Excel sah morgen
const
// Tage von der FILETIME-Epoche (1601-01-01) zur TDateTime-Epoche (1899-12-30)
// Probe: 25569 + 109205 = 134774, die Unix-Epoche in FILETIME-Tagen
FileTimeDayBias = 109205;
function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
// Erst auf ganze Millisekunden runden, dann auf 100-ns-Ticks skalieren.
// Das Double direkt auf Ticks zu skalieren macht aus 04:00 ein 03:59:59.9999
Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
Der Rundungskommentar in diesem Gerüst ist die zweite, kleinere Lektion aus demselben Code. Einen gebrochenen TDateTime direkt mit 864.000.000.000 Ticks pro Tag zu multiplizieren lässt binären Gleitkommafehler in die unteren Stellen sickern, und ein Stempel von exakt 04:00 kam als 03:59:59.9999 zurück. HotXLS v2.384.48 rundet vor dem Skalieren auf ganze Millisekunden, sodass Werte zur vollen Stunde die Reise unversehrt überstehen. Dasselbe Release ergänzte den Zeitzonen-Schritt, den dieses Gerüst bewusst weglässt, weil der Input hier bereits UTC ist
Welche SummaryInformation-Property-IDs tragen die Datumswerte?
In dem von [MS-OLEPS] definierten \005SummaryInformation-Property-Set liegt die Erstellungszeit unter Property-ID $0C (PIDSI_CREATE_DTM), die letzte Speicherzeit unter $0D (PIDSI_LASTSAVE_DTM) und die gesamte Bearbeitungszeit unter $0A (PIDSI_EDITTIME). Ältere HotXLS-Builds schrieben den Speicherstempel nach $0E, also PIDSI_PAGECOUNT, sodass Excel kein Speicherdatum zeigen konnte und eine Seitenzahl-Property einen Zeitstempel trug. Seit v2.384.17 respektiert der Reader auch dieses Legacy-Layout: Fehlt $0D und trägt $0E ein VT_FILETIME, gilt der Wert als letzte Speicherzeit. Jedes gelesene PROPVARIANT wird jetzt außerdem mit PropVariantClear freigegeben, denn eine fehlerhafte Datei kann unter jeder dieser IDs einen String parken. Wer diese Streams mit eigenen Augen sehen will: Der Walk-through OLE2-Compound-Files in Delphi ohne COM IStorage lesen zeigt, wie man hinkommt
PIDSI_EDITTIME ist die Falle in der Falle. Die Property ist als VT_FILETIME typisiert, enthält aber eine Dauer, die rohe Zahl vergangener 100-ns-Ticks ohne addierte Epoche. Der alte Writer behandelte sie wie ein Datum, teilte EditTimeMinutes durch 1440 und schob das Ergebnis durch die Epochenumrechnung – 125 Bearbeitungsminuten landeten so als rund 299 Jahre in der Datei. Der aktuelle Reader erkennt diese Kodierung an ihrer Größe: Keine echte Bearbeitungssitzung spannt drei Jahrhunderte, also wird jeder Wert ab 109206 Tagen vor dem Befüllen von EditTimeMinutes um den Legacy-Offset vermindert
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
Book.Title := 'Q3 settlement';
// API-Werte sind lokale Zeit; die Datei speichert UTC-FILETIMEs
Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
Book.LastSavedDate := Now; // HotXLS schreibt, was Sie zuweisen, es stempelt kein Now
Book.RevisionNumber := 7;
Book.EditTimeMinutes := 125; // eine Dauer, gespeichert als rohe Ticks
if Book.SaveAs('settlement.xls') <> 1 then
raise Exception.Create('Save failed');
finally
Book.Free;
end;
end;
Warum gingen XLSX-Daten genau um den Zeitzonen-Offset daneben?
XLSX-Daten gingen um den Zonen-Offset daneben, weil dcterms:created und dcterms:modified in docProps/core.xml W3CDTF-Werte mit angehängtem Z sind, was im Core-Properties-Modell von ECMA-376 Part 2 UTC bedeutet, und HotXLS stempelte früher lokale Zeit mit diesem Z. Eine um 09:30 auf einer Maschine in UTC+8 erzeugte Arbeitsmappe trug 09:30:00Z, und Excel auf derselben Maschine rechnete es zu 17:30 um. Die klassische Engine hatte denselben Makel in ihren FILETIME-Werten, und über TXLSXWorkbook.CustomProperties.AddDate hinzugefügte benutzerdefinierte Datumseigenschaften (geschrieben als vt:filetime) teilten ihn. Seit v2.384.48 rechnen alle drei Pfade vor dem Schreiben um und beim Lesen zurück, wann immer der Stempel ein Z trägt, und seit v2.384.59 versteht die Leseseite auch Sekundenbruchteile und explizite +hh:mm / -hh:mm-Offsets
Bei der Umrechnung selbst geht ein naiver Fix schief. LocalFileTimeToFileTime wendet den Offset an, der gerade jetzt gilt, also kommt ein Januari-Stempel, der im Juli konvertiert wird, in jeder Zone mit Sommerzeit um eine Stunde daneben heraus. HotXLS ruft stattdessen TzSpecificLocalTimeToSystemTime und SystemTimeToTzSpecificLocalTime, die Normal- oder Sommerzeit aus dem Datum wählen, das konvertiert wird, und ein nicht gesetzter Wert von null geht unangetastet durch, sodass er nie zu einem 1899-Datum wird, das um ein paar Stunden verschoben ist
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('report-template.xlsx') <> 1 then
raise Exception.Create('Template not available');
Book.Created := EncodeDate(2026, 7, 1) + EncodeTime(9, 30, 0, 0);
Book.Modified := Now;
Book.CustomProperties.AddDate('ApprovedOn',
EncodeDate(2026, 1, 20) + EncodeTime(17, 0, 0, 0));
Book.SaveAs('report.xlsx');
// Auf einer Maschine mit Mitteleuropäischer Zeit enthält core.xml jetzt
// <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
// (UTC+2 im Juli), während ApprovedOn als 16:00Z geschrieben wird (UTC+1 im Januar)
finally
Book.Free;
end;
end;
Was konvertiert HotXLS beim Lesen von Zeitstempeln nicht?
Der HotXLS-W3CDTF-Reader konvertiert seit v2.384.59 jede zonemarkierte Form des Profils, und der eine Fall, den er weiterhin in Ruhe lässt, ist eine Zeit ohne Zone. Vor diesem Release nahm der Parser die ersten 19 Zeichen und rechnete nur dann von UTC um, wenn Zeichen 20 ein Z war – ein Stempel mit Sekundenbruchteilen (01:30:00.5Z) oder explizitem Offset (+08:00) wurde also als lokale Zeit ohne Anpassung gelesen und landete um den Zonen-Offset daneben. Seit HotXLS 2.384.59 parsen Created, Modified und datenwertige Custom-Properties Sekundenbruchteile beliebiger Länge, Z und +hh:mm / -hh:mm-Offsets, rechnen den Zeitpunkt nach UTC und dann in lokale Zeit um, und einen reinen Datumstempel wie 2026-07-01 lesen sie als ebendieses Datum. Ein Stempel mit Zeit, aber ohne Zonenmarker, den das W3CDTF-Profil nicht erlaubt und für den ECMA-376 Part 2 keine Regel vorsieht, wird weiterhin unverändert als lokale Zeit gelesen, und ein Stempel, der sich gar nicht parsen lässt, kommt als null zurück. Arbeitsmappen, die durch Excel gegangen sind, sind in Ordnung; Pakete anderer Generatoren, die die Zone fallen lassen, verdienen eine Stichprobe
Dateien, die ältere HotXLS-Builds geschrieben haben, sind die andere ehrliche Grenze. Ein vor v2.384.48 geschriebener XLSX-Stempel war lokale Zeit in einem Z-Kostüm, und nichts in der Datei unterscheidet ihn von einem korrekten, also verschiebt der aktuelle Reader ihn um den Zonen-Offset. Klassische FILETIME-Stempel aus diesen Builds bekommen dieselbe Verschiebung, und ein vor v2.384.17 geschriebenes Erstellungsdatum liest zusätzlich einen Tag zu spät zurück, weil sich der Extratag der alten Konstante ebenfalls nicht nachweisen lässt; nur die Edit-Time-Kodierung und die $0E-Platzierung haben eine erkennbare Signatur. Denken Sie auch daran, dass der API-Wert lokal zur Maschine ist, die liest – ein Dienst in UTC und ein Desktop in Tokio melden für dieselbe Datei unterschiedliche CreatedDate-Werte, beide korrekt
Wie testen Sie Dokument-Zeitstempel?
Testen Sie Dokument-Zeitstempel gegen etwas, das Ihr eigener Code nicht geschrieben hat. Beide Bugs aus diesem Artikel haben eine Speichern-und-erneut-öffnen-Prüfung passiert, denn ein symmetrischer Fehler ist für einen symmetrischen Test unsichtbar. Vergleichen Sie mit einer von Excel gespeicherten Arbeitsmappe, oder prüfen Sie die rohen Bytes und den XML-Text nach dem Speichern, und lassen Sie die Suite auf einer Maschine laufen, die auf eine Nicht-UTC-Zone eingestellt ist, mit einem Testdatum auf jeder Seite einer Sommerzeit-Umstellung. Ein Build-Agent, der in UTC läuft, besteht den alten, kaputten Code munter weiter
Dokument-Zeitstempel sind klein, aber sie sind das, wonach Recordsysteme, Suchindizes und Audit-Trails sortieren, und ein Datum, das um einen Tag oder acht Stunden danebenliegt, ist schlimmer als ein fehlendes, weil niemand es anzweifelt. Die HotXLS Delphi spreadsheet component übernimmt die Epoch-Arithmetik, die Property-IDs und die UTC-Umrechnung für .xls und .xlsx, sodass Ihr Code schlicht lokale TDateTime-Werte zuweisen und das Dateiformat der Bibliothek überlassen kann