Articolo tecnico

Seriali di data in Excel in Delphi: 1900 rispetto a 1904 e numFmt

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];   // riga 1, col 1 (con base 1)
    // Value può arrivare come varDate o come un serial numerico semplice;
    // l'indice del formato è il segnale che li distingue.
    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

Cronologia che confronta i sistemi di data Excel 1900 e 1904, mostrando il divario fisso di 1462 giorni tra le epoche e il fantasma 29 febbraio 1900 giorno bisestile gestito da HotXLS in Delphi
I sistemi 1900 e 1904 contano da diversi punti giorno zero, così lo stesso giorno di calendario siede esattamente a 1462 numeri seriali di distanza; solo l'asse 1900 porta il fantasmatico 29 febbraio al seriale 60

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

Rilevamento delle date in due stadi per le celle HotXLS: prima gli intervalli di id dei formati numerici incorporati, poi una scansione consapevole di virgolette e parentesi dei codici di formato personalizzati in Delphi
Gli id di formato integrati risolvono subito la maggior parte delle celle; i codici personalizzati ricadono su una passeggiata per caratteri che ignora i letterali tra virgolette, le sezioni tra parentesi quadre e le escape con backslash

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

Flusso di lavoro per leggere date da fogli di calcolo di terze parti con HotXLS: il formato numerico deve dichiarare una data e il flag Date1904 decide se il seriale necessita dello spostamento di 1462 giorni prima di diventare un TDateTime Delphi
Tre fatti decidono ogni conversione: il formato deve rivendicare una data, il flag Date1904 fissa l'epoca, e un Double nudo prende la correzione di 1462 giorni mentre un varDate viene usato così com'è
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');

    // Il flag 1904 è a livello di workbook: leggilo una volta, applicalo a
    // ogni serial restituito dal workbook.
    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];
      // Una data è una data solo quando il suo formato lo dice; lo stesso valore numerico
      // con un formato semplice è solo una quantità.
      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