HotXLS guarda los timestamps de propiedades de documento de Excel como UTC dentro del archivo y los expone como hora local por el API: TXLSWorkbook.CreatedDate y LastSavedDate para .xls, TXLSXWorkbook.Created y Modified para .xlsx. Desde la v2.384.48 ambos motores convierten la hora local a UTC al escribir y de vuelta al leer, usando las reglas de horario de verano que aplican en la propia fecha del stamp. Llegar ahí tomó dos fixes, y ambos bugs sobrevivieron por la misma razón vergonzosa: cada round trip automatizado pasaba, mientras el panel File > Info de Excel mostraba el día equivocado o la hora equivocada. Si ya leyó nuestra introducción a establecer propiedades de documento de Excel en Delphi, esta es la parte donde las fechas dejan de ser valores simples
¿Por qué un test de guardar y reabrir escondía un error de un día?
Un round trip consigo mismo escondía el error porque escritor y lector compartían la misma constante equivocada, así que el error se cancelaba solo. Una fecha de un property set OLE es un FILETIME, un conteo de 64 bits de ticks de 100 nanosegundos desde 1601-01-01 UTC ([MS-DTYP] §2.3.3), mientras un TDateTime de Delphi cuenta días desde 1899-12-30, el mismo origen serial que cubre los seriales de fecha de Excel en Delphi y los sistemas 1900 vs 1904. La brecha entre las dos épocas es de 109205 días, algo que puede verificar sin calendario: 25569 (la época Unix como TDateTime) más 109205 da 134774, la época Unix contada en días FILETIME. Los builds de HotXLS anteriores a la v2.384.17 usaban 109206, así que cada stamp de creación y guardado se escribía un día tarde y se leía un día temprano. El test suite veía el valor que había asignado; Excel veía mañana
const
// días desde la época FILETIME (1601-01-01) a la época TDateTime (1899-12-30)
// chequeo: 25569 + 109205 = 134774, la época Unix en días FILETIME
FileTimeDayBias = 109205;
function UtcDateTimeToFileTimeTicks(UtcStamp: TDateTime): Int64;
begin
// Redondee primero a milisegundos enteros, después escale a ticks de 100 ns.
// Escalar el Double directo a ticks convierte 04:00 en 03:59:59.9999
Result := Round((UtcStamp + FileTimeDayBias) * 86400000.0) * 10000;
end;
El comentario sobre el redondeo de ese sketch es la segunda lección, más pequeña, del mismo código. Multiplicar un TDateTime fraccionario directamente por 864.000.000.000 ticks por día deja que el error del punto flotante binario se filtre a los dígitos del fondo, y un stamp de exactamente 04:00 volvía como 03:59:59.9999. HotXLS v2.384.48 redondea a milisegundos enteros antes de escalar, así que los valores en punto sobreviven el viaje intactos. La misma release agregó el paso de zona horaria que este sketch deliberadamente deja afuera, porque la entrada aquí ya es UTC
¿Qué property IDs de SummaryInformation guardan las fechas?
En el property set \005SummaryInformation definido por [MS-OLEPS], la hora de creación vive bajo el property ID $0C (PIDSI_CREATE_DTM), la hora del último guardado bajo $0D (PIDSI_LASTSAVE_DTM), y el tiempo total de edición bajo $0A (PIDSI_EDITTIME). Los builds viejos de HotXLS escribían el stamp de último guardado en $0E, que es PIDSI_PAGECOUNT, así que Excel no tenía fecha de guardado que mostrar y una propiedad de conteo de páginas cargando un timestamp. Desde la v2.384.17 el lector también honra ese layout legado: cuando $0D está ausente y $0E carga un VT_FILETIME, el valor se toma como la hora del último guardado. Cada lectura de PROPVARIANT también se libera ahora con PropVariantClear, porque un archivo malformado puede estacionar un string bajo cualquiera de estos IDs. Si quiere ver esos streams con sus propios ojos, el walk-through sobre leer archivos compuestos OLE2 en Delphi sin COM IStorage muestra cómo llegar a ellos
PIDSI_EDITTIME es la trampa dentro de la trampa. La propiedad está tipada VT_FILETIME pero carga una duración, el número crudo de ticks de 100 ns transcurridos sin época sumada. El viejo escritor la trataba como fecha, dividiendo EditTimeMinutes por 1440 y empujando el resultado por la conversión de época, así que 125 minutos de edición aterrizaban en el archivo como unos 299 años. El lector actual reconoce esa codificación por su tamaño: ninguna sesión de edición real abarca tres siglos, así que cualquier valor de 109206 días o más tiene el offset legado restado antes de llenar EditTimeMinutes
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create;
try
Book.Title := 'Q3 settlement';
// Los valores del API son hora local; el archivo guarda FILETIMEs UTC
Book.CreatedDate := EncodeDate(2026, 1, 15) + EncodeTime(9, 30, 0, 0);
Book.LastSavedDate := Now; // HotXLS escribe lo que usted asigna, no estampa Now por su cuenta
Book.RevisionNumber := 7;
Book.EditTimeMinutes := 125; // una duración, guardada como ticks crudos
if Book.SaveAs('settlement.xls') <> 1 then
raise Exception.Create('Save failed');
finally
Book.Free;
end;
end;
¿Por qué las fechas XLSX estaban desviadas exactamente el offset de zona?
Las fechas XLSX estaban desviadas por el offset de zona porque dcterms:created y dcterms:modified en docProps/core.xml son valores W3CDTF etiquetados con Z, que significa UTC bajo el modelo de core properties de ECMA-376 Part 2, y HotXLS estampaba la hora local con esa Z puesta. Un libro creado a las 09:30 en una máquina en UTC+8 llevaba 09:30:00Z, y Excel en esa misma máquina lo convertía a 17:30. El motor clásico tenía la falla idéntica en sus valores FILETIME, y las propiedades de fecha personalizadas agregadas vía TXLSXWorkbook.CustomProperties.AddDate (escritas como vt:filetime) la compartían también. Desde la v2.384.48 los tres caminos convierten antes de escribir y convierten de vuelta al leer siempre que el stamp lleve una Z, y desde la v2.384.59 el lado de lectura también honra fracciones de segundo y offsets explícitos +hh:mm / -hh:mm
La conversión en sí es donde un fix ingenuo se equivoca. LocalFileTimeToFileTime aplica el offset vigente ahora mismo, así que un stamp de enero convertido en julio sale con una hora de desvío en cualquier zona con horario de verano. HotXLS llama TzSpecificLocalTimeToSystemTime y SystemTimeToTzSpecificLocalTime en su lugar, que eligen hora estándar o de verano según la fecha que se convierte, y un valor sin fijar de cero pasa intacto para que nunca se convierta en una fecha de 1899 corrida unas horas
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');
// En una máquina configurada en Central European Time, core.xml ahora lleva
// <dcterms:created xsi:type="dcterms:W3CDTF">2026-07-01T07:30:00Z</dcterms:created>
// (UTC+2 en julio), mientras ApprovedOn se escribe como 16:00Z (UTC+1 en enero)
finally
Book.Free;
end;
end;
¿Qué no convierte HotXLS al leer timestamps?
El lector W3CDTF de HotXLS convierte toda forma con etiqueta de zona del perfil desde la v2.384.59, y el único caso que todavía deja quieto es una hora sin zona. Antes de esa release el parser tomaba los primeros 19 caracteres y convertía desde UTC solo cuando el carácter 20 era Z, así que un stamp con fracciones de segundo (01:30:00.5Z) o un offset explícito (+08:00) se leía como hora local sin ajuste y terminaba desviado el offset de zona. Desde HotXLS 2.384.59, Created, Modified y las propiedades personalizadas con valor de fecha parsean fracciones de segundo de cualquier largo, Z, y offsets +hh:mm / -hh:mm, convierten el instante a UTC y después a hora local, y leen un stamp solo-fecha como 2026-07-01 como esa fecha. Un stamp con hora pero sin marcador de zona, que el perfil W3CDTF no permite y para el que ECMA-376 Part 2 no da regla, se sigue leyendo como hora local sin cambios, y un stamp que no parsea en absoluto vuelve como cero. Los libros que pasan por Excel están bien; los paquetes producidos por otros generadores que sueltan la zona merecen una revisión puntual
Los archivos escritos por builds viejos de HotXLS son el otro límite honesto. Un stamp XLSX escrito antes de la v2.384.48 era hora local vistiendo una Z, y nada en el archivo lo distingue de uno correcto, así que el lector actual lo corre el offset de zona. Los stamps FILETIME clásicos de esos builds toman el mismo corrimiento, y una fecha de creación escrita antes de la v2.384.17 adicionalmente se lee un día tarde, porque el día extra de la vieja constante tampoco se puede detectar; solo la codificación de edit-time y la colocación en $0E tienen una firma reconocible. Tenga presente también que el valor del API es local a la máquina que lee, así que un servicio corriendo en UTC y un escritorio en Tokio reportarán valores de CreatedDate distintos para el mismo archivo, ambos correctos
¿Cómo se deben probar los timestamps de documento?
Pruebe los timestamps de documento contra algo que su propio código no escribió. Ambos bugs pasaron un chequeo de guardar y reabrir, porque un error simétrico es invisible para un test simétrico. Compare contra un libro guardado por Excel, o afirme los bytes crudos y el texto XML después de guardar, y corra el suite en una máquina configurada en una zona que no sea UTC con una fecha de prueba a cada lado de un cambio de horario de verano. Un build agent que corre en UTC pasará felizmente el código viejo, roto
Los timestamps de documento son pequeños, pero son aquello por lo que ordenan los sistemas de registros, los índices de búsqueda y las pistas de auditoría, y una fecha desviada un día u ocho horas es peor que una faltante porque nadie la cuestiona. El componente de hojas de cálculo HotXLS para Delphi maneja la aritmética de épocas, los property IDs y la conversión UTC para .xls y .xlsx, así que su código puede asignar valores TDateTime locales corrientes y dejarle el formato de archivo a la librería