Apri un foglio di calcolo, fai clic su una cella che mostra 2026-06-19 e la barra delle formule mostrerà comunque una data. Leggi la stessa cella da Delphi e otterrai il valore numerico 46192. Entrambe le visualizzazioni sono corrette, poiché Excel non ha memorizzato una data in quella cella. Ha registrato un numero seriale, ovvero un conteggio di giorni, e vi ha associato un formato numerico che indica allo schermo di mostrare tale valore come data di calendario. Non vi è alcun tipo data nel valore della cella. Vi sono un numero e una regola di visualizzazione, e quest'ultima è l'unico elemento che distingue una data da una quantità semplice.
Questa separazione è all'origine di ogni bug relativo alle date che una libreria per fogli di calcolo deve evitare. Un numero seriale da solo non indica quale giorno sia, poiché non definisce il giorno zero di riferimento. Lo stesso valore identifica due date distanti quattro anni a seconda di un singolo flag della cartella di lavoro. Inoltre, un valore da interpretare come data verrà letto come semplice quantità numerica a meno che non si verifichi il suo formato riconoscendovi una struttura di data. In base a questo principio è strutturato il modello delle date in HotXLS.
Una cella di data è un numero associato a un formato
Excel memorizza le date come numero di giorni trascorsi da un'epoca specifica, indicando l'ora del giorno nella parte frazionaria. Mezzogiorno su un valore seriale è rappresentato da .5. La parte intera costituisce il conteggio dei giorni. Nulla nel valore memorizzato indica la natura temporale. Ciò che la definisce è il formato numerico della cella: lo standard ECMA-376 definisce questo elemento come numFmt, e una cella il cui codice di formato definisce una data o un'ora viene visualizzata in tale modalità. Rimuovendo il formato, la cella mostrerà un numero; il valore sottostante non ha subito modifiche.
Per questo motivo la lettura del valore di una cella restituisce un tipo Variant che può essere un varDate o un semplice Double, ed è per questo che il formato numerico sulla cella rappresenta il segnale che indica l'intento originale. Quando HotXLS apre un file XLSX, una cella trasferisce sia il suo Value sia il suo NumberFormatIndex in TXLSXCell, e l'indice del formato è l'elemento da verificare per stabilire se il valore rappresenti una data.
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;
Due epoche, distanti 1462 giorni
Il sistema di date predefinito, utilizzato da ogni cartella di lavoro su Windows, conta a partire dalla fine del 1899, in modo che il valore seriale 1 corrisponda al primo giorno del 1900. L'altro sistema, derivato dai primi Macintosh, conta dall'inizio del 1904, quindi il suo valore seriale 1 corrisponde a quattro anni e un giorno dopo. Una cartella di lavoro memorizza il sistema in uso in un flag. In un pacchetto OOXML tale impostazione corrisponde al flag date1904 nella sezione workbook; HotXLS lo espone come proprietà Date1904 della cartella di lavoro.
La differenza tra le due epoche è esattamente pari a 1462 giorni. Si tratta di quattro anni solari (tre da 365 giorni e uno da 366, per un totale di 1461 giorni) più un giorno per la differenza tra le due convenzioni del giorno zero. La cifra è fissa e facile da ricordare. La sua rilevanza è legata al fatto che non è zero. Un valore seriale copiato da una cartella di lavoro del sistema 1904 e interpretato con le regole del sistema 1900 (o viceversa) sfasa ogni data di 1462 giorni, posizionando le date in modo errato di circa quattro anni, problema facile da confondere con la corruzione dei dati.
Poiché la gestione TDateTime di Delphi è associata alla convenzione del sistema 1900, una libreria che mappa i valori seriali di Excel a TDateTime deve applicare uno offset di 1462 in entrambe le direzioni quando la cartella di lavoro è contrassegnata come 1904. Leggendo un seriale del sistema 1904, sottrai 1462 prima di gestirlo come TDateTime; scrivendo un valore TDateTime in una cartella di lavoro 1904, sottrai 1462 dal seriale in modo che Excel mostri il giorno desiderato. HotXLS applica questa correzione internamente quando serializza i valori delle date per cartelle di lavoro con impostazione Date1904, garantendo che il valore assegnato come TDateTime corrisponda allo stesso giorno di calendario visualizzato sullo schermo.
La gestione intenzionale dell'anno bisestile 1900
Vi è una particolarità nel sistema 1900. Excel considera il 1900 come anno bisestile e accetta il 29 febbraio 1900 come data valida, associandole il seriale 60. L'anno 1900 non è stato un anno bisestile (gli anni secolari sono bisestili solo se divisibili per 400, e il 1900 non lo è). Questo giorno fittizio è un comportamento di compatibilità ereditato da un vecchio foglio di calcolo che includeva tale anomalia, conservato in seguito affinché l'aritmetica dei seriali rimanga allineata tra file creati in epoche diverse.
La conseguenza pratica è minima ma esistente: per qualsiasi data a partire dal 1° marzo 1900, il seriale è superiore di un'unità rispetto a un conteggio dei giorni corretto, poiché il 29 febbraio inesistente ha occupato un valore. Una libreria di fogli di calcolo riproduce questa particolarità anziché risolverla, poiché l'allineamento con l'aritmetica di Excel è l'obiettivo richiesto. Modificare questo comportamento sposterebbe ogni data moderna di un giorno rispetto a quanto mostrato in Excel, un risultato peggiore rispetto a conservare una differenza risalente a decenni fa che non influisce sulle date in uso commerciale. Il sistema 1904 non contiene giorni fittizi, motivo per cui storicamente era preferito in determinati ambiti.
Rilevare una data tramite numFmt
Quando si riceve un valore numerico da un file generato esternamente, il suo formato rappresenta l'unica indicazione della natura di data. Lo standard ECMA-376 assegna un blocco di identificatori di formato predefiniti il cui significato è fisso, e i formati di data e ora occupano intervalli noti. Gli identificatori da 14 a 22 rappresentano i formati generali di data e ora, come i classici m/d/yyyy, h:mm e correlati. Gli identificatori da 45 a 47 sono dedicati al tempo trascorso. Altri intervalli, da 27 a 36 e da 50 a 58, rappresentano formati di data e ora specifici della localizzazione per calendari CJK, definiti in ECMA-376 §18.8.30. Una cella il cui identificatore di formato rientra in questi intervalli è gestita come data o ora.
Gli identificatori predefiniti coprono i casi comuni ma non quelli personalizzati. Quando una cartella di lavoro definisce un proprio codice di formato, ad esempio con ordinamenti personalizzati o nomi di mesi localizzati, l'id si colloca oltre l'intervallo predefinito e punta alla tabella dei formati numerici della cartella. Per questi casi, il riconoscimento della data richiede l'analisi della stringa del codice alla ricerca di token di data. HotXLS raccoglie queste verifiche nel predicato interno XlsxNumFmtIsDate, che restituisce vero per gli intervalli predefiniti e altrimenti analizza il formato tramite XlsxFormatCodeIsDate. A livello pubblico questo corrisponde alla stringa NumberFormat della cella e alla proprietà NumberFormatIndex, che forniscono il codice di formato risolto e l'id da verificare.
Perché l'analisi del formato non può limitarsi a cercare i caratteri d e m
L'analisi di un codice alla ricerca di token di data sembra semplice finché non si considerano gli altri elementi presenti in un formato numerico. Una ricerca grezza dei caratteri associati alle date, ovvero d, m, y, h e s per giorno, mese, anno, ora e secondo, fallirebbe in corrispondenza di strutture che non rappresentano affatto token temporali.
La prima è la stringa letterale racchiusa tra virgolette. Un formato numerico può includere testo tra virgolette, ad esempio un formato finanziario come #,##0 "MM" che accoda i caratteri M ed M a un numero senza alcun valore temporale. Un parser che conteggi i caratteri tra virgolette come token di mese identificherebbe erroneamente quel formato come data. La seconda è la sezione tra parentesi quadre. I formati numerici contengono istruzioni racchiuse tra parentesi quadre, come i nomi dei colori [Red], condizioni di confronto come [>1000], tag locali e marcatori di tempo trascorso [h] e [mm]. Alcune informazioni tra parentesi contengono lettere di data e altre no, e trattarle allo stesso modo del corpo del formato provocherebbe sia falsi positivi sia omissioni.
Il parser corretto analizza il codice di formato carattere per carattere, verificando se si trovi all'interno di una stringa tra virgolette e la profondità di nidificazione delle parentesi quadre, e gestisce anche i caratteri di escape con barra rovesciata (backslash) che escludono il carattere successivo. Solo i caratteri di data non esclusi trovati all'esterno di stringhe letterali e parentesi quadre sono considerati token reali. Questa è la logica di scansione di XlsxFormatCodeIsDate: una virgoletta attiva uno stato che disattiva il rilevamento fino alla chiusura, una barra rovesciata salta il carattere successivo, e le parentesi quadre disattivano il controllo al loro interno. In questo modo, #,##0 "MM" viene letto correttamente come formato numerico, mentre un codice personalizzato contenente solo m o d all'esterno di virgolette viene identificato come data.
Lettura delle date da file generati esternamente
Tutti gli elementi descritti convergono in un unico flusso di lavoro: convertire un valore numerico generato esternamente in una data affidabile. Il seriale fornisce il conteggio dei giorni, il flag Date1904 specifica l'epoca di inizio e l'identificatore del formato (o il codice personalizzato) rappresenta l'indicazione che il valore sia effettivamente una data. Omettere uno di questi elementi produce un risultato errato ma apparentemente plausibile.
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;
Il formato storico BIFF presenta un'ulteriore particolarità. In uno stream .xls obsoleto, un gruppo di celle numeriche adiacenti può essere compresso in un singolo record multi-cella (MULRK), che memorizza valori e riferimenti di formato in una sola struttura. Le celle di data memorizzate in questo modo sono a tutti gli effetti date, quindi la stessa verifica deve essere eseguita per ciascuna cella, e l'offset 1904 si applica ugualmente. Un lettore che controllasse solo i record numerici indipendenti, saltando quelli compressi, convertirebbe le date in numeri interi.
Associare i seriali a TDateTime nella pratica
Una volta confermato il formato di data ed estratto il flag Date1904, la conversione è immediata. Un valore restituito da HotXLS come varDate è un oggetto TDateTime pronto all'uso. Un valore restituito come Double semplice (quando il file sorgente ha scritto il seriale senza un formato riconosciuto) viene convertito leggendolo come conteggio giorni sul sistema 1900 e, per cartelle di lavoro 1904, sottraendo prima l'offset di 1462 giorni per allineare le epoche. Al contrario, l'assegnazione di un valore TDateTime memorizza il seriale basato sul sistema 1900, e HotXLS applica lo spostamento di 1462 giorni al salvataggio se la cartella di lavoro utilizza il sistema 1904, in modo che Excel mostri la data corretta.
Imposta questo flag in modo consapevole durante la generazione delle cartelle di lavoro. L'opzione predefinita mantiene Date1904 a false, allineandosi ad Excel per Windows; impostalo a true solo per riprodurre cartelle di lavoro create su Mac o se i sistemi a valle lo richiedono. La regola essenziale consiste nella coerenza: seleziona l'epoca una volta per cartella di lavoro, scrivi ogni data in quel formato e leggi ogni seriale basandoti sul flag realmente presente nel file.
Le date rappresentano solo una parte della gestione dei valori delle celle. La gestione dei metadati, come le informazioni su autore, titolo e timestamp memorizzate come TDateTime, è trattata nel nostro articolo sui metadati e sulle proprietà dei documenti. Quando una data deriva da un calcolo anziché da un valore fisso, le regole di elaborazione descritte nell'articolo sul motore delle formule e sulle funzioni personalizzate determinano il seriale tracciato dal formato. Entrambi operano sullo stesso modello di date incluso nel componente HotXLS per Delphi e C++Builder, che legge e scrive date XLS e XLSX senza richiedere l'automazione di Excel.