Tehnički članak

Excel timestampovi u Delphi-ju: FILETIME, UTC i DST

HotXLS vremenske oznake svojstava Excel dokumenta čuva kao UTC unutar fajla i izlaže ih kao lokalno vreme kroz API: TXLSWorkbook.CreatedDate i LastSavedDate za .xls, TXLSXWorkbook.Created i Modified za .xlsx. Od v2.384.48 oba engine-a pretvaraju lokalno vreme u UTC pri upisu i nazad pri čitanju, koristeći pravila za letnje računanje vremena koja važe na datum same oznake. Do tog stanja vodile su dve popravke, a oba buga preživela su iz istog sramnog razloga: svaki automatizovani round trip prolazio je, dok je Excelov File > Info panel pokazivao pogrešan dan ili pogrešan sat. Ako ste čitali naš pregled o podešavanju svojstava Excel dokumenta u Delphi-ju, ovde je deo gde datumi prestaju da budu jednostavne vrednosti

Zašto je test čuvanja i ponovnog otvaranja sakrio grešku od jednog dana?

Sopstveni round trip sakrio je grešku jer su pisac i čitač delili istu pogrešnu konstantu, pa se greška oduzela sama sebi. Datum u OLE property set-u je FILETIME, 64-bitno brojanje tikova od 100 nanosekundi od 1601-01-01 UTC ([MS-DTYP] §2.3.3), dok Delphi TDateTime broji dane od 1899-12-30, istog serijskog početka o kome piše Excel date serials u Delphi-ju i sistemi 1900 naspram 1904. Razmak između te dve epohe je 109205 dana, što možete proveriti i bez kalendara: 25569 (Unix epoha kao TDateTime) plus 109205 daje 134774, Unix epoha izbrojana u FILETIME danima. HotXLS buildovi pre v2.384.17 koristili su 109206, pa je svaka oznaka kreiranja i čuvanja upisivana dan kasnije a čitana dan ranije. Test suite video je vrednost koju je dodelio; Excel je video sutra

const
  // dani od FILETIME epohe (1601-01-01) do TDateTime epohe (1899-12-30)
  // provera: 25569 + 109205 = 134774, Unix epoha u FILETIME danima
  FileTimeDayBias = 109205;

function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
  // Prvo zaokružite na cele milisekunde, pa tek onda skalirajte na tikove od 100 ns.
  // Direktno skaliranje Double-a na tikove pretvara 04:00 u 03:59:59.9999
  Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
HotXLS vremenska linija FILETIME epohe 1601-01-01, TDateTime epohe 1899-12-30 i Unix epohe 1970, sa prikazom 109205-dnevnog biasa iza UtcDateTimeToFileTimeTicks i kako su buildovi pre v2.384.17 upisivali svaku CreatedDate oznaku dan kasnije a čitali je dan ranije sa 109206
Skica UtcDateTimeToFileTimeTicks drži bias tamo gde se pogrešna konstanta sama oduzima — simetričan test čuvanja i ponovnog otvaranja video je vrednost koju je dodelio dok je Excel Info panel pokazivao sutra

Komentar o zaokruživanju u toj skici je druga, manja lekcija iz istog koda. Množenje razlomljenog TDateTime-a direktno sa 864.000.000.000 tikova po danu pušta grešku binarnog floating point-a u donje cifre, i oznaka od tačno 04:00 vraćala se kao 03:59:59.9999. HotXLS v2.384.48 zaokružuje na cele milisekunde pre skaliranja, pa vrednosti na pun sat prežive put nepromenjene. Isto izdanje dodalo je i korak vremenske zone koji ova skica namerno izostavlja, jer je ulaz ovde već UTC

Koji SummaryInformation property ID-jevi nose datume?

U \005SummaryInformation property set-u definisanom u [MS-OLEPS], vreme kreiranja živi pod property ID-jem $0C (PIDSI_CREATE_DTM), vreme poslednjeg čuvanja pod $0D (PIDSI_LASTSAVE_DTM), a ukupno vreme uređivanja pod $0A (PIDSI_EDITTIME). Stariji HotXLS buildovi upisivali su oznaku poslednjeg čuvanja u $0E, koje je PIDSI_PAGECOUNT, pa Excel nije imao datum čuvanja da prikaže nego property za broj strana koji je nosio vremensku oznaku. Od v2.384.17 čitač poštuje i taj legacy raspored: kada $0D nedostaje a $0E nosi VT_FILETIME, vrednost se uzima kao vreme poslednjeg čuvanja. Svako čitanje PROPVARIANT-a sada se i oslobađa sa PropVariantClear, jer pokvaren fajl može da parkira string pod bilo koji od ovih ID-jeva. Ako želite da vidite te streamove sopstvenim očima, walk-through o čitanju OLE2 compound fajlova u Delphi-ju bez COM IStorage pokazuje kako da dođete do njih

PIDSI_EDITTIME je zamka unutar zamke. Property je tipiziran kao VT_FILETIME ali nosi trajanje, sirovi broj proteklih tikova od 100 ns bez dodate epohe. Stari pisac tretirao ga je kao datum, delio EditTimeMinutes sa 1440 i gurao rezultat kroz konverziju epohe, pa je 125 minuta uređivanja u fajl sletelo kao otprilike 299 godina. Trenutni čitač prepoznaje to enkodovanje po veličini: nijedna prava sesija uređivanja ne traje tri stoleća, pa se svakoj vrednosti od 109206 dana ili više oduzima legacy offset pre nego što se EditTimeMinutes popuni

HotXLS mapa 005SummaryInformation property set-a gde PIDSI_CREATE_DTM na $0C nosi vreme kreiranja, PIDSI_LASTSAVE_DTM na $0D oznaku čuvanja, PIDSI_EDITTIME na $0A sirovo trajanje umesto datuma, a $0E PIDSI_PAGECOUNT slot koji su stariji buildovi zloupotrebljavali za vremenske oznake
PIDSI_EDITTIME je zamka unutar zamke — tipiziran kao VT_FILETIME a nosi protekle tikove bez epohe, što je 125 minuta uređivanja jednom pretvorilo u otprilike 299 godina dok nije stigla heuristika čitača zasnovana na veličini
var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Title := 'Q3 settlement';
    // API vrednosti su lokalno vreme; fajl čuva UTC FILETIME-ove
    Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
    Book.LastSavedDate := Now;     // HotXLS upisuje ono što dodelite, sam ne pečati Now
    Book.RevisionNumber := 7;
    Book.EditTimeMinutes := 125;   // trajanje, čuvano kao sirovi tikovi
    if Book.SaveAs('settlement.xls') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Book.Free;
  end;
end;

Zašto su XLSX datumi bili pogrešni tačno za offset vremenske zone?

XLSX datumi bili su pogrešni za offset zone jer su dcterms:created i dcterms:modified u docProps/core.xml W3CDTF vrednosti označene sa Z, što pod modelom core properties iz ECMA-376 Part 2 znači UTC, a HotXLS je pečatio lokalno vreme sa tom Z nalepljenom. Radna sveska kreirana u 09:30 na mašini u UTC+8 nosila je 09:30:00Z, i Excel na istoj toj mašini pretvorio ju je u 17:30. Klasični engine imao je identičnu manu u svojim FILETIME vrednostima, i prilagođeni date property-jevi dodati kroz TXLSXWorkbook.CustomProperties.AddDate (upisivani kao vt:filetime) delili su je. Od v2.384.48 sve tri putanje pretvaraju pre upisa i pretvaraju nazad pri čitanju kad god oznaka nosi Z, a od v2.384.59 strana čitanja poštuje i razlomljene sekunde i eksplicitne +hh:mm / -hh:mm offsete

Sama konverzija je mesto gde naivna popravka promaši. LocalFileTimeToFileTime primenjuje offset koji važi upravo sada, pa januarska oznaka pretvorena u julu ispadne sat pogrešna u svakoj zoni sa letnjim vremenom. HotXLS umesto toga zove TzSpecificLocalTimeToSystemTime i SystemTimeToTzSpecificLocalTime, koje standardno ili letnje vreme biraju po datumu koji se pretvara, a neraspoređena nula vrednost prolazi nedirnuta pa nikada ne postane datum iz 1899. pomerjen za par sati

HotXLS putanje konverzije lokalnog vremena u UTC za januarsku oznaku 17:00 CET sačuvanu u julu: LocalFileTimeToFileTime primenjuje današnji letnji offset i sleta jedan sat pogrešno na 15:00Z, dok TzSpecificLocalTimeToSystemTime bira offset prema datumu same oznake i upisuje ispravno 16:00Z
Offset zone pripada datumu same oznake, a ne trenutnim pravilima mašine — jedan Windows API bira pravu stranu promene letnjeg vremena, drugi tiho pomera januarske oznake pretvorene u julu za sat
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 mašini podešenoj na Central European Time, core.xml sada nosi
    // <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
    // (UTC+2 u julu), dok se ApprovedOn upisuje kao 16:00Z (UTC+1 u januaru)
  finally
    Book.Free;
  end;
end;

Šta HotXLS ne pretvara pri čitanju vremenskih oznaka?

HotXLS W3CDTF čitač pretvara svaki oblik iz profila označen zonom od v2.384.59, i jedini slučaj koji i dalje ostavlja na miru je vreme bez zone. Pre tog izdanja parser je uzimao prvih 19 znakova i iz UTC-a pretvarao tek kada je 20. znak bio Z, pa je oznaka sa razlomljenim sekundama (01:30:00.5Z) ili eksplicitnim offsetom (+08:00) čitana kao lokalno vreme bez podešavanja i završavala pogrešna za offset zone. Od HotXLS 2.384.59, Created, Modified i prilagođeni property-jevi sa datumom parsiraju razlomljene sekunde bilo koje dužine, Z i +hh:mm / -hh:mm offsete, trenutak pretvaraju u UTC pa u lokalno vreme, a oznaku samo sa datumom poput 2026-07-01 čitaju kao taj datum. Oznaka sa vremenom a bez oznake zone, koju W3CDTF profil ne dozvoljava a ECMA-376 Part 2 ne daje nikakvo pravilo za nju, i dalje se čita kao lokalno vreme nepromenjena, a oznaka koja se uopšte ne parsira vraća se kao nula. Radne sveske koje su prošle kroz Excel su u redu; pakete koje su proizveli drugi generatori a koji izbace zonu vredi nasumično proveriti

Fajlovi starijih HotXLS buildova su druga poštena granica. XLSX oznaka upisana pre v2.384.48 bila je lokalno vreme u Z kostimu, i ništa u fajlu je ne razlikuje od ispravne, pa je trenutni čitač pomera za offset zone. Klasične FILETIME oznake tih buildova trpe isto pomeranje, a datum kreiranja upisan pre v2.384.17 dodatno se čita dan kasnije, jer se ni višak dana stare konstante ne može otkriti; samo edit-time enkodovanje i $0E pozicija imaju prepoznatljiv potpis. Imajte na umu i da je API vrednost lokalna za mašinu koja čita, pa će servis koji radi u UTC i desktop u Tokiju prijaviti različite CreatedDate vrednosti za isti fajl, obe ispravne

Kako biste trebali testirati vremenske oznake dokumenta?

Vremenske oznake dokumenta testirajte protiv nečega što vaš kod nije napisao. Oba ova buga prošla su proveru čuvanja i ponovnog otvaranja, jer je simetrična greška nevidljiva simetričnom testu. Poredite sa radnom sveskom koju je sačuvao Excel, ili asertujte sirove bajtove i XML tekst posle čuvanja, i pokrenite suite na mašini podešenoj na ne-UTC zonu, sa testnim datumom sa svake strane promene letnjeg vremena. Build agent koji radi u UTC srećno će proći stari, pokvaren kod

Vremenske oznake dokumenta su sitnica, ali po njima sortiraju sistemi za evidencije, search indeksi i audit tragovi, i datum pogrešan za dan ili za osam sati gori je od onog kojeg nema, jer ga niko ne preispituje. HotXLS Delphi spreadsheet komponenta radi aritmetiku epoha, property ID-jeve i UTC konverziju za i .xls i .xlsx, pa vaš kod može dodeljivati obične lokalne TDateTime vrednosti i prepustiti fajl format biblioteci