HotXLS Excel dokumentų savybių laiko žymas faile saugo kaip UTC, o per API atiduoda jas kaip vietinį laiką: TXLSWorkbook.CreatedDate ir LastSavedDate .xls formatui, TXLSXWorkbook.Created ir Modified .xlsx formatui. Nuo v2.384.48 abu varikliai rašydami vietinį laiką paverčia UTC, o skaitodami — atgal, taikydami vasaros laiko taisykles, galiojančias pačios žymos datai. Iki ten nuėjus prireikė dviejų pataisymų, ir abu bugai išgyveno dėl tos pačios gėdingos priežasties: kiekvienas automatizuotas round trip pereidavo, o Excel File > Info panelė rodė neteisingą dieną ar neteisingą valandą. Jei skaitėte mūsų apžvalgą kaip nustatyti Excel dokumento savybes Delphi programoje, tai ta vieta, kur datos nustoja būti paprastomis reikšmėmis
Kodėl išsaugojimo ir atvėrimo testas paslėpdavo vienos dienos klaidą?
Savas round trip klaidą slėpė todėl, kad rašytojas ir skaitytojas dalijosi ta pačia neteisinga konstanta, tad klaida panaikindavo pati save. OLE savybių rinkinio data yra FILETIME — 64 bitų 100 nanosekundžių tiktų skaičius nuo 1601-01-01 UTC ([MS-DTYP] §2.3.3), o Delphi TDateTime skaičiuoja dienas nuo 1899-12-30, tos pačios serijinės kilmės, aprašytos Excel datų serijose Delphi ir 1900 bei 1904 sistemose. Tarp dviejų epochų yra 109205 dienos, ką galite patikrinti be kalendoriaus: 25569 (Unix epocha kaip TDateTime) plus 109205 duoda 134774, Unix epochą, suskaičiuotą FILETIME dienomis. HotXLS versijos iki v2.384.17 naudojo 109206, todėl kiekviena sukūrimo ir įrašymo žyma būdavo parašoma diena vėliau ir skaitoma diena anksčiau. Testų rinkinys matė pačią jam priskirtą reikšmę; Excel matydavo rytojų
const
// dienos nuo FILETIME epochos (1601-01-01) iki TDateTime epochos (1899-12-30)
// patikra: 25569 + 109205 = 134774, Unix epocha FILETIME dienomis
FileTimeDayBias = 109205;
function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
// Pirmiausia apvalykite iki sveikų milisekundžių, tada perverskite į 100 ns tiktus.
// Double perversus tiesiai į tiktus 04:00 virsta 03:59:59.9999
Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
Apvalinimo komentaras tame eskize — antra, mažesnė pamoka iš to paties kodo. Trupmeninį TDateTime padauginus tiesiai iš 864 000 000 000 tiktų per dieną, dvejetainės slankiojo kablelio klaida susprogsdavo į žemiausius skaitmenis, ir tiksliai 04:00 žyma sugrįzdavo kaip 03:59:59.9999. HotXLS v2.384.48 prieš masteliokeitimą apvalina iki sveikų milisekundžių, tad tikslaus laiko reikšmės išgyvena kelionę nesugadintos. Tas pats leidimas pridėjo laiko zonos žingsnį, kurį šis eskizas sąmoningai palieka nuošaly, nes čia įvestis jau yra UTC
Kurie SummaryInformation property ID saugo datas?
\005SummaryInformation savybių rinkinyje, apibrėžtame [MS-OLEPS], sukūrimo laikas gyvena po property ID $0C (PIDSI_CREATE_DTM), paskutinio įrašymo laikas — po $0D (PIDSI_LASTSAVE_DTM), o bendras redagavimo laikas — po $0A (PIDSI_EDITTIME). Senesnės HotXLS versijos paskutinio įrašymo žymą rašydavo į $0E, tai yra PIDSI_PAGECOUNT, tad Excel neturėdavo ką parodyti kaip įrašymo datą, o puslapių skaičiaus savybė laikydavo laiko žymą. Nuo v2.384.17 skaitytojas gerbia ir tą paveldėtą išdėstymą: kai $0D nėra, o $0E neša VT_FILETIME, reikšmė paimama kaip paskutinio įrašymo laikas. Kiekvienas perskaitytas PROPVARIANT dabar ir atlaisvinamas su PropVariantClear, nes sugadintas failas po bet kurio iš tų ID gali padėti string reikšmę. Jei norite tuos srautus pamatyti savo akimis, aprašyme kaip skaityti OLE2 compound failus Delphi programoje be COM IStorage parodoma, kaip prie jų prieiti
PIDSI_EDITTIME yra spąstai spąstuose. Savybė tipuota VT_FILETIME, bet laiko trukmę — žalią praėjusių 100 ns tiktų skaičių be jokios pridėtos epochos. Senas rašytojas ją elgdavo kaip datą: EditTimeMinutes padalydavo iš 1440 ir rezultatą praleisdavo per epochos konversiją, tad 125 redagavimo minutės faile atsidurdavo kaip apie 299 metai. Dabartinis skaitytojas tą koduotę atpažįsta pagal dydį: jokia tikra redagavimo sesija nenutęsia trijų amžių, tad bet kokia 109206 dienų ar daugiau reikšmė prieš užpildant EditTimeMinutes atima paveldėtąjį poslinkį
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
Book.Title := 'Q3 settlement';
// API reikšmės yra vietinis laikas; faile saugomi UTC FILETIME
Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
Book.LastSavedDate := Now; // HotXLS rašo tai, ką priskiriate, pats Now neužregistruoja
Book.RevisionNumber := 7;
Book.EditTimeMinutes := 125; // trukmė, saugoma kaip žali tiktai
if Book.SaveAs('settlement.xls') <> 1 then
raise Exception.Create('Save failed');
finally
Book.Free;
end;
end;
Kodėl XLSX datos klydo lygiai per laiko zonos poslinkį?
XLSX datos klydo per zonos poslinkį, nes dcterms:created ir dcterms:modified docProps/core.xml yra W3CDTF reikšmės, pažymėtos Z, kas pagal ECMA-376 Part 2 core properties modelį reiškia UTC, o HotXLS anksčiau žymėdavo vietinį laiką su ta Z prie galo. Darbaknygė, sukurta 09:30 mašinoje UTC+8 zonoje, nešdavo 09:30:00Z, ir Excel toje pačioje mašinoje ją paversdavo 17:30. Klasikinis variklis turėjo identišką ydą savo FILETIME reikšmėse, ir savos datos savybės, pridėtos per TXLSXWorkbook.CustomProperties.AddDate (rašomas kaip vt:filetime), dalijosi ja taip pat. Nuo v2.384.48 visi trys keliai rašydami konvertuoja, o skaitodami konvertuoja atgal, kai tik žyma neša Z, o nuo v2.384.59 skaitanti pusė taip pat gerbia trupmenines sekundes ir aiškius +hh:mm / -hh:mm poslinkius
Pati konversija — ten, kur naivus pataisymas klysta. LocalFileTimeToFileTime taiko poslinkį, galiojantį dabar, tad sausio žyma, konvertuota liepą, išeina per valandą tikslu bet kurioje zonoje su vasaros laiku. HotXLS vietoj to kviečia TzSpecificLocalTimeToSystemTime ir SystemTimeToTzSpecificLocalTime, kurios standartinį ar vasaros laiką renkasi pagal konvertuojamą datą, o nenustatyta nulinė reikšmė praeina neliečiamas, kad niekada nevirstų 1899 metų data, pasislinkusia per keletą valandų
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');
// Mašinoje, nustatytoje į Central European Time, core.xml dabar laiko
// <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
// (UTC+2 liepą), o ApprovedOn rašomas kaip 16:00Z (UTC+1 sausį)
finally
Book.Free;
end;
end;
Ką HotXLS nekonvertuoja skaitant laiko žymas?
HotXLS W3CDTF skaitytojas nuo v2.384.59 konvertuoja kiekvieną profilio formą su zonos žyma, ir vienintelis atvejis, kurį vis dar palieka ramybėje, — laikas be zonos. Iki to leidimo parseris imdavo pirmuosius 19 simbolių ir iš UTC konvertuodavo tik tada, kai dvidešimtas simbolis buvo Z, tad žyma su trupmeninėmis sekundėmis (01:30:00.5Z) ar aiškiu poslinkiu (+08:00) būdavo skaitoma kaip vietinis laikas be jokio pataikymo ir galiausiai klydavo per zonos poslinkį. Nuo HotXLS 2.384.59 Created, Modified ir datos reikšmės custom savybės išskaido bet kokio ilgio trupmenines sekundes, Z bei +hh:mm / -hh:mm poslinkius, akimirksnį paverčia UTC ir paskui vietiniu laiku, o tik datą turinčią žymą, tokią kaip 2026-07-01, skaito kaip tą datą. Žyma su laiku, bet be zonos ženklo, kurios W3CDTF profilis neleidžia ir už kurią ECMA-376 Part 2 neduoda jokios taisyklės, vis dar skaitoma kaip nepakeistas vietinis laikas, o žyma, kuri visai neišsiskaido, sugrįžta kaip nulis. Per Excel praėję darbaknygės yra geros; kitų generatorių pagaminti paketai, išmetantys zoną, nusipelno vietinės patikros
Senesnių HotXLS versijų parašyti failai — kita sąžininga riba. XLSX žyma, parašyta iki v2.384.48, buvo vietinis laikas, apsirengęs Z, ir niekas faile jos neatskiria nuo teisingos, tad dabartinis skaitytojas paslenka ją per zonos poslinkį. Klasikinės FILETIME žymos iš tų versijų gauna tą patį poslinkį, o sukūrimo data, parašyta iki v2.384.17, papildomai skaitoma diena vėliau, nes senosios konstantos papildoma diena taip pat neaptinkama; tik edit-time koduotė ir $0E vietos pasirinkimas turi atpažįstamą parašą. Turėkite omenyje ir tai, kad API reikšmė yra vietinė skaitančiai mašinai, tad servisas, veikiantis UTC zonoje, ir stalinius kompiuteris Tokijo zonoje tam pačiam failui parodys skirtingas CreatedDate reikšmes, abi teisėtas
Kaip turėtumėte testuoti dokumentų laiko žymas?
Testuokite dokumentų laiko žymas prieš kažką, ko jūsų paties kodas nerašė. Abu šie bugai praėjo išsaugojimo ir atvėrimo patikrą, nes simetriška klaida simetriškam testui nematoma. Palyginkite su Excel išsaugota darbaknyge, arba tikrinkite žalius baitus ir XML tekstą po išsaugojimo, ir paleiskite rinkinį mašinoje, nustatytoje ne UTC zonoje, su testavimo data kiekvienoje vasaros laiko perėjimo pusėje. UTC zonoje veikiantis build agentas seną, sulaužytą kodą mielai praleis
Dokumentų laiko žymos yra smulkmena, bet būtent pagal jas rikiuojasi įrašų sistemos, paieškos indeksai ir audito pėdsakai, o data, klydstanti per dieną ar per aštuonias valandas, yra blogesnė už nesamą, nes niekas dėl jos nesukelia abejonės. HotXLS Delphi skaičiuoklės komponentas epochų aritmetiką, property ID ir UTC konversiją atlieka tiek .xls, tiek .xlsx formatams, tad jūsų kodas gali priskirti paprastas vietines TDateTime reikšmes ir failo formatą palikti bibliotekai