Tehnični članak

Časovni žigi Excel v Delphiju: FILETIME, UTC in DST

HotXLS hrani časovne žige lastnosti dokumentov Excel znotraj datoteke kot UTC in jih skozi API izpostavi kot krajevni čas: TXLSWorkbook.CreatedDate in LastSavedDate za .xls, TXLSXWorkbook.Created in Modified za .xlsx. Od v2.384.48 oba pogona pretvorita krajevni čas v UTC ob pisanju in nazaj ob branju, po pravilih poletnega časa, ki veljajo na datum samega žiga. Do tja sta vodila dva popravka, in obe napaki sta preživeli iz istega sramotnega razloga: vsak avtomatiziran round-trip je uspel, podokno File > Info v Excelu pa je pokazovalo napačen dan ali napačno uro. Če ste prebrali naš pregled nastavljanja lastnosti dokumentov Excel v Delphiju, je to del, kjer datumi prenehajo biti preproste vrednosti

Zakaj je preizkus shrani in ponovno odpri skril napako za en dan?

Sam round-trip je skril napako, ker sta si pisatelj in bralec delila isto napačno konstanto, zato se je napaka sama uničila. Datum nabora lastnosti OLE je FILETIME, 64-bitno štetje tikov po 100 nanosekundah od 1601-01-01 UTC ([MS-DTYP] §2.3.3), Delphi TDateTime pa šteje dneve od 1899-12-30, istega serijskega izhodišča, ki ga pokriva serijske številke datumov Excel v Delphiju in sistema 1900 vs 1904. Razlika med epochama je 109205 dni, kar lahko preverite brez koledarja: 25569 (Unix epoha kot TDateTime) plus 109205 da 134774, Unix epoha prešteta v dnevih FILETIME. Gradnje HotXLS pred v2.384.17 so uporabile 109206, tako da je bil vsak žig ustvarjanja in shranjevanja zapisan en dan prepozno in prebran en dan prezgodaj. Testna zbirka je videla vrednost, ki jo je dodelila; Excel je videl jutri

const
  // dnevi od epohe FILETIME (1601-01-01) do epohe TDateTime (1899-12-30)
  // preveri: 25569 + 109205 = 134774, Unix epoha v dnevih FILETIME
  FileTimeDayBias = 109205;

function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
  // Najprej zaokroži na cele milisekunde, šele nato skaliraj na tike po 100 ns.
  // Neposredno skaliranje Double na tike spremeni 04:00 v 03:59:59.9999
  Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
Časovnica HotXLS epohe FILETIME 1601-01-01, epohe TDateTime 1899-12-30 in Unix epohe 1970, ki prikazuje odmik 109205 dni za UtcDateTimeToFileTimeTicks in kako so gradnje pred v2.384.17 vsak žig CreatedDate zapisale en dan prepozno in prebrale en dan prezgodaj s 109206
Skica UtcDateTimeToFileTimeTicks pusti odmik tam, kjer se napačna konstanta sama uniči — simetričen preizkus shrani in ponovno odpri je videl vrednost, ki jo je dodelil, podokno Info v Excelu pa je pokazalo jutri

Komentar o zaokroževanju v tej skici je druga, manjša lekcija iz iste kode. Če ulomkovni TDateTime neposredno pomnožite s 864.000.000.000 tiki na dan, napaka dvojiške plavajoče vejice zateče v spodnje števke, žig točno 04:00 pa se je vrnil kot 03:59:59.9999. HotXLS v2.384.48 zaokroži na cele milisekunde, preden skalira, tako da vrednosti na celo uro preživijo pot nedotaknjene. Ista izdaja je dodala korak časovnega pasu, ki ga ta skica namenoma izpušča, ker je vhod tu že UTC

Kateri ID-ji lastnosti SummaryInformation nosijo datume?

V naboru lastnosti \005SummaryInformation, ki ga definira [MS-OLEPS], živi čas ustvarjanja pod ID-jem lastnosti $0C (PIDSI_CREATE_DTM), čas zadnjega shranjevanja pod $0D (PIDSI_LASTSAVE_DTM), skupni čas urejanja pa pod $0A (PIDSI_EDITTIME). Starejše gradnje HotXLS so žig zadnjega shranjevanja zapisale v $0E, kar je PIDSI_PAGECOUNT, zato Excel ni imel datuma shranjevanja za pokazati, lastnost števila strani pa je nosila časovni žig. Od v2.384.17 bralec spoštuje tudi to starejšo razporeditev: kadar $0D manjka in $0E nosi VT_FILETIME, se vrednost vzame kot čas zadnjega shranjevanja. Vsako branje PROPVARIANT se zdaj tudi sprosti s PropVariantClear, ker lahko deformirana datoteka parkira niz pod katerim koli od teh ID-jev. Če želite te tokove videti z lastnimi očmi, prikaz branja sestavljenih datotek OLE2 v Delphiju brez COM IStorage pokaže, kako do njih

PIDSI_EDITTIME je past znotraj pasti. Lastnost je tipizirana kot VT_FILETIME, nosi pa trajanje, surovo število pretečenih tikov po 100 ns brez dodane epohe. Stari pisatelj jo je obravnaval kot datum, EditTimeMinutes delil s 1440 in rezultat pognal skozi pretvorbo epohe, tako da je 125 minut urejanja pristalo v datoteki kot približno 299 let. Trenutni bralec prepozna to kodiranje po velikosti: nobeno pravo sejo urejanja ne prečka treh stoletij, zato se vsaki vrednosti 109206 dni ali več odšteje starejši odmik, preden se izpolni EditTimeMinutes

Zemljevid HotXLS nabora lastnosti 005SummaryInformation, kjer PIDSI_CREATE_DTM na $0C nosi čas ustvarjanja, PIDSI_LASTSAVE_DTM na $0D žig shranjevanja, PIDSI_EDITTIME na $0A surovo trajanje namesto datuma, $0E PIDSI_PAGECOUNT pa režo, ki so jo starejše gradnje zlorabile za časovne žige
PIDSI_EDITTIME je past znotraj pasti — tipiziran kot VT_FILETIME, nosi pa pretečene tike brez epohe, kar je 125 minut urejanja enkrat spremenilo v približno 299 let, dokler ni prišla hevristika bralca na osnovi velikosti
var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Title := 'Q3 settlement';
    // Vrednosti API so krajevni čas; datoteka hrani FILETIME v UTC
    Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
    Book.LastSavedDate := Now;     // HotXLS zapiše, kar dodelite, sam ne žigi Now
    Book.RevisionNumber := 7;
    Book.EditTimeMinutes := 125;   // trajanje, shranjeno kot surovi tiki
    if Book.SaveAs('settlement.xls') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Book.Free;
  end;
end;

Zakaj so bili datumi XLSX zgrešeni natanko za odmik časovnega pasu?

Datumi XLSX so bili zgrešeni za odmik pasu, ker sta dcterms:created in dcterms:modified v docProps/core.xml vrednosti W3CDTF z značko Z, kar pomeni UTC v modelu osnovnih lastnosti ECMA-376 Part 2, HotXLS pa je žigal krajevni čas s to Z pripeto. Delovni zvezek, ustvarjen ob 09:30 na stroju v UTC+8, je nosil 09:30:00Z, Excel na istem stroju pa ga je pretvoril v 17:30. Klasični pogon je imel identično napako v svojih vrednostih FILETIME, datumske lastnosti po meri, dodane prek TXLSXWorkbook.CustomProperties.AddDate (zapisane kot vt:filetime), pa so jo delile. Od v2.384.48 vse tri poti pretvorijo pred pisanjem in pretvorijo nazaj ob branju, kadar koli žig nosi Z, od v2.384.59 pa bralna stran spoštuje tudi ulomkovne sekunde in izrecne odmike +hh:mm / -hh:mm

Pretvorba sama je mesto, kjer naivni popravek zgreši. LocalFileTimeToFileTime uporabi odmik, ki velja takoj zdaj, zato januarski žig, pretvorjen julija, pride ven za uro zgrešen v katerem koli pasu s poletnim časom. HotXLS namesto tega pokliče TzSpecificLocalTimeToSystemTime in SystemTimeToTzSpecificLocalTime, ki standardni ali poletni čas izberejo iz datuma, ki se pretvarja, nenastavljena ničelna vrednost pa gre skozi nedotaknjena, tako da se nikoli ne spremeni v datum 1899, premaknjen za nekaj ur

Poti pretvorbe iz krajevnega v UTC v HotXLS za januarski žig 17:00 CET, shranjen julija: LocalFileTimeToFileTime uporabi današnji poletni odmik in pristane za uro zgrešeno pri 15:00Z, TzSpecificLocalTimeToSystemTime pa izbere odmik iz datuma samega žiga in zapiše pravilne 16:00Z
Odmik pasu pripada datumu samega žiga, ne trenutnim pravilom stroja — en Windows API izbere pravo stran spremembe poletnega časa, drugi pa januarske žige, pretvorjene julija, tiho prestavi za uro
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 stroju, nastavljenem na srednjeevropski čas, core.xml zdaj nosi
    // <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
    // (UTC+2 julija), ApprovedOn pa se zapiše kot 16:00Z (UTC+1 januarja)
  finally
    Book.Free;
  end;
end;

Kaj HotXLS ne pretvori pri branju časovnih žigov?

Bralnik W3CDTF v HotXLS od v2.384.59 pretvori vsako obliko profila z značko pasu, edini primer, ki ga še pusti pri miru, pa je čas brez pasu. Pred to izdajo je razčlenjevalnik vzel prvih 19 znakov in pretvoril iz UTC le, ko je bil znak 20 Z, zato je bil žig z ulomkovnimi sekundami (01:30:00.5Z) ali izrecnim odmikom (+08:00) prebran kot krajevni čas brez prilagoditve in končal zgrešen za odmik pasu. Od HotXLS 2.384.59 Created, Modified in lastnosti po meri z vrednostjo datuma razčlenijo ulomkovne sekunde poljubne dolžine, Z ter odmike +hh:mm / -hh:mm, pretvorijo trenutek v UTC in nato v krajevni čas, žig samo z datumom, kot je 2026-07-01, pa preberejo kot ta datum. Žig s časom brez značke pasu, ki ga profil W3CDTF ne dovoljuje in ECMA-376 Part 2 zanj ne podaja pravila, se še vedno prebere kot nespremenjen krajevni čas, žig, ki se sploh ne razčleni, pa se vrne kot nič. Delovni zvezki, ki gredo skozi Excel, so v redu; paketi, izdelani s strani drugih generatorjev, ki spustijo pas, pa si zaslužijo točkovni pregled

Datoteke, napisane s starejšimi gradnjami HotXLS, so druga poštena meja. Žig XLSX, zapisan pred v2.384.48, je bil krajevni čas v obleki Z, nič v datoteki ga ne loči od pravilnega, zato ga trenutni bralec prestavi za odmik pasu. Klasični žigi FILETIME teh gradenj vzamejo isti premik, datum ustvarjanja, zapisan pred v2.384.17, pa se dodatno prebere en dan prepozno, ker tudi dodatnega dne stare konstante ni mogoče zaznati; prepoznaven podpis imata le kodiranje časa urejanja in umestitev $0E. Upoštevajte tudi, da je vrednost API krajevna za stroj, ki bere, zato bosta storitev v UTC in namizje v Tokiu za isto datoteko poročali različni vrednosti CreatedDate, obe pravilni

Kako naj preizkusite časovne žige dokumentov?

Časovne žige dokumentov preizkušajte proti nečemu, kar vaša koda ni napisala. Obe napaki sta prešli preizkus shrani in ponovno odpri, ker je simetrična napaka nevidna simetričnemu preizkusu. Primerjajte z delovnim zvezkom, shranjenim v Excelu, ali trdite surove bajte in besedilo XML po shranjevanju, zbirko pa poženite na stroju, nastavljenem na pas, ki ni UTC, s preizkusnim datumom na vsaki strani spremembe poletnega časa. Build agent, ki teče v UTC, bo staro, pokvarjeno kodo veselo prestal

Časovni žigi dokumentov so majhni, vendar so tisto, po čemer razvrščajo sistemi zapisov, iskalni indeksi in revizijske sledi, datum, zgrešen za dan ali za osem ur, pa je slabši od manjkajočega, ker ga nihče ne izpodbija. Komponenta preglednic HotXLS za Delphi poskrbi za aritmetiko epoh, ID-je lastnosti in pretvorbo UTC za .xls in .xlsx, tako da lahko vaša koda dodeljuje preproste krajevne vrednosti TDateTime in format datoteke prepusti knjižnici