HotXLS lagrar tidsstämplar för Excel-dokumentegenskaper som UTC i filen och exponerar dem som lokal tid genom API:t: TXLSWorkbook.CreatedDate och LastSavedDate för .xls, TXLSXWorkbook.Created och Modified för .xlsx. Sedan v2.384.48 konverterar båda motorerna lokal tid till UTC vid skrivning och tillbaka vid läsning, med de sommartidsregler som gäller på stämpelns eget datum. Vägen dit krävde två fixar, och båda buggarna hade överlevt av samma pinsamma skäl: varje automatiserad rundtur gick igenom, medan Excels File > Info-panel visade fel dag eller fel timme. Om du har läst vår översikt av att sätta Excel-dokumentegenskaper i Delphi, är det här delen där datumen slutar vara enkla värden
Varför dolde ett spara-sen-öppna-test ett endagsfel?
En rundtur mot sig själv dolde felet eftersom skrivaren och läsaren delade samma felaktiga konstant, så misstaget strök ut sig självt. Ett datum i en OLE-egenskapsmängd är en FILETIME, en 64-bitars räkning av 100-nanosekundstick sedan 1601-01-01 UTC ([MS-DTYP] §2.3.3), medan en Delphi-TDateTime räknar dagar från 1899-12-30, samma serialursprung som tas upp i Excel-datumserialer i Delphi och systemen 1900 kontra 1904. Avståndet mellan de två epokerna är 109205 dagar, vilket du kan kontrollera utan kalender: 25569 (Unix-epoken som TDateTime) plus 109205 ger 134774, Unix-epoken räknad i FILETIME-dagar. HotXLS-byggen före v2.384.17 använde 109206, så varje skapande- och sparningstämpel skrevs en dag för sent och lästes en dag för tidigt. Testsviten såg värdet den tilldelat; Excel såg imorgon
const
// dagar från FILETIME-epoken (1601-01-01) till TDateTime-epoken (1899-12-30)
// kontroll: 25569 + 109205 = 134774, Unix-epoken i FILETIME-dagar
FileTimeDayBias = 109205;
function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
// Avrunda till hela millisekunder först, skala sedan till 100 ns-tick.
// Att skala Double:n rakt till tick gör 04:00 till 03:59:59.9999
Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
Avrundningskommentaren i den skissen är den andra, mindre lärdomen från samma kod. Att multiplicera en bråkdelad TDateTime direkt med 864 000 000 000 tick per dag låter binärt flyttalsfel läcka in i de sista siffrorna, och en stämpel på exakt 04:00 kom tillbaka som 03:59:59.9999. HotXLS v2.384.48 avrundar till hela millisekunder innan skalningen, så jämna heltimmesvärden överlever färden oskadda. Samma utgåva lade till tidszonssteget som den här skissen medvetet utelämnar, eftersom indata här redan är UTC
Vilka SummaryInformation-egenskaps-ID:n bär datumen?
I egenskapsmängden \005SummaryInformation som definieras av [MS-OLEPS] ligger skapandetiden under egenskaps-ID $0C (PIDSI_CREATE_DTM), senast-sparad-tiden under $0D (PIDSI_LASTSAVE_DTM) och den totala redigeringstiden under $0A (PIDSI_EDITTIME). Äldre HotXLS-byggen skrev sparningstämpeln till $0E, som är PIDSI_PAGECOUNT, så Excel hade inget sparningsdatum att visa och en sidräkningsegenskap som höll en tidsstämpel. Sedan v2.384.17 hedrar läsaren även den äldre layouten: när $0D saknas och $0E bär en VT_FILETIME tas värdet som senast-sparad-tiden. Varje PROPVARIANT-läsning frigörs nu också med PropVariantClear, eftersom en felformaterad fil kan parkera en sträng under vilket av dessa ID:n som helst. Om du vill se de strömmarna med egna ögon visar genomgången att läsa OLE2-sammansättningsfiler i Delphi utan COM IStorage hur du når dem
PIDSI_EDITTIME är fällan inuti fällan. Egenskapen är typad som VT_FILETIME men rymmer en varaktighet, det råa antalet förflutna 100 ns-tick utan tillagd epok. Den gamla skrivaren behandlade den som ett datum, delade EditTimeMinutes med 1440 och pressade resultatet genom epokomvandlingen, så 125 minuters redigering hamnade i filen som dryga 299 år. Den nuvarande läsaren känner igen den kodningen på storleken: ingen verklig redigeringssession spänner över tre århundraden, så alla värden på 109206 dagar eller mer får den gamla förskjutningen subtraherad innan EditTimeMinutes fylls i
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
Book.Title := 'Q3 settlement';
// API-värden är lokal tid; filen lagrar UTC-FILETIME:er
Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
Book.LastSavedDate := Now; // HotXLS skriver vad du tilldelar, den stämplar inte Now
Book.RevisionNumber := 7;
Book.EditTimeMinutes := 125; // en varaktighet, lagrad som råa tick
if Book.SaveAs('settlement.xls') <> 1 then
raise Exception.Create('Save failed');
finally
Book.Free;
end;
end;
Varför avvek XLSX-datum med exakt tidszonsoffseten?
XLSX-datum avvek med zonsoffseten eftersom dcterms:created och dcterms:modified i docProps/core.xml är W3CDTF-värden märkta med Z, vilket betyder UTC enligt core properties-modellen i ECMA-376 Part 2, och HotXLS stämplade förr lokal tid med den Z-en på. En arbetsbok skapad 09:30 på en maskin i UTC+8 bar 09:30:00Z, och Excel på samma maskin omvandlade den till 17:30. Den klassiska motorn hade samma fel i sina FILETIME-värden, och egna datumegenskaper lagda genom TXLSXWorkbook.CustomProperties.AddDate (skrivna som vt:filetime) delade det också. Sedan v2.384.48 omvandlar alla tre vägarna innan skrivning och tillbaka vid läsning närhelst stämpeln bär en Z, och sedan v2.384.59 hedrar lässidan även bråkdelar av sekunder och explicita +hh:mm- / -hh:mm-offsets
Omvandlingen i sig är där en naiv fix går fel. LocalFileTimeToFileTime tillämpar den offset som gäller just nu, så en januaristämpel som omvandlas i juli hamnar en timme fel i valfri zon med sommartid. HotXLS anropar i stället TzSpecificLocalTimeToSystemTime och SystemTimeToTzSpecificLocalTime, som väljer normaltid eller sommartid utifrån det datum som omvandlas, och ett osatt nollvärde passerar orört så att det aldrig blir ett 1899-datum förskjutet några timmar
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');
// På en maskin inställd på Central European Time innehåller core.xml nu
// <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
// (UTC+2 i juli), medan ApprovedOn skrivs som 16:00Z (UTC+1 i januari)
finally
Book.Free;
end;
end;
Vad omvandlar HotXLS inte när tidsstämplar läses?
HotXLS W3CDTF-läsare omvandlar varje zonmärkt form av profilen sedan v2.384.59, och det enda fall den fortfarande lämnar orört är en tid utan zon. Före den utgåvan tog tolken de första 19 tecknen och omvandlade från UTC bara när tecken 20 var en Z, så en stämpel med bråkdelar av sekunder (01:30:00.5Z) eller en explicit offset (+08:00) lästes som lokal tid utan justering och hamnade fel med zonsoffseten. Sedan HotXLS 2.384.59 tolkar Created, Modified och datumvärda egna egenskaper bråkdelar av sekunder av valfri längd, Z samt +hh:mm- / -hh:mm-offsets, omvandlar tidpunkten till UTC och sedan till lokal tid och läser en ren datumsstämpel som 2026-07-01 som just det datumet. En stämpel med tid men ingen zonmarkör, som W3CDTF-profilen inte tillåter och ECMA-376 Part 2 inte ger någon regel för, läses fortfarande som lokal tid oförändrad, och en stämpel som inte går att tolka alls kommer tillbaka som noll. Arbetsböcker som passerat Excel är fina; paket från andra generatorer som tappar zonen förtjänar stickprov
Filer skrivna av äldre HotXLS-byggen är den andra ärliga gränsen. En XLSX-stämpel skriven före v2.384.48 var lokal tid iklädd en Z, och ingenting i filen skiljer den från en korrekt, så den nuvarande läsaren skiftar den med zonsoffseten. Klassiska FILETIME-stämplar från de byggena tar samma skifte, och ett skapandedatum skrivet före v2.384.17 läses dessutom tillbaka en dag för sent, eftersom den extra dagen i den gamla konstanten inte heller går att upptäcka; bara redigeringstidskodningen och placeringen i $0E har en igenkännbar signatur. Tänk också på att API-värdet är lokalt för maskinen som gör läsningen, så en tjänst i UTC och en dator i Tokyo rapporterar olika CreatedDate-värden för samma fil, båda korrekta
Hur ska du testa dokumenttidsstämplar?
Testa dokumenttidsstämplar mot något din egen kod inte skrev. Båda dessa buggar gick igenom en spara-sen-öppna-kontroll, för ett symmetriskt misstag är osynligt för ett symmetriskt test. Jämför mot en arbetsbok sparad av Excel, eller hävda mot de råa bytena och XML-texten efter sparning, och kör sviten på en maskin inställd på en icke-UTC-zon med ett testdatum på var sida om en sommartidsomställning. En byggagent som körs i UTC går gärna igenom den gamla, trasiga koden
Dokumenttidsstämplar är små, men de är det som registersystem, sökindex och revisionsspår sorterar efter, och ett datum som är fel med en dag eller åtta timmar är värre än ett saknat eftersom ingen ifrågasätter det. HotXLS Delphi-kalkylbladskomponenten sköter epokaritmetiken, egenskaps-ID:n och UTC-omvandlingen för både .xls och .xlsx, så din kod kan tilldela vanliga lokala TDateTime-värden och lämna filformatet till biblioteket