HotXLS trzyma znaczniki czasu właściwości dokumentu Excel jako UTC wewnątrz pliku i wystawia je przez API jako czas lokalny: TXLSWorkbook.CreatedDate i LastSavedDate dla .xls, TXLSXWorkbook.Created i Modified dla .xlsx. Od v2.384.48 oba silniki konwertują czas lokalny na UTC przy zapisie i z powrotem przy odczycie, używając zasad czasu letniego obowiązujących w dniu samego znacznika. Droga do tego zajęła dwie poprawki, a oba błędy przeżyły z tego samego żenującego powodu: każdy zautomatyzowany round-trip przechodził, podczas gdy panel Excela File > Info pokazywał zły dzień albo złą godzinę. Jeśli czytałeś nasz przegląd ustawiania właściwości dokumentu Excel w Delphi, to jest ten moment, w którym daty przestają być prostymi wartościami
Dlaczego test zapisz-otwórz ukrył błąd o jeden dzień?
Samodzienny round-trip ukrywał błąd, bo pisarz i czytnik dzieliły tę samą złą stałą, więc pomyłka kasowała się sama. Data w zestawie właściwości OLE to FILETIME, 64-bitowy licznik tików po 100 nanosekund od 1601-01-01 UTC ([MS-DTYP] §2.3.3), podczas gdy Delphi TDateTime liczy dni od 1899-12-30, tego samego początku serialu, który opisuje serial dat Excela w Delphi i systemy 1900 vs 1904. Odstęp między epokami to 109205 dni, co sprawdzisz bez kalendarza: 25569 (epoka Unixa jako TDateTime) plus 109205 daje 134774, epokę Unixa liczoną w dniach FILETIME. Buildy HotXLS sprzed v2.384.17 używały 109206, więc każdy znacznik utworzenia i zapisu był zapisywany dzień za późno i odczytywany dzień za wcześnie. Zestaw testów widział wartość, którą sam przypisał; Excel widział jutro
const
// dni od epoki FILETIME (1601-01-01) do epoki TDateTime (1899-12-30)
// sprawdź: 25569 + 109205 = 134774, epoka Unixa w dniach FILETIME
FileTimeDayBias = 109205;
function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
// Najpierw zaokrąglij do pełnych milisekund, potem przeskaluj do tików po 100 ns.
// Bezpośrednie skalowanie Double do tików zamienia 04:00 w 03:59:59.9999
Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
Komentarz o zaokrąglaniu w tym szkicu to druga, mniejsza lekcja z tego samego kodu. Pomnożenie ułamkowego TDateTime wprost przez 864 000 000 000 tików na dzień puszcza błąd binarnej arytmetyki zmiennoprzecinkowej do najniższych cyfr, i znacznik dokładnie 04:00 wracał jako 03:59:59.9999. HotXLS v2.384.48 zaokrągla do pełnych milisekund przed skalowaniem, więc wartości równo-na-godzinę przeżywają podróż nietknięte. To samo wydanie dodało krok strefy czasowej, który ten szkic celowo pomija, bo wejście jest tu już UTC
Które identyfikatory właściwości SummaryInformation trzymają daty?
W zestawie właściwości \005SummaryInformation zdefiniowanym przez [MS-OLEPS] czas utworzenia mieszka pod identyfikatorem właściwości $0C (PIDSI_CREATE_DTM), czas ostatniego zapisu pod $0D (PIDSI_LASTSAVE_DTM), a łączny czas edycji pod $0A (PIDSI_EDITTIME). Starsze buildy HotXLS zapisywały znacznik ostatniego zapisu do $0E, czyli PIDSI_PAGECOUNT, więc Excel nie miał daty zapisu do pokazania, za to miał właściwość liczby stron trzymającą znacznik czasu. Od v2.384.17 czytnik respektuje też tamten starszy układ: gdy $0D nie istnieje, a $0E niesie VT_FILETIME, wartość jest brana jako czas ostatniego zapisu. Każdy odczytany PROPVARIANT jest teraz też zwalniany przez PropVariantClear, bo zniekształcony plik potrafi zaparkować łańcuch pod dowolnym z tych identyfikatorów. Jeśli chcesz zobaczyć te strumienie na własne oczy, przejście o czytaniu plików złożonych OLE2 w Delphi bez COM IStorage pokazuje, jak do nich dotrzeć
PIDSI_EDITTIME to pułapka wewnątrz pułapki. Właściwość ma typ VT_FILETIME, ale trzyma czas trwania, surową liczbę upływających tików po 100 ns bez dodanej epoki. Stary pisarz traktował ją jak datę, dzieląc EditTimeMinutes przez 1440 i pchając wynik przez konwersję epok, więc 125 minut edycji lądowało w pliku jako jakieś 299 lat. Obecny czytnik rozpoznaje to kodowanie po rozmiarze: żadna prawdziwa sesja edycji nie rozciąga się na trzy stulecia, więc od każdej wartości 109206 dni lub większej odejmuje się starsze przesunięcie, zanim wypełnione zostanie EditTimeMinutes
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
Book.Title := 'Q3 settlement';
// wartości API to czas lokalny; plik trzyma UTC FILETIME
Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
Book.LastSavedDate := Now; // HotXLS zapisuje to, co przypiszesz, sam nie stempluje Now
Book.RevisionNumber := 7;
Book.EditTimeMinutes := 125; // czas trwania, zapisany jako surowe tiki
if Book.SaveAs('settlement.xls') <> 1 then
raise Exception.Create('Save failed');
finally
Book.Free;
end;
end;
Dlaczego daty XLSX myliły się dokładnie o przesunięcie strefy?
Daty XLSX myliły się o przesunięcie strefy, bo dcterms:created i dcterms:modified w docProps/core.xml to wartości W3CDTF opatrzone Z, co w modelu właściwości podstawowych ECMA-376 Part 2 znaczy UTC, a HotXLS stemplował dotąd czas lokalny z doklejoną tą Z. Skoroszyt utworzony o 09:30 na maszynie w UTC+8 niósł 09:30:00Z, a Excel na tej samej maszynie konwertował go na 17:30. Silnik klasyczny miał identyczną wadę w swoich FILETIME, a właściwości dat dodawane przez TXLSXWorkbook.CustomProperties.AddDate (zapisywane jako vt:filetime) dzieliły ją też. Od v2.384.48 wszystkie trzy ścieżki konwertują przed zapisem i konwertują z powrotem przy odczycie, kiedy tylko znacznik niesie Z, a od v2.384.59 strona odczytu respektuje też ułamkowe sekundy i jawne przesunięcia +hh:mm / -hh:mm
Sama konwersja to miejsce, gdzie naiwna poprawka idzie na manowce. LocalFileTimeToFileTime stosuje przesunięcie obowiązujące teraz, więc styczniowy znacznik konwertowany w lipcu wychodzi z błędem godziny w każdej strefie z czasem letnim. HotXLS woła zamiast tego TzSpecificLocalTimeToSystemTime i SystemTimeToTzSpecificLocalTime, które wybierają czas standardowy albo letni na podstawie konwertowanej daty, a nieustawiona wartość zero przechodzi nietknięta, więc nigdy nie zamienia się w datę z 1899 przesuniętą o kilka godzin
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');
// Na maszynie ustawionej na czas środkowoeuropejski core.xml trzyma teraz
// <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
// (UTC+2 w lipcu), a ApprovedOn zapisuje się jako 16:00Z (UTC+1 w styczniu)
finally
Book.Free;
end;
end;
Czego HotXLS nie konwertuje przy czytaniu znaczników czasu?
Czytnik W3CDTF w HotXLS konwertuje każdą formę profilu z oznaczeniem strefy od v2.384.59, a jednym przypadkiem, który nadal zostawia w spokoju, jest czas bez strefy. Przed tym wydaniem parser brał pierwsze 19 znaków i konwertował z UTC tylko wtedy, gdy dwudziestym znakiem było Z, więc znacznik z ułamkowymi sekundami (01:30:00.5Z) albo jawnym przesunięciem (+08:00) był czytany jako czas lokalny bez korekty i wychodził z błędem przesunięcia strefy. Od HotXLS 2.384.59 Created, Modified i właściwości niestandardowe z wartościami dat parsują ułamkowe sekundy dowolnej długości, Z oraz przesunięcia +hh:mm / -hh:mm, konwertują chwilę na UTC, potem na czas lokalny, a znacznik tylko z datą, jak 2026-07-01, czytają jako tę datę. Znacznik z czasem, ale bez oznaczenia strefy, którego profil W3CDTF nie dopuszcza, a ECMA-376 Part 2 nie daje na niego reguły, jest nadal czytany jako niezmieniony czas lokalny, a znacznik, który w ogóle się nie parsuje, wraca jako zero. Skoroszyty, które przechodziły przez Excela, są w porządku; pakiety od innych generatorów wyrzucających strefę zasługują na kontrolkę
Pliki zapisane przez starsze buildy HotXLS to druga uczciwa granica. Znacznik XLSX zapisany przed v2.384.48 był czasem lokalnym w przebraniu Z, a nic w pliku nie odróżnia go od poprawnego, więc obecny czytnik przesuwa go o przesunięcie strefy. Klasyczne znaczniki FILETIME z tamtych buildów biorą to samo przesunięcie, a data utworzenia zapisana przed v2.384.17 czyta się dodatkowo dzień za późno, bo dodatkowego dnia starej stałej też nie da się wykryć; tylko kodowanie czasu edycji i umieszczenie w $0E mają rozpoznawalny podpis. Miej też na uwadze, że wartość API jest lokalna dla maszyny, która czyta, więc serwis biegnący w UTC i biurko w Tokio zgłoszą różne wartości CreatedDate dla tego samego pliku, obie poprawne
Jak testować znaczniki czasu dokumentów?
Testuj znaczniki czasu dokumentów przeciwko czemuś, czego twój własny kod nie zapisał. Oba te błędy przeszły kontrolę zapisz-potem-otwórz, bo symetryczna pomyłka jest niewidzialna dla symetrycznego testu. Porównuj ze skoroszytem zapisanym przez Excela albo asercjonuj surowe bajty i tekst XML po zapisie, a zestaw testów odpalaj na maszynie ustawionej na strefę inną niż UTC, z datą testową po obu stronach zmiany czasu letniego. Agent buildowy biegnący w UTC szczęśliwie przepuści stary, zepsuty kod
Znaczniki czasu dokumentów są drobiazgiem, ale to po nich sortują systemy rejestrów, indeksy wyszukiwania i ślady audytu, a data myląca się o dzień albo o osiem godzin jest gorsza od brakującej, bo nikt jej nie zakwestionuje. Arkuszowy komponent HotXLS dla Delphi ogarnia arytmetykę epok, identyfikatory właściwości i konwersję UTC dla .xls i .xlsx, więc twój kod może przypisywać zwykłe lokalne wartości TDateTime i zostawić format pliku bibliotece