HotXLS съхранява timestamp-овете на документните свойства на Excel като UTC вътре във файла и ги излага като локално време през API-я: TXLSWorkbook.CreatedDate и LastSavedDate за .xls, TXLSXWorkbook.Created и Modified за .xlsx. От v2.384.48 и двата engine-а конвертират локалното време към UTC при запис и обратно при четене, ползвайки правилата за лятното време, важащи за собствената дата на stamp-а. Пътят дотам отне два fix-а и двата бъга оцеляха по една и съща притеснителна причина: всеки автоматизиран round trip минаваше, докато панелът File > Info на Excel показваше грешен ден или грешен час. Ако сте чели нашия преглед на задаването на документни свойства на Excel в Delphi, тук е мястото, където датите спират да са прости стойности
Защо тест „запиши и отвори отново“ скри грешка от един ден?
Собственият round trip скри грешката, защото writer и reader споделяха същата грешна константа, така че грешката се анулираше сама. Дата в OLE property set е FILETIME, 64-битово броене на тактове по 100 наносекунди от 1601-01-01 UTC ([MS-DTYP] §2.3.3), докато Delphi TDateTime брои дни от 1899-12-30 — същият serial произход, разгледан в Excel date serials в Delphi и системите 1900 срещу 1904. Разстоянието между двете епохи е 109205 дни, което можете да проверите и без календар: 25569 (Unix епохата като TDateTime) плюс 109205 дава 134774, Unix епохата, преброена в FILETIME дни. HotXLS build-ове преди v2.384.17 ползваха 109206, така че всеки stamp на създаване и запис се записваше с един ден късно и се четеше с един ден рано. Тест suite-ът виждаше стойността, която сам е задал; Excel виждаше утрешния ден
const
// дни от FILETIME епохата (1601-01-01) до TDateTime епохата (1899-12-30)
// проверка: 25569 + 109205 = 134774, Unix епохата в FILETIME дни
FileTimeDayBias = 109205;
function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
// Първо закръглете до цели милисекунди, чак после мащабирайте към тактове по 100 ns.
// Директното мащабиране на Double към тактове превръща 04:00 в 03:59:59.9999
Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
Коментарът за закръгляне в тази скица е вторият, по-малкият урок от същия код. Умножаването на дробен TDateTime директно по 864 000 000 000 такта на ден позволява на грешката от двоичната плаваща запетая да се промъкне в последните цифри и stamp в точно 04:00 се връщаше като 03:59:59.9999. HotXLS v2.384.48 закръгля до цели милисекунди преди мащабирането, така че стойностите на ровно часа оцеляват непокътнати. Същият release добави и стъпката за часовата зона, която тази скица нарочно пропуска, защото входът тук вече е UTC
Кои property ID-та в SummaryInformation пазят датите?
В \005SummaryInformation property set-а, дефиниран от [MS-OLEPS], времето на създаване стои под property ID $0C (PIDSI_CREATE_DTM), времето на последен запис под $0D (PIDSI_LASTSAVE_DTM), а общото време за редактиране под $0A (PIDSI_EDITTIME). По-старите HotXLS build-ове записваха stamp-а на последен запис в $0E, тоест PIDSI_PAGECOUNT, така че Excel нямаше дата на запис за показване, а имаше property за брой страници, в което стои timestamp. От v2.384.17 нататък reader-ът отчита и този legacy layout: когато $0D липсва, а $0E носи VT_FILETIME, стойността се приема за време на последен запис. Всяко прочитане на PROPVARIANT вече се освобождава с PropVariantClear, защото повреден файл може да е паркирал string под който и да е от тези ID-та. Ако искате да видите тези stream-ове със собствените си очи, разходката из четенето на OLE2 compound файлове в Delphi без COM IStorage показва как да стигнете до тях
PIDSI_EDITTIME е капанът вътре в капана. Свойството е типизирано VT_FILETIME, но пази продължителност — суров брой изминали тактове по 100 ns, без прибавена епоха. Старият writer го третираше като дата, делеше EditTimeMinutes на 1440 и прокарваше резултата през конверсията на епохата, така че 125 минути редактиране кацаха във файла като около 299 години. Текущият reader разпознава това кодиране по размер: никоя истинска сесия за редактиране не обхваща три века, така че всяка стойност от 109206 дни или повече получава приспаднато legacy отместването, преди EditTimeMinutes да се попълни
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
Book.Title := 'Q3 settlement';
// API стойностите са локално време; файлът пази UTC FILETIME-и
Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
Book.LastSavedDate := Now; // HotXLS записва това, което задавате, не поставя Now
Book.RevisionNumber := 7;
Book.EditTimeMinutes := 125; // продължителност, съхранена като сурови тактове
if Book.SaveAs('settlement.xls') <> 1 then
raise Exception.Create('Save failed');
finally
Book.Free;
end;
end;
Защо датите в XLSX грешеха с точно отместването на часовата зона?
Датите в XLSX грешеха с отместването на зоната, защото dcterms:created и dcterms:modified в docProps/core.xml са W3CDTF стойности, маркирани с Z, което под модела на core properties в ECMA-376 Part 2 значи UTC, а HotXLS доскоро поставяше локалното време с лепната отгоре Z. Работна книга, създадена в 09:30 на машина в UTC+8, носеше 09:30:00Z, а Excel на същата машина го конвертираше в 17:30. Classic engine-ът имаше същия дефект в FILETIME стойностите си, а custom date свойства, добавяни чрез TXLSXWorkbook.CustomProperties.AddDate (записвани като vt:filetime), го споделяха. От v2.384.48 и трите пътя конвертират преди запис и конвертират обратно при четене, щом stamp-ът носи Z, а от v2.384.59 страната на четене отчита и дробни секунди и изрични отмествания +hh:mm / -hh:mm
Самата конверсия е мястото, където наивният fix се проваля. LocalFileTimeToFileTime прилага отместването, което важи в момента, така че януарски stamp, конвертиран през юли, излиза с час грешка във всяка зона с лятно време. HotXLS вместо това вика TzSpecificLocalTimeToSystemTime и SystemTimeToTzSpecificLocalTime, които избират стандартно или лятно време според датата, която се конвертира, а незададена стойност нула минава недокосната, така че никога не се превръща в дата от 1899, отместена с няколко часа
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');
// На машина, настроена на Central European Time, core.xml вече пази
// <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
// (UTC+2 през юли), докато ApprovedOn се записва като 16:00Z (UTC+1 през януари)
finally
Book.Free;
end;
end;
Какво не конвертира HotXLS при четене на timestamp-ове?
W3CDTF reader-ът на HotXLS конвертира всяка зонирана форма от профила от v2.384.59 нататък, а единственият случай, който още оставя настрана, е време без зона. Преди този release parser-ът взимаше първите 19 символа и конвертираше от UTC само когато символ 20 беше Z, така че stamp с дробни секунди (01:30:00.5Z) или изрично отместване (+08:00) се четеше като локално време без корекция и излизаше с грешка от отместването на зоната. От HotXLS 2.384.59 Created, Modified и custom свойствата с дата парсват дробни секунди с произволна дължина, Z и отмествания +hh:mm / -hh:mm, конвертират момента към UTC и после към локално време, а date-only stamp като 2026-07-01 четат като тази дата. Stamp с време, но без маркер за зона — което W3CDTF профилът не позволява, а ECMA-376 Part 2 няма правило за него — все още се чете непроменен като локално време, а stamp, който изобщо не се парсва, се връща като нула. Работни книги, минали през Excel, са наред; пакети, произведени от други генератори, които изхвърлят зоната, заслужават точкова проверка
Файловете, записани от по-стари HotXLS build-ове, са другата честна граница. XLSX stamp, записан преди v2.384.48, е било локално време, облякло Z, и нищо във файла не го различава от коректен, така че текущият reader го мести с отместването на зоната. Classic FILETIME stamp-ове от тези build-ове получават същото местене, а дата на създаване, записана преди v2.384.17, допълнително се чете с един ден късно, защото излишният ден на старата константа също не може да бъде засечен; само edit-time кодирането и разположението в $0E имат разпознаваема сигнатура. Имайте предвид също, че API стойността е локална за машината, която чете, така че service в UTC и desktop в Токио ще докладват различни CreatedDate за един и същ файл, и двете коректни
Как да тествате документните timestamp-ове?
Тествайте документните timestamp-ове срещу нещо, което вашият код не е писал. И двата бъга минаха през проверка запис-и-повторно-отваряне, защото симетрична грешка е невидима за симетричен тест. Сравнявайте с работна книга, записана от Excel, или assert-вайте суровите байтове и XML текста след запис, и пускайте suite-а на машина, настроена на зона различна от UTC, с тестова дата от двете страни на смяна на лятното време. Build agent, който върви в UTC, ще прегърне стария, счупен код с удоволствие
Документните timestamp-ове са дребни, но именно по тях сортират системите за записи, search индексите и audit trail-овете, а дата, грешаща с ден или с осем часа, е по-лоша от липсваща, защото никой не я поставя под съмнение. HotXLS Delphi spreadsheet компонента поема епохната аритметика, property ID-тата и UTC конверсията и за .xls, и за .xlsx, така че вашият код може да задава обикновени локални TDateTime стойности и да остави файловия формат на библиотеката