Műszaki cikk

Excel dokumentum-időbélyegek Delphiben: FILETIME, UTC és DST

A HotXLS az Excel dokumentumtulajdonságok időbélyegeit UTC-ben tárolja a fájlon belül, és helyi időként teszi elérhetővé az API-n keresztül: TXLSWorkbook.CreatedDate és LastSavedDate .xls-nél, TXLSXWorkbook.Created és Modified .xlsx-nél. A v2.384.48 óta mindkét motor íráskor helyi időről UTC-re, olvasáskor vissza konvertál, az időbélyeg saját dátumára érvényes nyári időszámítási szabályokkal. Idáig két javításon át vezetett az út, és mindkét hiba ugyanazért a kínos okért maradt fenn: minden automatizált round trip átment, miközben az Excel File > Info panelje rossz napot vagy rossz órát mutatott. Ha már olvasta az Excel dokumentumtulajdonságok beállításáról Delphiben szóló összefoglalónkat, ez az a rész, ahol a dátumok abbahagyják az egyszerű érték létét

Miért rejtette el a mentés-újranyitás teszt az egy napos hibát?

Az önmagára visszacsatolt round trip azért rejtette el a hibát, mert az író és az olvasó ugyanazt a rossz konstanst használta, így a hiba önmagát kivonta. Egy OLE tulajdonsághalmaz dátuma FILETIME, azaz 64 bites számolás 100 nanoszekundumos tickben 1601-01-01 UTC óta ([MS-DTYP] §2.3.3), a Delphi TDateTime-ja viszont napokat számol 1899-12-30 óta — ugyanaz a sorozatkiindulás, amelyet az Excel dátumsorozatairól Delphiben, az 1900 és az 1904-es rendszerről szóló cikk tárgyal. A két epocha közti rés 109205 nap, amit naptár nélkül is ellenőrizhet: 25569 (a Unix epocha TDateTime-ként) plusz 109205 adja az 134774-et, a Unix epochát FILETIME napokban számolva. A v2.384.17 előtti HotXLS build-ek 109206-ot használtak, így minden létrehozási és mentési bélyeg egy nappal később íródott és egy nappal korábban olvasódott vissza. A tesztcsomag azt az értéket látta, amelyet hozzárendelt; az Excel a holnapot látta

const
  // nap a FILETIME epochától (1601-01-01) a TDateTime epocháig (1899-12-30)
  // ellenőrzés: 25569 + 109205 = 134774, a Unix epocha FILETIME napokban
  FileTimeDayBias = 109205;

function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
  // Előbb kerekítsen egész milliszekundumra, majd skálázzon 100 ns tickre.
  // A Double közvetlen tickre skálázása a 04:00-ból 03:59:59.9999-et csinál
  Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
HotXLS idővonal a FILETIME epochával (1601-01-01), a TDateTime epochával (1899-12-30) és az 1970-es Unix epochával, megmutatva az UtcDateTimeToFileTimeTicks mögötti 109205 napos eltölést, valamint azt, hogy a v2.384.17 előtti build-ek minden CreatedDate bélyeget egy nappal később írtak és 109206-tal egy nappal korábban olvastak
Az UtcDateTimeToFileTimeTicks vázlat az eltölést ott tartja, ahol egy rossz konstans önmagát kivonja — a szimmetrikus mentés-újranyitás teszt a hozzárendelt értéket látta, miközben az Excel Info panelje a holnapot mutatta

Az említett vázlat kerekítési megjegyzése ugyanennek a kódnak a második, kisebb leckéje. Egy tört részes TDateTime közvetlen napi 864 000 000 000 tickkel való szorzása átengedi a bináris lebegőpontos hibát az alsó számjegyekbe, és egy pontosan 04:00 órás bélyeg 03:59:59.9999-ként jött vissza. A HotXLS v2.384.48-ja skálázás előtt egész milliszekundumra kerekít, így a pontos órára eső értékek sértetlenül vészelik át az utat. Ugyanez a kiadás tette hozzá azt az időzóna-lépést, amelyet ez a vázlat szándékosan kihagy, mert a bemenet itt már UTC

Melyik SummaryInformation property azonosító tárolja a dátumokat?

A [MS-OLEPS] által definiált \005SummaryInformation tulajdonsághalmazban a létrehozás ideje a $0C property azonosító alatt lakik (PIDSI_CREATE_DTM), az utolsó mentés ideje a $0D alatt (PIDSI_LASTSAVE_DTM), a teljes szerkesztési idő pedig a $0A alatt (PIDSI_EDITTIME). A régebbi HotXLS build-ek az utolsó mentés bélyegét a $0E-be írták, ami a PIDSI_PAGECOUNT, így az Excelnek nem volt mentési dátuma, amit mutathatott volna, viszont volt egy időbélyeget hordozó oldalszám-tulajdonsága. A v2.384.17 óta az olvasó ezt az örökölt elrendezést is tiszteletben tartja: ha a $0D hiányzik, és a $0E VT_FILETIME-ot hordoz, az érték az utolsó mentés idejeként érkezik. Minden beolvasott PROPVARIANT ma már PropVariantClear-rel szabadul fel, mert egy sérült fájl bármelyik ilyen azonosító alá parkolhat karakterláncot. Ha a saját szemével szeretné látni ezeket a streameket, a OLE2 összetett fájlok COM IStorage nélküli olvasásáról Delphiben szóló áttekintés megmutatja az utat

A PIDSI_EDITTIME a csapda a csapdában. A tulajdonság típusa VT_FILETIME, de időtartamot hordoz: a lejárt 100 ns tick nyers számát, epocha hozzáadása nélkül. A régi író dátumként bánt vele, a EditTimeMinutes-t 1440-rel osztotta, és az eredményt az epocha-konverzión tolta át, így 125 perc szerkesztés nagyjából 299 évként landolt a fájlban. A mostani olvasó mérete alapján ismeri fel ezt a kódolást: egyetlen valódi szerkesztési munkamenet sem ível át három évszázadon, ezért minden 109206 napos vagy nagyobb értékből az örökölt eltolást levonják, mielőtt a EditTimeMinutes kitöltődne

HotXLS térkép a 005SummaryInformation tulajdonsághalmazról, ahol a $0C-nél élő PIDSI_CREATE_DTM a létrehozás idejét, a $0D-nél élő PIDSI_LASTSAVE_DTM a mentési bélyeget, a $0A-nál élő PIDSI_EDITTIME nyers időtartamot hordoz dátum helyett, a $0E PIDSI_PAGECOUNT pedig az a rés, amelyet a régebbi build-ek időbélyegre használtak félre
A PIDSI_EDITTIME a csapda a csapdában — VT_FILETIME-ként típusozott, mégis lejárt tick-eket hordoz epocha nélkül, ami 125 szerkesztési percből nagyjából 299 évet faragott, míg meg nem érkezett a méretalapú olvasói heurisztika
var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Title := 'Q3 settlement';
    // az API értékei helyi időt jelentenek; a fájl UTC FILETIME-okat tárol
    Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
    Book.LastSavedDate := Now;     // a HotXLS azt írja, amit Ön rendel hozzá, magától nem Now-t bélyegz
    Book.RevisionNumber := 7;
    Book.EditTimeMinutes := 125;   // időtartam, nyers tickként tárolva
    if Book.SaveAs('settlement.xls') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Book.Free;
  end;
end;

Miért tértek el az XLSX dátumai pontosan az időzóna-eltolással?

Az XLSX dátumai azért tértek el a zónaeltolással, mert a docProps/core.xml dcterms:created és dcterms:modified mezői Z-vel jelölt W3CDTF értékek, ami UTC-t jelent az ECMA-376 Part 2 core properties modellje alatt, a HotXLS pedig helyi időt bélyegzett, arra ragasztott Z-vel. Egy UTC+8-as gépen 09:30-kor készült munkafüzet 09:30:00Z-t hordozott, és ugyanazon gép Excelje 17:30-ra konvertálta. A klasszikus motornak ugyanez a hiba ott lapult a FILETIME értékeiben, és a TXLSXWorkbook.CustomProperties.AddDate útján hozzáadott egyéni dátumtulajdonságok (vt:filetime-ként írva) is osztoztak rajta. A v2.384.48 óta mindhárom út konvertál írás előtt, és olvasáskor vissza, valahányszor a bélyeg Z-t hordoz, a v2.384.59 óta pedig az olvasó oldal a tört másodperceket és az explicit +hh:mm / -hh:mm eltolásokat is kezeli

Magánál a konverziónál bukik el a naiv javítás. A LocalFileTimeToFileTime a mostanában érvényes eltolást alkalmazza, így egy júliusban konvertált januári bélyeg bármelyik nyári időszámítást alkalmazó zónában egy órával téved. A HotXLS helyette TzSpecificLocalTimeToSystemTime-ot és SystemTimeToTzSpecificLocalTime-ot hív, amelyek a konvertálandó dátumból választanak standard vagy nyári időt, a nulla értékű unset pedig érintetlenül megy át, hogy sose legyen belőle néhány órával eltolódott 1899-es dátum

HotXLS helyi-UTC konverziós utak egy júliusban mentett, januári 17:00 CET bélyeghez: a LocalFileTimeToFileTime a mai nyári eltolást alkalmazza, és egy órával tévedve 15:00Z-nél landol, a TzSpecificLocalTimeToSystemTime viszont a bélyeg saját dátumából választja az eltolást, és a helyes 16:00Z-t írja
A zónaeltolás a bélyeg saját dátumához tartozik, nem a gép aktuális szabályaihoz — az egyik Windows API a nyári időszámítási váltás helyes oldalát választja, a másik csendben egy órával tolja el a júliusban konvertált januári bélyegeket
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');
    // Közép-európai időre állított gépen a core.xml ma már ezt tartja:
    // <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
    // (júliusban UTC+2), az ApprovedOn pedig 16:00Z-ként íródik (januárban UTC+1)
  finally
    Book.Free;
  end;
end;

Mit nem konvertál a HotXLS az időbélyegek olvasásakor?

A HotXLS W3CDTF olvasója a v2.384.59 óta a profil minden zónajelölt formáját konvertálja, és az egyetlen eset, amelyet még mindig békén hagy, a zóna nélküli idő. Az előző kiadásig a parser az első 19 karaktert vette, és csak akkor konvertált UTC-ből, ha a 20. karakter Z volt, így egy tört másodperces (01:30:00.5Z) vagy explicit eltolású (+08:00) bélyeg helyi időként, korrekció nélkül olvasódott, és a zónaeltolással tévedt el. A HotXLS 2.384.59 óta a Created, a Modified és a dátumértékű egyéni tulajdonságok bármilyen hosszú tört másodpercet, Z-t, valamint +hh:mm / -hh:mm eltolásokat dolgoznak fel, a pillanatot UTC-re, majd helyi időre konvertálják, és egy csak dátumot hordozó, 2026-07-01 jellegű bélyeget annak a dátumnak olvasnak. Az időt de zónajelölést nem hordozó bélyeg, amelyet a W3CDTF profil tilt, és amelyre az ECMA-376 Part 2 sem ad szabályt, továbbra is változatlan helyi időként olvasódik, a teljesen elemezhetetlen bélyeg pedig nullaként tér vissza. Az Excelen átmenő munkafüzetek rendben vannak; a zónát elhagyó, más generátoroktól származó csomagok megérdemlik a helyszíni ellenőrzést

A régebbi HotXLS build-ek fájljai a másik becsületes határvonal. A v2.384.48 előtt írt XLSX bélyeg Z-t viselő helyi idő volt, és semmi a fájlban nem különbözteti meg a helyesitől, ezért a mostani olvasó a zónaeltolással tolja el. Azoknak a build-eknek a klasszikus FILETIME bélyegei ugyanezt az eltolást kapják, és a v2.384.17 előtt írt létrehozási dátum ráadásul egy nappal későbben olvasódik vissza, mert a régi konstans fölös napja sem észlelhető; csak az edit-time kódolásnak és a $0E elhelyezésnek van felismerhető szignatúrája. Tartsa észben azt is, hogy az API értéke az olvasást végző gép szerinti helyi idő, így egy UTC-ben futó szolgáltatás és egy tokiói asztali gép ugyanarra a fájlra eltérő CreatedDate értéket jelent, mindkettőt helyesen

Hogyan tesztelje a dokumentum-időbélyegeket?

Dokumentum-időbélyegeket ahhoz képest teszteljen, amit a saját kódja nem írt. Mindkét hiba átment a mentés-után-újranyitás ellenőrzésen, mert a szimmetrikus hiba láthatatlan a szimmetrikus teszt előtt. Hasonlítson Excel által mentett munkafüzethez, vagy állítsa ellen a nyers bájtokat és az XML szövegét mentés után, és futtassa a csomagot nem UTC zónára állított gépen, nyári időszámítási váltás mindkét oldalára eső tesztdátummal. Egy UTC-ben futó build ágens boldogan engedi át a régi, törött kódot

A dokumentum-időbélyegek kicsik, de azok, amelyek alapján a rekordrendszerek, a keresési indexek és az audit nyomvonalak rendeznek, és egy nappal vagy nyolc órával tévedő dátum rosszabb a hiányzónál, mert senki sem kérdőjelezi meg. A HotXLS Delphi táblázatkezelő komponens kezeli az epocha-számtant, a property azonosítókat és az UTC-konverziót .xls-re és .xlsx-re egyaránt, így az Ön kódja csupán egyszerű helyi TDateTime értékeket rendel hozzá, a fájlformátumot pedig a könyvtárra hagyhatja