Artikel Teknis

Timestamp Dokumen Excel di Delphi: FILETIME, UTC, dan DST

HotXLS menyimpan timestamp property dokumen Excel sebagai UTC di dalam berkas dan mengeksposnya sebagai waktu lokal lewat API: TXLSWorkbook.CreatedDate dan LastSavedDate untuk .xls, TXLSXWorkbook.Created dan Modified untuk .xlsx. Sejak v2.384.48 kedua engine mengonversi waktu lokal ke UTC saat menulis dan kembali saat membaca, memakai aturan daylight saving yang berlaku pada tanggal stamp itu sendiri. Sampai ke sana butuh dua fix, dan kedua bug bertahan dengan alasan yang sama-sama memalukan: semua round trip otomatis lolos, sementara pane File > Info milik Excel memperlihatkan hari atau jam yang salah. Kalau Anda sudah membaca gambaran umum kami tentang menyetel property dokumen Excel di Delphi, inilah bagian di mana tanggal berhenti jadi nilai sederhana

Mengapa tes save-lalu-reopen menyembunyikan kesalahan sehari?

Round trip ke dirinya sendiri menyembunyikan kesalahan karena writer dan reader berbagi konstanta salah yang sama, jadi kesalahannya membatalkan dirinya sendiri. Tanggal property set OLE adalah FILETIME, hitungan 64 bit tick 100 nanodetik sejak 1601-01-01 UTC ([MS-DTYP] §2.3.3), sedangkan TDateTime Delphi menghitung hari sejak 1899-12-30, asal serial yang sama yang dibahas di serial tanggal Excel di Delphi dan sistem 1900 vs 1904. Jarak antara kedua epoch adalah 109205 hari, dan Anda bisa memeriksanya tanpa kalender: 25569 (epoch Unix sebagai TDateTime) ditambah 109205 menghasilkan 134774, epoch Unix dihitung dalam hari FILETIME. Build HotXLS sebelum v2.384.17 memakai 109206, jadi semua stamp pembuatan dan penyimpanan ditulis sehari terlambat dan dibaca sehari terlalu cepat. Test suite melihat nilai yang ia tetapkan sendiri; Excel melihat hari esok

const
  // hari dari epoch FILETIME (1601-01-01) ke epoch TDateTime (1899-12-30)
  // cek: 25569 + 109205 = 134774, epoch Unix dalam hari FILETIME
  FileTimeDayBias = 109205;

function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
  // Bulatkan ke milidetik utuh dulu, baru skalakan ke tick 100 ns.
  // Menskalakan Double langsung ke tick mengubah 04:00 jadi 03:59:59.9999
  Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
Garis waktu HotXLS atas epoch FILETIME 1601-01-01, epoch TDateTime 1899-12-30, dan epoch Unix 1970, memperlihatkan bias 109205 hari di balik UtcDateTimeToFileTimeTicks dan bagaimana build sebelum v2.384.17 menulis tiap stamp CreatedDate sehari terlambat dan membacanya sehari terlalu cepat dengan 109206
Sketsa UtcDateTimeToFileTimeTicks menaruh bias di tempat konstanta salah membatalkan dirinya sendiri — tes save-lalu-reopen yang simetris melihat nilai yang ia tetapkan sementara pane Info Excel memperlihatkan hari esok

Komentar rounding di sketsa itu adalah pelajaran kedua yang lebih kecil dari kode yang sama. Mengalikan TDateTime pecahan langsung dengan 864.000.000.000 tick per hari membiarkan error floating-point biner merembes ke digit-digit bawah, dan stamp tepat 04:00 kembali sebagai 03:59:59.9999. HotXLS v2.384.48 membulatkan ke milidetik utuh sebelum menskalakan, jadi nilai pas di tepi jam selamat menempuh perjalanan tanpa cacat. Release yang sama menambahkan langkah zona waktu yang sengaja ditinggalkan sketsa ini, karena input di sini sudah UTC

Property ID SummaryInformation mana yang menyimpan tanggalnya?

Di property set \005SummaryInformation yang didefinisikan [MS-OLEPS], waktu pembuatan ada di property ID $0C (PIDSI_CREATE_DTM), waktu terakhir disimpan di $0D (PIDSI_LASTSAVE_DTM), dan total waktu penyuntingan di $0A (PIDSI_EDITTIME). Build HotXLS yang lebih lama menulis stamp terakhir-disimpan ke $0E, yang adalah PIDSI_PAGECOUNT, jadi Excel tidak punya tanggal save untuk ditampilkan dan sebuah property page count malah berisi timestamp. Sejak v2.384.17 reader juga menghormati layout legacy itu: saat $0D absen dan $0E membawa VT_FILETIME, nilainya diambil sebagai waktu terakhir disimpan. Setiap PROPVARIANT yang dibaca kini juga dilepas dengan PropVariantClear, karena berkas malformed bisa menitipkan string di salah satu ID ini. Kalau Anda ingin melihat stream-stream itu dengan mata kepala sendiri, walk-through tentang membaca berkas compound OLE2 di Delphi tanpa COM IStorage menunjukkan cara mencapainya

PIDSI_EDITTIME adalah jebakan di dalam jebakan. Property ini bertipe VT_FILETIME tapi menampung durasi, angka mentah tick 100 ns yang berlalu tanpa epoch ditambahkan. Writer lama memperlakukannya seperti tanggal, membagi EditTimeMinutes dengan 1440 dan mendorong hasilnya lewat konversi epoch, sehingga 125 menit penyuntingan mendarat di berkas sebagai kira-kira 299 tahun. Reader saat ini mengenali encoding itu dari ukurannya: tidak ada sesi penyuntingan sungguhan yang membentang tiga abad, jadi nilai 109206 hari atau lebih terlebih dahu offset legacy-nya sebelum EditTimeMinutes diisi

Peta HotXLS atas property set 005SummaryInformation di mana PIDSI_CREATE_DTM di $0C menampung waktu pembuatan, PIDSI_LASTSAVE_DTM di $0D stamp save, PIDSI_EDITTIME di $0A durasi mentah alih-alih tanggal, dan $0E PIDSI_PAGECOUNT slot yang disalahgunakan build lama untuk timestamp
PIDSI_EDITTIME adalah jebakan di dalam jebakan — bertipe VT_FILETIME tapi menampung tick yang berlalu tanpa epoch, yang pernah mengubah 125 menit penyuntingan menjadi kira-kira 299 tahun sampai heuristik reader berbasis ukuran datang
var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Title := 'Q3 settlement';
    // nilai API adalah waktu lokal; berkas menyimpan FILETIME UTC
    Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
    Book.LastSavedDate := Now;     // HotXLS menulis yang Anda assign, ia tidak meng-stamp Now
    Book.RevisionNumber := 7;
    Book.EditTimeMinutes := 125;   // sebuah durasi, tersimpan sebagai tick mentah
    if Book.SaveAs('settlement.xls') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Book.Free;
  end;
end;

Mengapa tanggal XLSX meleset tepat sebesar offset zona waktu?

Tanggal XLSX meleset sebesar offset zona karena dcterms:created dan dcterms:modified di docProps/core.xml adalah nilai W3CDTF yang ditandai Z, yang berarti UTC di bawah model core properties ECMA-376 Part 2, dan HotXLS dulu meng-stamp waktu lokal dengan Z itu menempel. Workbook yang dibuat pukul 09:30 di mesin UTC+8 membawa 09:30:00Z, dan Excel di mesin yang sama mengonversinya jadi 17:30. Engine klasik punya cacat identik di nilai FILETIME-nya, dan property tanggal kustom yang ditambah lewat TXLSXWorkbook.CustomProperties.AddDate (ditulis sebagai vt:filetime) ikut mewarisinya. Sejak v2.384.48 ketiga jalur mengonversi sebelum menulis dan mengonversi kembali saat membaca kapan pun stamp membawa Z, dan sejak v2.384.59 sisi baca juga menghormati detik pecahan serta offset eksplisit +hh:mm / -hh:mm

Di konversinya sendirilah fix yang naif tersesat. LocalFileTimeToFileTime menerapkan offset yang berlaku saat ini, jadi stamp Januari yang dikonversi di bulan Juli keluar meleset satu jam di zona mana pun yang punya daylight saving. HotXLS memanggil TzSpecificLocalTimeToSystemTime dan SystemTimeToTzSpecificLocalTime sebagai gantinya, yang memilih waktu standar atau daylight dari tanggal yang dikonversi, dan nilai nol yang tak diset lewat tanpa disentuh sehingga tidak pernah berubah jadi tanggal 1899 yang bergeser beberapa jam

Jalur konversi lokal ke UTC HotXLS untuk stamp 17:00 CET bulan Januari yang disimpan di bulan Juli: LocalFileTimeToFileTime menerapkan offset daylight hari ini dan mendarat meleset satu jam di 15:00Z, sementara TzSpecificLocalTimeToSystemTime memilih offset dari tanggal stamp itu sendiri dan menulis 16:00Z yang benar
Offset zona milik tanggal stamp itu sendiri, bukan milik aturan terkini mesin — satu Windows API memilih sisi yang benar dari perubahan daylight saving, yang lain diam-diam menggeser stamp Januari yang dikonversi di bulan Juli sejam
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');
    // Di mesin yang diset ke Central European Time, core.xml kini memuat
    // <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
    // (UTC+2 di bulan Juli), sementara ApprovedOn ditulis sebagai 16:00Z (UTC+1 di bulan Januari)
  finally
    Book.Free;
  end;
end;

Apa yang tidak dikonversi HotXLS saat membaca timestamp?

Reader W3CDTF HotXLS mengonversi setiap bentuk bertanda zona dari profil sejak v2.384.59, dan satu kasus yang tetap dibiarkan adalah waktu tanpa zona. Sebelum release itu, parser mengambil 19 karakter pertama dan hanya mengonversi dari UTC saat karakter ke-20 adalah Z, jadi stamp dengan detik pecahan (01:30:00.5Z) atau offset eksplisit (+08:00) dibaca sebagai waktu lokal tanpa penyesuaian dan berakhir meleset sebesar offset zona. Sejak HotXLS 2.384.59, Created, Modified, dan property kustom bernilai tanggal meng-parse detik pecahan sepanjang apa pun, Z, serta offset +hh:mm / -hh:mm, mengonversi instannya ke UTC lalu ke waktu lokal, dan membaca stamp hanya-tanggal seperti 2026-07-01 sebagai tanggal itu. Stamp dengan waktu tapi tanpa penanda zona, yang tidak diizinkan profil W3CDTF dan tidak diberi aturan oleh ECMA-376 Part 2, tetap dibaca sebagai waktu lokal tanpa diubah, dan stamp yang sama sekali tak ter-parse kembali sebagai nol. Workbook yang lewat tangan Excel aman; paket buatan generator lain yang menjatuhkan zonanya pantas dapat spot check

Berkas yang ditulis build HotXLS lama adalah batas jujur yang lain. Stamp XLSX yang ditulis sebelum v2.384.48 adalah waktu lokal yang memakai jas Z, dan tidak ada apa pun di berkas yang membedakannya dari yang benar, jadi reader saat ini menggesernya sebesar offset zona. Stamp FILETIME klasik dari build-build itu kena geseran yang sama, dan tanggal pembuatan yang ditulis sebelum v2.384.17 tambahan dibaca kembali sehari terlambat, karena hari ekstra milik konstanta lama juga tak terdeteksi; hanya encoding edit-time dan penempatan $0E yang punya tanda tangan yang bisa dikenali. Ingat juga bahwa nilai API itu lokal bagi mesin yang membaca, jadi service yang berjalan di UTC dan desktop di Tokyo akan melaporkan nilai CreatedDate berbeda untuk berkas yang sama, keduanya benar

Bagaimana seharusnya Anda menguji timestamp dokumen?

Uji timestamp dokumen terhadap sesuatu yang bukan ditulis kode Anda sendiri. Kedua bug ini lolos pemeriksaan save-lalu-reopen, karena kesalahan simetris tak terlihat oleh tes yang simetris. Bandingkan dengan workbook yang disimpan Excel, atau assert byte mentah dan teks XML setelah menyimpan, dan jalankan suite di mesin yang diset ke zona non-UTC dengan tanggal uji di kedua sisi perubahan daylight saving. Build agent yang berjalan di UTC akan senang hati meloloskan kode lama yang rusak

Timestamp dokumen kecil, tapi dialah yang dipakai sistem record, search index, dan audit trail untuk mengurutkan, dan tanggal yang meleset sehari atau delapan jam lebih buruk daripada yang hilang karena tak ada yang mencurigainya. Komponen spreadsheet Delphi HotXLS menangani hitungan epoch, property ID, dan konversi UTC untuk .xls maupun .xlsx, jadi kode Anda cukup meng-assign nilai TDateTime lokal biasa dan menyerahkan format berkas ke library