HotXLS stochează timestamp-urile proprietăților de document Excel ca UTC în interiorul fișierului și le expune ca oră locală prin API: TXLSWorkbook.CreatedDate și LastSavedDate pentru .xls, TXLSXWorkbook.Created și Modified pentru .xlsx. Din v2.384.48, ambele motoare convertesc ora locală în UTC la scriere și înapoi la citire, folosind regulile de ora de vară valabile la data propriului timestamp. Drumul până acolo a luat două reparări, iar ambele bug-uri au supraviețuit din același motiv jenant: fiecare round trip automatizat trecea, în timp ce panoul File > Info din Excel arăta ziua sau ora greșită. Dacă ați citit prezentarea noastră despre setarea proprietăților de document Excel în Delphi, iată partea în care datele încetează să mai fie valori simple
De ce un test de salvare-și-redeschidere ascundea o eroare de o zi?
Un round trip cu sine ascundea eroarea pentru că scriitorul și cititorul împărtășeau aceeași constantă greșită, deci greșeala se anula singură. O dată dintr-un set de proprietăți OLE este un FILETIME, un contor pe 64 de biți de tick-uri de 100 de nanosecunde de la 1601-01-01 UTC ([MS-DTYP] §2.3.3), în timp ce un TDateTime din Delphi numără zilele de la 1899-12-30, aceeași origine serială descrisă în numerele seriale de date Excel în Delphi și sistemele 1900 vs 1904. Golul dintre cele două epoci este de 109205 de zile, lucru pe care îl puteți verifica fără calendar: 25569 (epoca Unix ca TDateTime) plus 109205 dă 134774, epoca Unix numărată în zile de FILETIME. Build-urile HotXLS de dinainte de v2.384.17 foloseau 109206, deci fiecare timestamp de creare și de salvare era scris cu o zi mai târziu și citit cu o zi mai devreme. Suita de teste vedea valoarea pe care o asignase; Excel vedea mâine
const
// zile de la epoca FILETIME (1601-01-01) la epoca TDateTime (1899-12-30)
// verificare: 25569 + 109205 = 134774, epoca Unix în zile FILETIME
FileTimeDayBias = 109205;
function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
// Rotunjiți întâi la milisecunde întregi, apoi scalați la tick-uri de 100 ns.
// Scalarea Double-ului direct la tick-uri transformă 04:00 în 03:59:59.9999
Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
Comentariul despre rotunjire din schița aceea este a doua lecție, mai mică, din același cod. Înmulțirea unui TDateTime fracționar direct cu 864.000.000.000 de tick-uri pe zi lasă eroarea binară în virgulă mobilă să se scurgă în cifrele de jos, iar un timestamp de exact 04:00 se întorcea ca 03:59:59.9999. HotXLS v2.384.48 rotunjește la milisecunde întregi înainte de scalare, deci valorile fixe pe oră supraviețuiesc drumului intacte. Aceeași versiune a adăugat pasul de fus orar pe care schița îl omite deliberat, pentru că intrarea aici este deja UTC
Ce ID-uri de proprietăți SummaryInformation țin datele?
În setul de proprietăți \005SummaryInformation definit de [MS-OLEPS], ora de creare stă sub ID-ul de proprietate $0C (PIDSI_CREATE_DTM), ora ultimei salvări sub $0D (PIDSI_LASTSAVE_DTM), iar timpul total de editare sub $0A (PIDSI_EDITTIME). Build-urile HotXLS mai vechi scriau timestamp-ul de ultimă salvare la $0E, care este PIDSI_PAGECOUNT, deci Excel nu avea ce dată de salvare să arate și o proprietate de număr de pagini care ținea un timestamp. Din v2.384.17, cititorul onorează și acel aranjament vechi: când $0D lipsește și $0E poartă un VT_FILETIME, valoarea este luată ca oră a ultimei salvări. Fiecare PROPVARIANT citit este eliberat acum și cu PropVariantClear, pentru că un fișier malformat poate parca un șir sub oricare dintre aceste ID-uri. Dacă vreți să vedeți aceste fluxuri cu ochii dumneavoastră, tutorialul despre citirea fișierelor compuse OLE2 în Delphi fără COM IStorage arată cum ajungeți la ele
PIDSI_EDITTIME este capcana din capcană. Proprietatea este tipizată VT_FILETIME dar ține o durată, numărul brut de tick-uri de 100 de nanosecunde scurse, fără nicio epocă adăugată. Vechiul scriitor o trata ca pe o dată, împărțind EditTimeMinutes la 1440 și împingând rezultatul prin conversia de epocă, deci 125 de minute de editare ajungeau în fișier ca vreo 299 de ani. Cititorul actual recunoaște această codare după mărime: nicio sesiune reală de editare nu se întinde pe trei secole, deci orice valoare de 109206 de zile sau peste primește offsetul vechi scăzut înainte ca EditTimeMinutes să fie completat
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
Book.Title := 'Q3 settlement';
// Valorile API sunt oră locală; fișierul stochează FILETIME-uri UTC
Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
Book.LastSavedDate := Now; // HotXLS scrie ce asignați, nu pune singur Now
Book.RevisionNumber := 7;
Book.EditTimeMinutes := 125; // o durată, stocată ca tick-uri brute
if Book.SaveAs('settlement.xls') <> 1 then
raise Exception.Create('Save failed');
finally
Book.Free;
end;
end;
De ce datele XLSX erau derapate exact cu offsetul de fus orar?
Datele XLSX erau derapate cu offsetul zonei pentru că dcterms:created și dcterms:modified din docProps/core.xml sunt valori W3CDTF etichetate cu Z, ceea ce înseamnă UTC în modelul de proprietăți de bază din ECMA-376 Part 2, iar HotXLS puna timestamp de oră locală cu acel Z atașat. Un registru de lucru creat la 09:30 pe o mașină în UTC+8 purta 09:30:00Z, iar Excel pe aceeași mașină îl convertea în 17:30. Motorul clasic avea aceeași deficiență în valorile FILETIME, iar proprietățile de dată personalizate adăugate prin TXLSXWorkbook.CustomProperties.AddDate (scrise ca vt:filetime) o împărtășeau și ele. Din v2.384.48, toate cele trei căi convertesc înainte de scriere și înapoi la citire ori de câte ori timestamp-ul poartă un Z, iar din v2.384.59 partea de citire onorează și secunde fracționare și offseturi explicite +hh:mm / -hh:mm
Conversia în sine este locul unde o reparare naivă greșește. LocalFileTimeToFileTime aplică offsetul în vigoare chiar acum, deci un timestamp din ianuarie convertit în iulie iese derapat cu o oră în orice zonă cu ora de vară. HotXLS apelează în schimb TzSpecificLocalTimeToSystemTime și SystemTimeToTzSpecificLocalTime, care aleg ora standard sau ora de vară după data convertită, iar o valoare neseta de zero trece neatinsă, ca să nu se transforme niciodată într-o dată din 1899 deplasată cu câteva ore
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');
// Pe o mașină setată pe Ora Europei Centrale, core.xml ține acum
// <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
// (UTC+2 în iulie), în timp ce ApprovedOn se scrie ca 16:00Z (UTC+1 în ianuarie)
finally
Book.Free;
end;
end;
Ce nu convertește HotXLS la citirea timestamp-urilor?
Cititorul W3CDTF din HotXLS convertește fiecare formă etichetată cu zonă a profilului din v2.384.59, iar singurul caz pe care îl lasă în pace este o oră fără zonă. Înainte de versiunea aceasta parserul lua primele 19 caractere și convertea din UTC doar când caracterul 20 era Z, deci un timestamp cu secunde fracționare (01:30:00.5Z) sau cu offset explicit (+08:00) era citit ca oră locală fără nicio ajustare și ieșea derapat cu offsetul zonei. Din HotXLS 2.384.59, Created, Modified și proprietățile personalizate cu valoare de dată parsează secunde fracționare de orice lungime, Z și offseturi +hh:mm / -hh:mm, convertesc instantul în UTC și apoi în oră locală și citesc un timestamp doar-dată precum 2026-07-01 ca data aceea. Un timestamp cu oră dar fără marcator de zonă, pe care profilul W3CDTF nu-l permite și pentru care ECMA-376 Part 2 nu dă nicio regulă, este citit în continuare ca oră locală neschimbat, iar un timestamp care nu se parsează deloc revine ca zero. Registrele de lucru care trec prin Excel sunt în regulă; pachetele produse de alți generatori care renunță la zonă merită o verificare punctuală
Fișierele scrise de build-uri HotXLS mai vechi sunt cealaltă graniță onestă. Un timestamp XLSX scris dinainte de v2.384.48 era oră locală purtând un Z, iar nimic din fișier nu-l deosebește de unul corect, deci cititorul actual îl deplasează cu offsetul zonei. Timestamp-urile FILETIME clasice din acele build-uri primesc aceeași deplasare, iar o dată de creare scrisă dinainte de v2.384.17 se citește în plus cu o zi mai târziu, pentru că ziua în plus a vechii constante nu poate fi detectată nici ea; doar codarea edit-time și plasarea la $0E au o semnătură recunoscută. Țineți minte și că valoarea API este locală mașinii care citește, deci un serviciu rulat în UTC și un desktop din Tokyo vor raporta valori CreatedDate diferite pentru același fișier, ambele corecte
Cum ar trebui să testați timestamp-urile de document?
Testați timestamp-urile de document împotriva a ceva ce propriul cod nu a scris. Ambele bug-uri de aici au trecut o verificare de salvare-urmată-de-redeschidere, pentru că o greșeală simetrică e invizibilă unui test simetric. Comparați cu un registru de lucru salvat de Excel sau afirmați octeții brute și textul XML după salvare, și rulați suita pe o mașină setată pe o zonă non-UTC cu o dată de test de fiecare parte a unei schimbări de oră de vară. Un build agent care rulează în UTC va trece fericit vechiul cod stricat
Timestamp-urile de document sunt mici, dar sunt după care sortează sistemele de evidență, indecșii de căutare și pistele de audit, iar o dată derapată cu o zi sau cu opt ore e mai rea decât una lipsă pentru că nimeni nu o pune la îndoială. Componenta de spreadsheet HotXLS pentru Delphi se ocupă de aritmetica de epocă, de ID-urile de proprietăți și de conversia UTC pentru .xls și .xlsx, astfel încât codul dumneavoastră poate asigna valori TDateTime locale obișnuite și poate lăsa formatul de fișier în grija bibliotecii