Tekninen artikkeli

Excelin aikaleimat Delphissä: FILETIME, UTC ja DST

HotXLS tallentaa Excelin asiakirjapropertyjen aikaleimat UTC:na tiedoston sisälle ja näyttää ne paikallisena aikana API:n kautta: TXLSWorkbook.CreatedDate ja LastSavedDate arvoille .xls, TXLSXWorkbook.Created ja Modified arvoille .xlsx. Versiosta v2.384.48 alkaen molemmat koneet konvertoivat paikallisen ajan UTC:ksi kirjoitettaessa ja takaisin luettaessa soveltaen kesäajan sääntöjä jotka pätevät leiman omalle päivämäärälle. Sinne pääseminen vaati kaksi korjausta, ja molemmat bugit olivat selvinneet samasta nolostuttavasta syystä: jokainen automatisoitu round trip meni läpi, kun taas Excelin File > Info -ruutu näytti väärän päivän tai väärän tunnin. Jos olet lukenut yleiskatsauksemme artikkelista Excel-asiakirjapropertyjen asettamisesta Delphissä, tämä on se kohta jossa päivämäärät lakkavat olemasta yksinkertaisia arvoja

Miksi tallenna ja avaa -testi kätti päivän mittaisen virheen?

Itseensä palautuva round trip kätki virheen, koska kirjoittaja ja lukija jakoivat saman väärän vakion, joten virhe kumosi itsensä. OLE-property-setin päivämäärä on FILETIME, 64-bittinen lukumäärä 100 nanosekunnin tikkejä hetkestä 1601-01-01 UTC ([MS-DTYP] §2.3.3), kun taas Delphin TDateTime laskee päivää hetkestä 1899-12-30, samasta sarja-alkuperästä jota käsittelee artikkeli Excelin päivämääräsarjat Delphissä ja 1900- ja 1904-järjestelmät. Kahden epookin väli on 109205 päivää, minkä voi tarkistaa ilman kalenteria: 25569 (Unix-epookki TDateTimena) plus 109205 antaa 134774, Unix-epookki FILETIME-päivinä laskettuna. HotXLS-buildit ennen v2.384.17:ää käyttivät lukua 109206, joten jokainen luonti- ja tallennusleima kirjoitettiin päivän myöhässä ja luettiin päivän aikaisin. Testisarja näki arvon jonka se oli asettanut; Excel näki huomisen

const
  // päivää FILETIME-epookista (1601-01-01) TDateTime-epookkiin (1899-12-30)
  // tarkistus: 25569 + 109205 = 134774, Unix-epookki FILETIME-päivinä
  FileTimeDayBias = 109205;

function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
  // Pyöristä ensin kokonaisiin millisekunteihin, skaalaa sitten 100 ns tikkeihin.
  // Doublen suora skaalaus tikkeiksi kääntää 04:00 arvoksi 03:59:59.9999
  Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
HotXLS:n aikajana FILETIME-epookista 1601-01-01, TDateTime-epookista 1899-12-30 ja Unix-epookista 1970 näyttäen 109205 päivän siirtymän UtcDateTimeToFileTimeTicksin takana ja miten v2.384.17:a vanhemmat buildit kirjoittivat jokaisen CreatedDate-leiman päivän myöhässä ja lukivat sen päivän aikaisin arvolla 109206
UtcDateTimeToFileTimeTicks-luonnos pitää siirtymän siellä missä väärä vakio kumoo itsensä — symmetrinen tallenna ja avaa -testi näki antamansa arvon kun taas Excelin Info-ruutu näytti huomista

Kyseisen luonnoksen pyöristyskommentti on toinen, pienempi oppi samasta koodista. Murtolukuisen TDateTimein kertominen suoraan 864 000 000 000:lla tikkeinä päivää kohden päästää binäärisen liukuluvun virheen valumaan alimpään numeroihin, ja täsmälleen 04:00:n leima palasi muodossa 03:59:59.9999. HotXLS v2.384.48 pyöristää kokonaisiin millisekunteihin ennen skaalausta, joten tasatuntien arvot selviävät matkasta koskemattomina. Sama julkaisu lisäsi aikavyöhykeaskeleen jonka tämä luonnos tarkoituksella jättää pois, koska syöte on tässä jo UTC:ta

Mitkä SummaryInformationin property-ID:t kantavat päivämääriä?

[MS-OLEPS]:n määrittelemässä \005SummaryInformation-property-setissä luontiaika asuu property-ID:n $0C (PIDSI_CREATE_DTM) alla, viimeisin tallennusaika $0D:n (PIDSI_LASTSAVE_DTM) alla ja kokonaismuokkausaika $0A:n (PIDSI_EDITTIME) alla. Vanhemmat HotXLS-buildit kirjoittivat tallennusleiman arvoon $0E, joka on PIDSI_PAGECOUNT, joten Excelillä ei ollut tallennuspäivää näytettäväksi ja sivumääräpropertysessä oli aikaleima. Versiosta v2.384.17 alkaen lukija kunnioittaa myös sitä vanhaa asettelua: kun $0D puuttuu ja $0E kantaa VT_FILETIMEa, arvo otetaan viimeisimmäksi tallennusajaksi. Jokainen luettu PROPVARIANT vapautetaan nyt myös PropVariantClearilla, koska viallinen tiedosto voi pysäköidä merkkijonon minkä tahansa näiden ID:n alle. Jos haluat nähdä nuo streamit omin silmin, artikkeli OLE2-yhdistetiedostojen lukemisesta Delphillä ilman COM IStoragea näyttää miten niihin pääsee käsiksi

PIDSI_EDITTIME on ansa ansan sisällä. Property on tyyppiä VT_FILETIME mutta kantaa kestoa, raakaa määrää kuluneita 100 ns:n tikkejä ilman epookkia. Vanha kirjoittaja kohteli sitä päivämääränä, jakamalla EditTimeMinutesin 1440:llä ja työntämällä tuloksen epookkikonversion läpi, joten 125 muokkausminuuttia laskeutui tiedostoon noin 299 vuotena. Nykyinen lukija tunnistaa tuon enkoodauksen koostaan: mikään oikea muokkausistunto ei ulotu kolmelle vuosisadalle, joten mistä tahansa arvosta 109206 päivää tai enemmän vähennetään vanha siirtymä ennen kuin EditTimeMinutes täytetään

HotXLS:n kartta 005SummaryInformation-property-setistä jossa PIDSI_CREATE_DTM arvossa $0C kantaa luontiaikaa, PIDSI_LASTSAVE_DTM arvossa $0D tallennusleimaa, PIDSI_EDITTIME arvossa $0A raakaa kestoa päivämäärän sijaan, ja $0E PIDSI_PAGECOUNT se paikka jota vanhemmat buildit väärinkäyttivät aikaleimoina
PIDSI_EDITTIME on ansa ansan sisällä — tyyppiä VT_FILETIME mutta kantaa kuluneita tikkejä ilman epookkia, mikä käänsi kerran 125 muokkausminuuttia noin 299 vuodeksi kunnes kokoon perustuva lukijan heuristiikka saapui
var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Title := 'Q3 settlement';
    // API-arvot ovat paikallista aikaa; tiedosto tallentaa UTC-FILETIMEt
    Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
    Book.LastSavedDate := Now;     // HotXLS kirjoittaa antamasi arvon, se ei itse leimaa hetkeä
    Book.RevisionNumber := 7;
    Book.EditTimeMinutes := 125;   // kesto, tallennettuna raakatikkeinä
    if Book.SaveAs('settlement.xls') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Book.Free;
  end;
end;

Miksi XLSX-päivämäärät olivat pielessä täsmälleen aikavyöhykesiirtymän verran?

XLSX-päivämäärät olivat pielessä vyöhykesiirtymän verran, koska dcterms:created ja dcterms:modified tiedostossa docProps/core.xml ovat W3CDTF-arvoja joita leimaa Z, mikä tarkoittaa UTC:ta ECMA-376 Part 2:n core properties -mallissa, ja HotXLS leimasi paikallista aikaa kyseinen Z perässä. UTC+8-koneella 09:30:n aikaan luotu työkirja kantoi arvoa 09:30:00Z, ja saman koneen Excel konvertoi sen muotoon 17:30. Klassisella koneella oli identtinen vika FILETIME-arvoissaan, ja TXLSXWorkbook.CustomProperties.AddDatein kautta lisätyt mukautetut päivämääräpropertiset (kirjoitetaan muodossa vt:filetime) jakivat sen myös. Versiosta v2.384.48 alkaen kaikki kolme polkua konvertoivat ennen kirjoitusta ja takaisin luettaessa aina kun leima kantaa Ztä, ja versiosta v2.384.59 alkaen lukupuoli kunnioittaa myös murtosekunteja ja eksplisiittisiä +hh:mm- ja -hh:mm-siirtymiä

Konversio sinänsä on paikka jossa naivi korjaus menee pieleen. LocalFileTimeToFileTime soveltaa juuri nyt voimassa olevaa siirtymää, joten heinäkuussa konvertoitu tammikuun leima tulee ulos tunnin pielessä millä tahansa kesäajan vyöhykkeellä. HotXLS kutsuu sen sijaan funktioita TzSpecificLocalTimeToSystemTime ja SystemTimeToTzSpecificLocalTime, jotka poimivat normaali- tai kesäajan konvertoitavasta päivämäärästä, ja asettamaton nolla-arvo kulkee läpi koskemattomana joten se ei koskaan muutu muutaman tunnin siirtymäksi 1899 päivämäärään

HotXLS:n paikallisen ajan ja UTC:n konversiopolut tammikuun 17:00 CET -leimalle joka tallennettiin heinäkuussa: LocalFileTimeToFileTime soveltaa tämänpäiväistä kesäaikasiirtymää ja laskeutuu tunnin pieleen arvoon 15:00Z, kun taas TzSpecificLocalTimeToSystemTime poimii siirtymän leiman omasta päivämäärästä ja kirjoittaa oikean 16:00Z:n
Vyöhykesiirtymä kuuluu leiman omalle päivämäärälle, ei koneen tämänhetkisille säännöille — toinen Windows API poimii oikean puolen kesäajan vaihtumisesta, toinen siirtää heinäkuussa konvertoitua tammikuun leimaa hiljaisena tunnin verran
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');
    // Keskieurooppalaiseen aikaan asetetulla koneella core.xml sisältää nyt
    // <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
    // (UTC+2 heinäkuussa), kun taas ApprovedOn kirjoitetaan muotoon 16:00Z (UTC+1 tammikuussa)
  finally
    Book.Free;
  end;
end;

Mitä HotXLS ei konvertoi lukiessaan aikaleimoja?

HotXLS:n W3CDTF-lukija konvertoi jokaisen profiilin vyöhykeleimatun muodon versiosta v2.384.59 alkaen, ja yksi tapaus johon se yhä ei koske on aika ilman vyöhykettä. Ennen kyseistä julkaisua jäsentin otti ensimmäiset 19 merkkiä ja konvertoi UTC:sta vain kun merkki 20 oli Z, joten leima jossa on murtosekunteja (01:30:00.5Z) tai eksplisiittinen siirtymä (+08:00) luettiin paikallisena aikana ilman säätöä ja päätyi vyöhykesiirtymän verran pieleen. HotXLS 2.384.59:stä alkaen Created, Modified ja päivämääräarvoiset mukautetut propertiset jäsentävät minkä pituiset murtosekunnit tahansa, Zn sekä +hh:mm- ja -hh:mm-siirtymät, konvertoivat hetken UTC:ksi ja sitten paikalliseksi ajaksi, ja lukevat vain päivämäärän leiman kuten 2026-07-01 kyseisenä päivänä. Leima jossa on aika mutta ei vyöhykemerkkiä, jota W3CDTF-profiili ei salli ja jolle ECMA-376 Part 2 ei anna sääntöä, luetaan yhä paikallisena aikana muuttumattomana, ja leima joka ei jäsenny lainkaan palautuu nollana. Excelin kautta kulkevat työkirjat ovat kunnossa; muiden generaattorien tuottamat paketit jotka pudottavat vyöhykkeen ansaitsevat satunnaistarkistuksen

Vanhempien HotXLS-buildien kirjoittamat tiedostot ovat toinen rehellinen raja. Ennen v2.384.48:aa kirjoitettu XLSX-leima oli paikallista aikaa joka käytti Ztä, eikä tiedostossa ole mitään joka erottaisi sen oikeasta, joten nykyinen lukija siirtää sen vyöhykesiirtymän verran. Kyseisten buildien klassiset FILETIME-leimat saavat saman siirtymän, ja ennen v2.384.17:ää kirjoitettu luontipäivämäärä lukee lisäksi yhden päivän myöhässä, koska vanhan vakion ylimääräistä päivää ei myöskään voi havaita; vain edit-time-enkoodauksella ja $0E:n sijoittelulla on tunnistettava signature. Pidä mielessä myös että API-arvo on paikallinen sille koneelle joka lukee, joten UTC:ssa pyörivä palvelu ja työpöytä Tokiossa raportoivat eri CreatedDate-arvot samasta tiedostosta, molemmat oikein

Miten asiakirjojen aikaleimoja kannattaa testata?

Testaa asiakirjojen aikaleimoja jotain vasten jota oma koodisi ei kirjoittanut. Molemmat näistä bugeista pääsivät tallenna ja avaa -tarkistuksen läpi, koska symmetrinen virhe on näkymätön symmetriselle testille. Vertaa Excelillä tallennettuun työkirjaan, tai assertoi raakatavut ja XML-teksti tallennuksen jälkeen, ja aja testisarja koneella joka on asetettu ei-UTC-vyöhykkeelle testipäivämäärällä kummallakin puolella kesäajan vaihtumista. UTC:ssa pyörivä build-agentti läpäisee vanhan, rikkinäisen koodin mielellään

Asiakirjojen aikaleimat ovat pieniä, mutta ne ovat se minkä mukaan rekisterijärjestelmät, hakuhakemistot ja auditointijäljet lajittelevat, ja päivä joka on pielessä päivän tai kahdeksan tunnin verran on pahempi kuin puuttuva koska kukaan ei kyseenalaista sitä. HotXLS Delphi Excel -komponentti hoitaa epookkilaskut, property-ID:t ja UTC-konversion sekä .xlsille että .xlsxille, joten koodisi voi asettaa pelkkiä paikallisia TDateTime-arvoja ja jättää tiedostomuodon kirjastolle