Abra una hoja de cálculo, haga clic en una celda que muestra 2026-06-19 y la barra de fórmulas seguirá mostrando una fecha. Lea la misma celda desde Delphi y obtendrá el número 46192. Ambas perspectivas son correctas, porque Excel nunca almacenó una fecha en esa celda. Almacenó un número serial (un recuento de días) y le asignó un formato de número que indica a la pantalla renderizar el recuento como una fecha de calendario. No existe un tipo de fecha en el valor de la celda. Hay un número y una regla de visualización, y esta última es lo único que distingue una fecha de una cantidad simple.
Esa separación es el origen de cada error de fecha que una biblioteca de hojas de cálculo debe esquivar. Un número serial por sí solo no indica qué día es, porque no dice cuál fue el día cero. El mismo número representa dos fechas con cuatro años de diferencia dependiendo de una sola bandera del libro de trabajo. Y un número que debería leerse de vuelta como una fecha se leerá como una cantidad simple a menos que algo inspeccione su formato y reconozca un patrón de fecha. Así es como está construido el modelo de fechas en HotXLS y por qué debe ser así.
Una celda de fecha es un número más un formato
Excel almacena una fecha como el número de días transcurridos desde una época inicial, con la hora del día en la parte fraccionaria. El mediodía en un serial lleva .5. La parte entera es el recuento de días. Nada en el valor almacenado lo identifica como temporal. Lo que lo identifica es el formato de número de la celda: la norma ECMA-376 llama a esto numFmt, y una celda cuyo código de formato define un patrón de fecha u hora se muestra como una fecha. Si elimina el formato, la misma celda mostrará un número; el valor subyacente nunca cambió.
Es por esto que leer el valor de una celda le entrega un Variant que puede ser un varDate o un Double simple, y por qué el formato de número en la misma celda es la señal que decide qué quiso expresar un tercero. Cuando HotXLS abre un archivo XLSX, una celda transporta tanto su Value como su NumberFormatIndex a TXLSXCell, y el índice de formato es lo que se consulta para saber si el número representa una fecha.
var
Book: TXLSXWorkbook;
Cell: TXLSXCell;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('timesheet.xlsx') <> 1 then
raise Exception.Create('Cannot open workbook');
Cell := Book.Sheets[0].Cells[1, 1]; // row 1, col 1 (1-based)
// Value may arrive as varDate or as a plain numeric serial;
// the format index is the signal that tells them apart.
Writeln('raw value : ', VarToStr(Cell.Value));
Writeln('numFmt idx: ', Cell.NumberFormatIndex);
Writeln('format : ', Cell.NumberFormat);
finally
Book.Free;
end;
end;
Dos épocas de inicio, con 1462 días de diferencia
El sistema de fechas predeterminado (el que utiliza cada libro de trabajo de Windows) cuenta desde el final de 1899, por lo que el serial 1 cae en el primer día de 1900. El otro sistema proviene de los primeros equipos Macintosh y cuenta desde el inicio de 1904, por lo que su serial 1 es cuatro años y un día posterior. Un libro de trabajo registra cuál sistema utiliza en una bandera. En un paquete OOXML, esa bandera es date1904 en la parte del libro de trabajo; HotXLS la expone como la propiedad Date1904 del libro de trabajo.
La diferencia entre las dos épocas es exactamente de 1462 días. Eso equivale a cuatro años calendario (tres de 365 días y uno de 366, que suman 1461) más uno adicional por el desfase de un día y poco más entre las dos convenciones de día cero. El número es fijo y puede recordarlo con facilidad. Su importancia radica en que no es cero. Un serial copiado de un libro de trabajo de 1904 e interpretado bajo las reglas de 1900, o viceversa, desfasa cada fecha en 1462 días, lo que se presenta como fechas erróneas por poco más de cuatro años y es fácil de confundir con datos corruptos.
Debido a que el propio TDateTime de Delphi está anclado a la convención de 1900, una biblioteca que mapea seriales de Excel a TDateTime tiene a aplicar un desfase de 1462 en ambas direcciones cada vez que el libro de trabajo está marcado con la bandera 1904. Al leer un serial de 1904, reste 1462 antes de tratarlo como un TDateTime; al escribir un TDateTime en un libro de trabajo de 1904, reste 1462 del serial para que Excel renderice el día que deseaba indicar. HotXLS aplica este desplazamiento internamente cuando serializa valores de fecha para un libro de trabajo cuya propiedad Date1904 está configurada, de modo que el valor que asigne como un TDateTime se conserve en el mismo día calendario en la pantalla de ida y vuelta.
La particularidad deliberada del año bisiesto de 1900
Hay un detalle famoso en el sistema de 1900. Excel trata a 1900 como un año bisiesto y acepta el 29 de febrero de 1900 como una fecha real (serial 60). El año 1900 no fue bisiesto, porque los años de fin de siglo solo son bisiestos si son divisibles por 400, y 1900 no lo es. El día fantasma es un comportamiento de compatibilidad deliberado heredado de una hoja de cálculo antigua que se distribuyó con el error, el cual se mantiene desde entonces para que las operaciones aritméticas seriales sigan siendo idénticas a lo largo de décadas de archivos.
La consecuencia práctica es pequeña pero real: para cualquier fecha a partir del 1 de marzo de 1900, el serial es uno más alto de lo que daría un recuento de días estrictamente correcto, porque el inexistente 29 de febrero consumió un número. A spreadsheet library reproduce la particularidad en lugar de corregirla, porque la tarea consiste exactamente en coincidir con las operaciones aritméticas de Excel. Corregirla pondría cada fecha moderna un día desfasada de lo que muestra Excel, lo cual es un resultado peor que arrastrar un error de desvío por uno que tiene cuarenta mil días de antigüedad y que ninguna fecha real en uso comercial toca jamás. El sistema de 1904 no contiene un día fantasma equivalente, lo cual es una de las razones por las que algunos prefirieron históricamente el sistema de 1904.
Detección de una fecha a partir de numFmt
Cuando llega un número proveniente de un archivo escrito por otra persona, su formato es la única evidencia de que representa una fecha. ECMA-376 asigna un bloque de identificadores de formato integrados cuyo significado está fijado por la especificación, y los formatos de fecha y hora ocupan rangos conocidos. Los identificadores 14 al 22 son los formatos de fecha y hora generales de configuración regional (el conocido m/d/yyyy, h:mm y sus similares). Los identificadores 45 al 47 son los formatos de tiempo transcurrido. Dos bandas adicionales (27 al 36 y 50 al 58) son los formatos de fecha y hora específicos de la configuración regional utilizados para calendarios CJK, definidos en ECMA-376 18.8.30. Una celda cuyo identificador de formato de número cae en cualquiera de estos rangos es una celda de fecha u hora.
Los identificadores integrados cubren los casos comunes pero no los personalizados. Cuando un libro de trabajo define su propio código de formato (por ejemplo, un orden no estándar o un nombre de mes localizado), el identificador se encuentra por encima del rango integrado y apunta a la tabla de formatos numéricos del libro de trabajo. Para estos, reconocer una fecha significa leer la cadena del código de formato y buscar tokens de fecha. HotXLS reúne ambas comprobaciones en un predicado interno, XlsxNumFmtIsDate, que devuelve true inmediatamente para los rangos de fecha integrados y, de lo contrario, analiza el código de formato personalizado mediante XlsxFormatCodeIsDate. La parte pública de eso es la cadena NumberFormat de la celda y su NumberFormatIndex, que le ofrecen tanto el código de formato resuelto como el identificador a probar.
Por qué el analizador de formato no puede limitarse a buscar d y m
Analizar un código de formato buscando tokens de fecha parece sencillo hasta que recuerda qué más existe en un formato de número. Una búsqueda ingenua de las letras que componen las fechas (la d, m, y, h y s de día, mes, año, hora y segundo) fallará ante dos estructuras que no representan tokens de fecha en absoluto.
La primera es el literal de cadena entre comillas. Un formato numérico puede incrustar texto literal entre comillas dobles, por lo que un formato financiero como #,##0 "MM" anexa los caracteres M y M a un número sin ningún significado temporal. Un escáner que cuente las letras dentro de las comillas como tokens de mes identificaría erróneamente ese formato de moneda como una fecha. La segunda es la sección entre corchetes. Los formatos numéricos contienen directivas entre corchetes (nombres de colores como [Red], condiciones de comparación como [>1000], etiquetas de configuración regional y marcadores de tiempo transcurrido como [h] y [mm]). Parte del contenido de los corchetes contiene letras de fecha y otra no, y tratar el texto entre corchetes de la misma manera que el cuerpo del formato conduce tanto a falsos positivos como a casos omitidos.
El analizador correcto recorre el código de formato carácter por carácter, rastreando si se encuentra dentro de un literal entre comillas y qué tan profundo está dentro del anidamiento de corchetes, y también respeta el escape de barra invertida que califica a un solo carácter posterior. Solo una letra de fecha sin escapar que se encuentre fuera de cualquier literal de cadena y fuera de cualquier sección entre corchetes cuenta como un token de fecha real. Así es exactamente como escanea XlsxFormatCodeIsDate: una comilla cambia un estado de literal que suprime la detección de tokens hasta la comilla de cierre, una barra invertida omite el siguiente carácter y un contador de profundidad de corchetes suprime la detección dentro de los bloques [...]. El beneficio es que #,##0 "MM" se lee correctamente como un formato numérico, mientras que un código personalizado breve que solo contiene una m o una d fuera de las comillas se sigue reconociendo correctamente como una fecha.
Lectura de fechas de archivos de terceros
Todo lo anterior converge en un único flujo de trabajo: convertir un número que escribió otra aplicación en una fecha en la que pueda confiar. El serial le proporciona el recuento de días, la bandera Date1904 del libro de trabajo le indica desde cuál época se mide el recuento y el identificador de formato de número o código personalizado de la celda es la única evidencia de que el número estaba destinado a ser una fecha en primer lugar. Omita cualquiera de los tres y obtendrá una respuesta incorrecta con apariencia plausible en lugar de un error visible.
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Cell: TXLSXCell;
r: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('vendor-export.xlsx') <> 1 then
raise Exception.Create('Cannot open export');
// The 1904 flag is workbook-wide: read it once, apply it to
// every serial the workbook hands back.
if Book.Date1904 then
Writeln('workbook uses the 1904 date system')
else
Writeln('workbook uses the 1900 date system');
Sheet := Book.Sheets[0];
for r := 1 to 10 do
begin
Cell := Sheet.Cells[r, 1];
// A date is only a date when its format says so; the same numeric
// value with a plain format is just a quantity.
Writeln(Format('row %d value=%s numFmt=%d code="%s"',
[r, VarToStr(Cell.Value), Cell.NumberFormatIndex, Cell.NumberFormat]));
end;
finally
Book.Free;
end;
end;
El lado de BIFF heredado contiene una trampa adicional que vale la pena mencionar. En un flujo .xls más antiguo, una serie de celdas numéricas adyacentes se pueden empaquetar en un único registro multicelda, el MULRK, que almacena varios valores con sus referencias de formato en una sola estructura. Las celdas de fecha almacenadas de esa manera no dejan de ser fechas por estar empaquetadas, de modo que la misma prueba de identificador de formato debe aplicarse celda por celda dentro del registro multicelda, y el desfase de 1904 sigue rigiendo cada serial que produce. Un lector que solo inspeccione registros numéricos independientes, y omita los empaquetados, convertirá silenciosamente una columna de fechas en una columna de enteros.
Mapeo de seriales a TDateTime en la práctica
Una vez que la comprobación de formato confirma una fecha y se conoce la bandera Date1904, la conversión es mecánica. Un valor que HotXLS ya devuelve como un varDate es un TDateTime que puede usar directamente. Un valor que llega como un Double simple, lo que ocurre cuando el origen escribió un serial sin un formato de fecha reconocido, se convierte leyéndolo como un recuento de días en el eje de 1900 y, para un libro de trabajo de 1904, restando primero el desfase de 1462 días para que las épocas se alineen. En sentido contrario, asignar un TDateTime a una celda almacena el serial basado en 1900, y HotXLS aplica el mismo desplazamiento de 1462 días al guardar cuando el libro de trabajo está marcado con la bandera 1904, de modo que el archivo guardado muestra la fecha que deseaba en lugar de una desfasada por cuatro años.
Configure la bandera deliberadamente cuando genere un libro de trabajo. El valor predeterminado deja Date1904 en false, lo que coincide con Excel para Windows y es casi siempre lo que desea; establézcalo en true únicamente cuando esté reproduciendo un libro de trabajo de origen Mac o cuando un sistema posterior espere específicamente el eje de 1904. La única regla que previene toda la clase de errores de cuatro años es la consistencia: elija la época de inicio una vez por libro de trabajo, escriba cada fecha bajo ella y lea cada serial de vuelta bajo la bandera que el archivo realmente contenga.
Las fechas son una columna dentro de una historia más amplia sobre lo que una celda realmente contiene. La capa de metadatos vecina (el título, el autor y las marcas de tiempo que acompañan a la cuadrícula) se cubre en nuestro artículo sobre metadatos de libros de trabajo y propiedades del documento, donde los mismos valores Created y Modified se almacenan como TDateTime con la misma convención de que valor no establecido equivale a cero. Cuando una fecha es el resultado de un cálculo en lugar de un valor almacenado, las reglas de evaluación de nuestro artículo sobre el motor de fórmulas y funciones personalizadas determinan el serial que luego renderiza el formato. Ambos trabajan sobre el mismo modelo de fechas que se incluye en el componente HotXLS para Delphi y C++Builder, el cual lee y escribe fechas de XLS y XLSX sin automatización de Excel.