HotXLS slaat de tijdstempels van Excel-documenteigenschappen in het bestand als UTC op en legt ze via de API als lokale tijd bloot: TXLSWorkbook.CreatedDate en LastSavedDate voor .xls, TXLSXWorkbook.Created en Modified voor .xlsx. Sinds v2.384.48 converteren beide engines lokale tijd bij het wegschrijven naar UTC en bij het lezen terug, met de zomertijdregels die op de datum van de stempel zelf gelden. Twee fixes waren daarvoor nodig, en beide bugs hadden om dezelfde gênante reden overleefd: elke geautomatiseerde round trip slaagde, terwijl het deelvenster Bestand > Info van Excel de verkeerde dag of het verkeerde uur toonde. Als u ons overzicht van het instellen van Excel-documenteigenschappen in Delphi hebt gelezen, dit is het deel waar datums ophouden eenvoudige waarden te zijn
Waarom verborg een opslag-en-heropen-test een fout van één dag?
Een round trip met zichzelf verborg de fout omdat writer en reader dezelfde verkeerde constante deelden, dus de vergissing hefte zichzelf op. Een datum in een OLE-property-set is een FILETIME, een 64-bits telling van 100-nanoseconde-ticks sinds 1601-01-01 UTC ([MS-DTYP] §2.3.3), terwijl een Delphi-TDateTime dagen telt vanaf 1899-12-30, hetzelfde serienulpunt als in Excel-datumserienummers in Delphi en de 1900- versus 1904-systemen. Het gat tussen de twee epochen is 109205 dagen, wat u zonder kalender kunt controleren: 25569 (het Unix-tijdperk als TDateTime) plus 109205 geeft 134774, het Unix-tijdperk geteld in FILETIME-dagen. HotXLS-builds vóór v2.384.17 gebruikten 109206, dus elke aanmaak- en opslagstempel werd één dag te laat weggeschreven en één dag te vroeg gelezen. De testsuite zag de waarde die hij had toegekend; Excel zag morgen
const
// dagen van het FILETIME-tijdperk (1601-01-01) naar het TDateTime-tijdperk (1899-12-30)
// controleer: 25569 + 109205 = 134774, het Unix-tijdperk in FILETIME-dagen
FileTimeDayBias = 109205;
function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
// Rond eerst af op hele milliseconden, schaal dan naar 100 ns-ticks.
// De Double rechtstreeks naar ticks schalen maakt van 04:00 03:59:59.9999
Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
Het afrondcommentaar in die schets is de tweede, kleinere les uit dezelfde code. Een fractionele TDateTime rechtstreeks vermenigvuldigen met 864.000.000.000 ticks per dag laat binnaire floating-point-fout naar de onderste cijfers lekken, en een stempel van precies 04:00 kwam terug als 03:59:59.9999. HotXLS v2.384.48 rondt af op hele milliseconden vóór het schalen, dus waarden op het hele uur overleven de reis ongeschonden. Dezelfde release voegde de tijdzone-stap toe die deze schets met opzet weglaat, omdat de invoer hier al UTC is
Welke SummaryInformation-property-ids bevatten de datums?
In de door [MS-OLEPS] gedefinieerde \005SummaryInformation-property-set staat de aanmaaktijd onder property-id $0C (PIDSI_CREATE_DTM), de laatst-opgeslagen-tijd onder $0D (PIDSI_LASTSAVE_DTM), en de totale bewerkingstijd onder $0A (PIDSI_EDITTIME). Oudere HotXLS-builds schreven de opslagstempel naar $0E, dat PIDSI_PAGECOUNT is, dus Excel had geen opslagdatum te tonen en een paginatelling-eigenschap met een tijdstempel erin. Sinds v2.384.17 eert de reader ook die oude indeling: wanneer $0D ontbreekt en $0E een VT_FILETIME draagt, geldt de waarde als laatst-opgeslagen-tijd. Elke gelezen PROPVARIANT wordt ook met PropVariantClear vrijgegeven, want een misvormd bestand kan onder elk van deze ids een string parkeren. Wilt u die streams met eigen ogen zien, de walk-through over het lezen van OLE2-compound-files in Delphi zonder COM IStorage laat zien hoe u ze bereikt
PIDSI_EDITTIME is de val binnen de val. De property is getypeerd als VT_FILETIME maar bevat een duur, het ruwe aantal verstreken 100-ns-ticks zonder epoch erbij. De oude writer behandelde haar als een datum, deelde EditTimeMinutes door 1440 en duwde het resultaat door de epoch-conversie, dus 125 minuten bewerken belandde in het bestand als ruwweg 299 jaar. De huidige reader herkent die encoding aan haar grootte: geen enkele echte bewerkingssessie beslaat drie eeuwen, dus elke waarde van 109206 dagen of meer krijgt de oude offset afgetrokken voordat EditTimeMinutes wordt gevuld
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
Book.Title := 'Q3 settlement';
// API-waarden zijn lokale tijd; het bestand bewaart UTC-FILETIMEs
Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
Book.LastSavedDate := Now; // HotXLS schrijft wat u toekent, hij stampt geen Now
Book.RevisionNumber := 7;
Book.EditTimeMinutes := 125; // een duur, opgeslagen als ruwe ticks
if Book.SaveAs('settlement.xls') <> 1 then
raise Exception.Create('Save failed');
finally
Book.Free;
end;
end;
Waarom zaten XLSX-datums er precies de tijdzone-offset naast?
XLSX-datums zaten er de zone-offset naast omdat dcterms:created en dcterms:modified in docProps/core.xml W3CDTF-waarden zijn met een Z-tag, wat onder het core-properties-model van ECMA-376 Part 2 UTC betekent, en HotXLS stempelde voorheen lokale tijd met die Z eraan. Een werkmap aangemaakt om 09:30 op een machine in UTC+8 droeg 09:30:00Z, en Excel op diezelfde machine zette haar om naar 17:30. De klassieke engine had hetzelfde mankement in zijn FILETIME-waarden, en aangepaste datum-eigenschappen die via TXLSXWorkbook.CustomProperties.AddDate werden toegevoegd (weggeschreven als vt:filetime) deden hetzelfde. Sinds v2.384.48 converteren alle drie de paden vóór het wegschrijven en bij het lezen terug zodra de stempel een Z draagt, en sinds v2.384.59 eert de leeskant ook fractionele seconden en expliciete +hh:mm- / -hh:mm-offsets
De conversie zelf is waar een naïeve fix de mist ingaat. LocalFileTimeToFileTime past de offset toe die nu geldt, dus een januaristempel die in juli wordt omgezet komt er een uur naast uit in elke zone met zomertijd. HotXLS roept in plaats daarvan TzSpecificLocalTimeToSystemTime en SystemTimeToTzSpecificLocalTime aan, die standaard- of zomertijd kiezen op basis van de te converteren datum, en een niet-gezette waarde van nul gaat onaangeroerd door zodat hij nooit in een 1899-datum verspringt van een paar uur
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');
// Op een machine die op Central European Time staat bevat core.xml nu
// <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
// (UTC+2 in juli), terwijl ApprovedOn als 16:00Z wordt weggeschreven (UTC+1 in januari)
finally
Book.Free;
end;
end;
Wat converteert HotXLS bij het lezen van tijdstempels niet?
De HotXLS-W3CDTF-reader converteert sinds v2.384.59 elke zonetagged vorm uit het profiel, en het ene geval dat ze nog met rust laat is een tijd zonder zone. Vóór die release nam de parser de eerste 19 tekens en convergeerde ze alleen vanuit UTC wanneer teken 20 een Z was, dus een stempel met fractionele seconden (01:30:00.5Z) of een expliciete offset (+08:00) werd als lokale tijd zonder aanpassing gelezen en zat er de zone-offset naast. Sinds HotXLS 2.384.59 parsen Created, Modified en aangepaste eigenschappen met datumwaarde fractionele seconden van elke lengte, Z, en +hh:mm- / -hh:mm-offsets, zetten het tijdstip om naar UTC en daarna naar lokale tijd, en lezen een stempel met alleen een datum zoals 2026-07-01 als die datum. Een stempel met een tijd maar geen zonemarkering, die het W3CDTF-profiel niet toestaat en waarvoor ECMA-376 Part 2 geen regel geeft, wordt nog steeds ongewijzigd als lokale tijd gelezen, en een stempel die helemaal niet parseert komt terug als nul. Werkmappen die door Excel gaan zijn in orde; pakketten van andere generatoren die de zone laten vallen verdienen een steekproef
Bestanden die door oudere HotXLS-builds zijn geschreven vormen de andere eerlijke grens. Een XLSX-stempel van vóór v2.384.48 was lokale tijd met een Z om, en niets in het bestand onderscheidt haar van een correcte, dus de huidige reader verschuift haar met de zone-offset. Klassieke FILETIME-stempels uit die builds krijgen dezelfde verschuiving, en een aanmaakdatum van vóór v2.384.17 komt bovendien één dag te laat terug, want de extra dag van de oude constante is evenmin te detecteren; alleen de edit-time-encoding en de $0E-plaatsing hebben een herkenbare signatuur. Bedenk ook dat de API-waarde lokaal is voor de machine die leest, dus een dienst die in UTC draait en een desktop in Tokio rapporteren verschillende CreatedDate-waarden voor hetzelfde bestand, beide correct
Hoe test u documenttijdstempels?
Test documenttijdstempels tegen iets dat uw eigen code niet heeft geschreven. Beide bugs uit dit verhaal haalden een opslag-daarna-heropen-controle, want een symmetrische vergissing is onzichtbaar voor een symmetrische test. Vergelijk met een door Excel opgeslagen werkmap, of eis de ruwe bytes en de XML-tekst na het opslaan, en draai de suite op een machine die in een niet-UTC-zone staat met een testdatum aan weerszijden van een zomertijdomslag. Een build-agent die in UTC draait laat de oude, kapotte code graag slagen
Documenttijdstempels zijn klein, maar het is waar registersystemen, zoekindexen en audittrails op sorteren, en een datum die er een dag of acht uur naast zit is erger dan een ontbrekende, omdat niemand haar in twijfel trekt. De HotXLS Delphi-spreadsheetcomponent regelt de epoch-rekenkunde, de property-ids en de UTC-conversie voor zowel .xls als .xlsx, zodat uw code gewone lokale TDateTime-waarden kan toekennen en het bestandsformaat aan de library kunt laten