Abra una hoja de cálculo, haga clic en una celda que muestra 2026-06-19 y la barra de fórmulas seguirá leyendo una fecha. Lea la misma celda desde Delphi y obtendrá el número 46192. Ambas vistas son correctas, porque Excel nunca almacenó una fecha en esa celda. Almacenó un número de serie, un recuento de días, y adjuntó un formato numérico que le indica a la pantalla que represente el recuento como una fecha de calendario. No hay un tipo de fecha en el valor de la celda. Hay un número y una regla de visualización, y la regla de visualización es lo único que distingue una fecha de una cantidad simple
Esa separación es la raíz de cada error de fecha que una biblioteca de hojas de cálculo debe esquivar. Un serial por sí solo no dice qué día es, porque no dice cuál fue el día cero. El mismo número significa dos fechas con cuatro años de diferencia dependiendo de una sola bandera en el libro de trabajo. Y un número que debería leerse como una fecha se leerá como una mera cantidad a menos que algo inspeccione su formato y reconozca un patrón de fecha. Así es como está construido el modelo de fecha 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 desde una época, 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 marca como temporal. Lo que lo marca es el formato numérico de la celda: ECMA-376 llama a esto un numFmt, y una celda cuyo código de formato indica un patrón de fecha u hora se muestra como una fecha. Quite el formato y la misma celda mostrará un número; el valor subyacente nunca cambió
Es por esto que leer el valor de una celda le devuelve un Variant que puede ser un varDate o puede ser un Double simple, y por qué el formato numérico en la misma celda es la señal que decide qué quiso decir un tercero. Cuando HotXLS abre un archivo XLSX, una celda lleva tanto su Value como su NumberFormatIndex a TXLSXCell, y el índice de formato es lo que usted consulta para saber si el número es 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, con 1462 días de diferencia
El sistema de fechas predeterminado, el que utilizan todos los libros de Windows, cuenta desde el final de 1899, de modo que el serial 1 cae en el primer día de 1900. El otro sistema se remonta a los primeros Macintosh y cuenta desde el inicio de 1904, por lo que su serial 1 es cuatro años y un día más tarde. Un libro registra cuál sistema utiliza en una bandera. En un paquete OOXML esa bandera es date1904 en la parte del libro; HotXLS la expone como la propiedad Date1904 del libro
La brecha entre las dos épocas es de exactamente 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 para compensar la diferencia de un día y pico entre las dos convenciones de día cero. El número es fijo y puede recordarlo fácilmente. Su importancia radica en que no es cero. Un serial copiado de un libro 1904 e interpretado bajo las reglas de 1900, o viceversa, sitúa cada fecha a 1462 días de distancia, 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 que compensar por 1462 en ambas direcciones siempre que el libro esté marcado como 1904. Al leer un serial de 1904, reste 1462 antes de tratarlo como un TDateTime; al escribir un TDateTime en un libro de 1904, reste 1462 del serial para que Excel represente el día que pretendía. HotXLS aplica este cambio internamente cuando serializa valores de fecha para un libro cuya propiedad Date1904 está configurada, de modo que el valor que asigne como un TDateTime regrese al mismo día calendario en la pantalla
La peculiaridad 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 un año bisiesto, porque los años centenarios son bisiestos solo cuando 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 temprana que incluía el error, conservado desde entonces para que la aritmética de los seriales se mantenga idéntica 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, debido a que el inexistente 29 de febrero de 1900 consumió un número. Una biblioteca de hojas de cálculo reproduce la peculiaridad en lugar de corregirla, porque la tarea consiste exactamente en coincidir con la aritmética de Excel. Corregirlo pondría cada fecha moderna un día desviada de lo que muestra Excel, lo cual es un resultado peor que arrastrar una desviación de un día de hace cuarenta mil días que ninguna fecha real en uso comercial toca jamás. El sistema de 1904 no tiene un día fantasma equivalente, que es una de las razones por las que algunas empresas lo prefirieron históricamente
Detección de una fecha a partir de numFmt
Cuando llega un número de un archivo que escribió otra persona, su formato es la única evidencia de que es 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 de configuración regional general, los familiares m/d/yyyy, h:mm y sus parientes. Los identificadores 45 al 47 son los formatos de tiempo transcurrido. Dos bandas adicionales, de la 27 a la 36 y de la 50 a la 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 numérico 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 está por encima del rango integrado y apunta a la tabla de formatos numéricos del libro de trabajo. Para esos casos, reconocer una fecha significa leer la cadena del código de formato y buscar tokens de fecha. HotXLS pliega ambas comprobaciones en un predicado interno, XlsxNumFmtIsDate, que devuelve verdadero inmediatamente para los rangos de fecha integrados y, en caso contrario, analiza el código de formato personalizado a través de XlsxFormatCodeIsDate. El lado público de esto es la cadena NumberFormat de la celda y su NumberFormatIndex, que le proporcionan tanto el código de formato resuelto como el identificador para probar
Por qué el analizador de formatos no puede simplemente buscar d y m
Analizar un código de formato para buscar tokens de fecha parece trivial hasta que recuerda qué más vive en un formato numérico. Una búsqueda ingenua de las letras que deletrean las fechas, la d, m, y, h y s de día, mes, año, hora y segundo, fallará en dos estructuras que no son tokens de fecha en absoluto
La primera es la cadena literal entrecomillada. Un formato numérico puede incrustar texto literal entre comillas dobles, por lo que un formato financiero como #,##0 "MM" añade 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 marcaría erróneamente ese formato de moneda como una fecha. La segunda es la sección entre corchetes. Los formatos numéricos llevan directivas entre corchetes, nombres de colores como [Red], condiciones de comparación como [>1000], etiquetas de configuración regional y los marcadores de tiempo transcurrido [h] y [mm]. Algunos contenidos de los corchetes contienen letras de fecha y otros no, y tratar el texto entre corchetes de la misma manera que el cuerpo del formato conduce tanto a falsos positivos como a casos perdidos
El analizador correcto recorre el código de formato carácter por carácter, rastreando si se encuentra dentro de un literal entrecomillado y qué tan profundo está dentro del anidamiento de corchetes, y también respeta el escape de barra invertida que califica a un único carácter siguiente. Solo una letra de fecha sin escape 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 analiza XlsxFormatCodeIsDate: una comilla invierte un estado de literal que suprime la detección de tokens hasta la comilla de cierre, una barra invertida salta el carácter siguiente y un contador de profundidad de corchetes suprime la detección dentro de las ejecuciones [...]. El beneficio es que #,##0 "MM" se lee correctamente como un formato numérico, mientras que un código personalizado conciso que no contiene nada más que una sola m o d fuera de comillas se reconoce correctamente como una fecha
Lectura de fechas de archivos de terceros
Todo lo anterior converge en un flujo de trabajo: convertir un número que escribió otra aplicación en una fecha en la que pueda confiar. El serial le indica el recuento de días, la bandera Date1904 del libro le indica desde qué época se mide el recuento y el identificador de formato numérico o código personalizado de la celda es la única evidencia de que el número fue pensado como una fecha en primer lugar. Elimine cualquiera de los tres y obtendrá una respuesta incorrecta 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 tiene una trampa adicional que vale la pena mencionar. En un flujo .xls más antiguo, una ejecución de celdas numéricas adyacentes se puede 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, por lo que la misma prueba de identificador de formato tiene que llegar al interior del registro multicelda y aplicarse por celda, y la compensación de 1904 sigue gobernando cada serial que produce. Un lector que solo inspecciona registros de números independientes, y omite 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 entrega de vuelta como un varDate es un TDateTime que puede usar directamente. Un valor que llega como un Double simple, lo que sucede 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 1904, restando la compensación de 1462 días primero para que las épocas se alineen. En la otra dirección, asignar un TDateTime a una celda almacena el serial basado en 1900, y HotXLS aplica el mismo cambio de 1462 días al guardar cuando el libro está marcado como 1904, de modo que el archivo guardado muestra la fecha que pretendía en lugar de una desplazada cuatro años
Establezca la bandera deliberadamente cuando genere un libro de trabajo. El valor predeterminado deja Date1904 como falso, lo que coincide con Excel para Windows y es casi siempre lo que desea; establézcalo en verdadero solo cuando esté reproduciendo un libro de origen Mac o cuando un sistema posterior espere específicamente el eje de 1904. La única regla que evita toda la clase de errores de cuatro años es la consistencia: elija la época una vez por libro de trabajo, escriba cada fecha bajo ella y lea cada serial bajo la bandera que el archivo realmente lleva
Las fechas son una columna en una historia más amplia sobre lo que realmente contiene una celda. La capa de metadatos vecina, el título, el autor y las marcas de tiempo que viajan junto a la cuadrícula, se cubren en nuestro artículo sobre metadatos de libros y propiedades del documento, donde los mismos valores Created y Modified valores se almacenan como TDateTime con la misma convención de valor no establecido es igual a cero. Cuando una fecha es el resultado de un cálculo en lugar de un valor almacenado, las reglas de evaluación en nuestro artículo sobre el motor de fórmulas y funciones personalizadas determinan el serial que el formato luego representa. Ambos funcionan sobre el mismo modelo de fecha que se incluye en el componente HotXLS para Delphi y C++Builder sin automatización de Excel