HotXLS memorizza i timestamp delle proprietà dei documenti Excel come UTC dentro il file e li espone come ora locale tramite l'API: TXLSWorkbook.CreatedDate e LastSavedDate per .xls, TXLSXWorkbook.Created e Modified per .xlsx. Dalla v2.384.48 entrambi gli engine convertono l'ora locale in UTC in scrittura e viceversa in lettura, usando le regole dell'ora legale valide sulla data della marcatura stessa. Arrivarci è costato due correzioni, ed entrambi i bug erano sopravvissuti per la stessa imbarazzante ragione: ogni round trip automatizzato passava, mentre il riquadro File > Info di Excel mostrava il giorno sbagliato o l'ora sbagliata. Se avete letto la nostra panoramica su impostare le proprietà dei documenti Excel in Delphi, questo è il punto in cui le date smettono di essere semplici valori
Perché un test salva-e-riapri nascondeva un errore di un giorno?
Un round trip su se stesso nascondeva l'errore perché writer e reader condividevano la stessa costante sbagliata, così lo sbaglio si cancellava da solo. Una data di un property set OLE è un FILETIME, un conteggio a 64 bit di tick da 100 nanosecondi dal 1601-01-01 UTC ([MS-DTYP] §2.3.3), mentre un TDateTime Delphi conta i giorni dal 1899-12-30, la stessa origine seriale trattata in seriali di date Excel in Delphi e i sistemi 1900 vs 1904. Il divario tra le due epoche è di 109205 giorni, verificabile senza calendario: 25569 (l'epoca Unix come TDateTime) più 109205 dà 134774, l'epoca Unix contata in giorni FILETIME. Le build HotXLS prima della v2.384.17 usavano 109206, quindi ogni marcatura di creazione e salvataggio veniva scritta un giorno tardi e letta un giorno presto. La suite di test vedeva il valore che aveva assegnato; Excel vedeva domani
const
// giorni dall'epoca FILETIME (1601-01-01) all'epoca TDateTime (1899-12-30)
// verifica: 25569 + 109205 = 134774, l'epoca Unix in giorni FILETIME
FileTimeDayBias = 109205;
function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
// Arrotondate prima a millisecondi interi, poi scalate a tick da 100 ns.
// Scalare il Double direttamente a tick trasforma 04:00 in 03:59:59.9999
Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
Il commento sull'arrotondamento in quello schema è la seconda, più piccola lezione dello stesso codice. Moltiplicare un TDateTime frazionario direttamente per 864.000.000.000 tick al giorno lascia che l'errore del floating point binario si infili nelle cifre finali, e una marcatura di 04:00 esatte tornava come 03:59:59.9999. HotXLS v2.384.48 arrotonda a millisecondi interi prima di scalare, così i valori in punto sopravvivono al viaggio intatti. La stessa release ha aggiunto il passaggio del fuso orario che questo schema omette deliberatamente, perché l'input qui è già UTC
Quali ID di proprietà SummaryInformation contengono le date?
Nel property set \005SummaryInformation definito da [MS-OLEPS], l'ora di creazione sta sotto l'ID di proprietà $0C (PIDSI_CREATE_DTM), l'ultimo salvataggio sotto $0D (PIDSI_LASTSAVE_DTM), e il tempo totale di editing sotto $0A (PIDSI_EDITTIME). Le build HotXLS più vecchie scrivevano la marcatura di salvataggio a $0E, che è PIDSI_PAGECOUNT, quindi Excel non aveva una data di salvataggio da mostrare e una proprietà conteggio pagine con dentro un timestamp. Dalla v2.384.17 il reader onora anche quel layout legacy: quando $0D è assente e $0E porta un VT_FILETIME, il valore è preso come ora di ultimo salvataggio. Ogni PROPVARIANT letto ormai viene anche rilasciato con PropVariantClear, perché un file malformato può parcheggiare una stringa sotto uno qualsiasi di questi ID. Se volete vedere quegli stream con i vostri occhi, la guida su leggere file compound OLE2 in Delphi senza COM IStorage mostra come arrivarci
PIDSI_EDITTIME è la trappola dentro la trappola. La proprietà è tipizzata VT_FILETIME ma contiene una durata, il numero grezzo di tick da 100 ns trascorsi senza epoca aggiunta. Il vecchio writer la trattava come una data, dividendo EditTimeMinutes per 1440 e spingendo il risultato nella conversione d'epoca, così 125 minuti di editing finivano nel file come circa 299 anni. Il reader attuale riconosce quella codifica dalla dimensione: nessuna sessione di editing reale copre tre secoli, quindi qualsiasi valore di 109206 giorni o più ha l'offset legacy sottratto prima che EditTimeMinutes venga riempito
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
Book.Title := 'Q3 settlement';
// i valori API sono ora locale; il file memorizza FILETIME UTC
Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
Book.LastSavedDate := Now; // HotXLS scrive ciò che assegnate, non timbra Now da sé
Book.RevisionNumber := 7;
Book.EditTimeMinutes := 125; // una durata, memorizzata come tick grezzi
if Book.SaveAs('settlement.xls') <> 1 then
raise Exception.Create('Save failed');
finally
Book.Free;
end;
end;
Perché le date XLSX erano sbagliate di esattamente l'offset del fuso?
Le date XLSX erano sbagliate dell'offset del fuso perché dcterms:created e dcterms:modified in docProps/core.xml sono valori W3CDTF taggati con Z, che significa UTC secondo il modello delle core properties di ECMA-376 Part 2, e HotXLS un tempo timbrava l'ora locale con quella Z attaccata. Un workbook creato alle 09:30 su una macchina in UTC+8 portava 09:30:00Z, ed Excel sulla stessa macchina lo convertiva in 17:30. L'engine classico aveva lo stesso identico difetto nei suoi valori FILETIME, e le proprietà data personalizzate aggiunte tramite TXLSXWorkbook.CustomProperties.AddDate (scritte come vt:filetime) lo condividevano anch'esse. Dalla v2.384.48 tutti e tre i percorsi convertono prima di scrivere e riconvertono in lettura ogni volta che la marcatura porta una Z, e dalla v2.384.59 il lato lettura onora anche i secondi frazionari e gli offset espliciti +hh:mm / -hh:mm
La conversione in sé è dove una correzione ingenua va storta. LocalFileTimeToFileTime applica l'offset in vigore adesso, quindi una marcatura di gennaio convertita a luglio esce con un'ora di scarto in qualsiasi fuso con ora legale. HotXLS chiama invece TzSpecificLocalTimeToSystemTime e SystemTimeToTzSpecificLocalTime, che scelgono ora solare o legale dalla data che si sta convertendo, e un valore non impostato di zero passa indenne, così non diventa mai una data 1899 spostata di qualche ora
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');
// Su una macchina impostata su Central European Time, core.xml ora contiene
// <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
// (UTC+2 a luglio), mentre ApprovedOn viene scritto come 16:00Z (UTC+1 a gennaio)
finally
Book.Free;
end;
end;
Che cosa HotXLS non converte leggendo i timestamp?
Il reader W3CDTF di HotXLS converte ogni forma con tag di fuso del profilo dalla v2.384.59, e l'unico caso che lascia ancora in pace è un'ora senza fuso. Prima di quella release il parser prendeva i primi 19 caratteri e convertiva da UTC solo quando il carattere 20 era Z, quindi una marcatura con secondi frazionari (01:30:00.5Z) o un offset esplicito (+08:00) veniva letta come ora locale senza correzioni e finiva fuori dell'offset del fuso. Da HotXLS 2.384.59, Created, Modified e le proprietà personalizzate con valore data analizzano secondi frazionari di qualsiasi lunghezza, Z e offset +hh:mm / -hh:mm, convertono l'istante in UTC e poi in ora locale, e leggono una marcatura solo-data come 2026-07-01 come quella data. Una marcatura con ora ma senza indicatore di fuso, che il profilo W3CDTF non consente e per cui ECMA-376 Part 2 non dà regole, resta letta come ora locale invariata, e una marcatura che non si analizza affatto torna come zero. I workbook che passano per Excel stanno bene; i pacchetti prodotti da altri generatori che buttano via il fuso meritano un controllo a campione
I file scritti da build HotXLS più vecchie sono l'altro confine onesto. Una marcatura XLSX scritta prima della v2.384.48 era ora locale con una Z addosso, e niente nel file la distingue da una corretta, quindi il reader attuale la sposta dell'offset del fuso. Le marcature FILETIME classiche di quelle build prendono lo stesso spostamento, e una data di creazione scritta prima della v2.384.17 si rilegge in più un giorno tardi, perché il giorno extra della vecchia costante non è rilevabile nemmeno lui; solo la codifica dell'edit time e la posizione a $0E hanno una firma riconoscibile. Tenete presente anche che il valore API è locale alla macchina che legge, quindi un servizio in UTC e un desktop a Tokyo riporteranno valori CreatedDate diversi per lo stesso file, entrambi corretti
Come si devono testare i timestamp dei documenti?
Testate i timestamp dei documenti contro qualcosa che il vostro codice non ha scritto. Entrambi questi bug hanno passato un controllo salva-poi-riapri, perché uno sbaglio simmetrico è invisibile a un test simmetrico. Confrontate con un workbook salvato da Excel, oppure fate assert sui byte grezzi e sul testo XML dopo il salvataggio, e fate girare la suite su una macchina impostata su un fuso non UTC con una data di test su ciascun lato di un cambio di ora legale. Un build agent che gira in UTC passerà allegramente il vecchio codice rotto
I timestamp dei documenti sono piccoli, ma sono ciò per cui i sistemi di record, gli indici di ricerca e le tracce di audit ordinano, e una data sbagliata di un giorno o di otto ore è peggiore di una mancante perché nessuno la mette in discussione. Il componente foglio di calcolo HotXLS per Delphi gestisce l'aritmetica delle epoche, gli ID di proprietà e la conversione UTC per .xls e .xlsx, così il vostro codice può assegnare semplici valori TDateTime locali e lasciare il formato file alla libreria