Articolo tecnico

Validità schema dei campi pivot XLSX in Delphi con HotXLS

HotXLS scrive definizioni di tabelle pivot XLSX i cui elementi pivotField e cacheField validano contro lo schema ECMA-376 Part 1 §18.10: gli attributi axis usano i token ST_Axis axisRow, axisCol e axisPage, i campi dell'area valori portano dataField="1", le liste di item non sono mai vuote, e i campi di cache memorizzano un numFmtId numerico. Dalla v2.384.33 il reader onora anche i default dello schema che prima sbagliava

I bug dietro questa pulizia condividono un tratto poco lusinghiero: nessuno di loro ha mai fatto fallire un test. HotXLS scriveva una pivot, HotXLS la rileggeva, ogni campo atterrava sull'axis giusto, e la suite di round trip restava verde per anni. Il problema era che writer e reader si erano messi d'accordo in silenzio su un dialetto privato. Una pivot costruita da Delphi sembrava a posto al componente che l'aveva fatta, mentre un controllo contro CT_PivotField e CT_CacheField tirava fuori token di enumerazione invalidi, un elemento vuoto che lo schema vieta e flag che Excel si aspetta ma non ha mai ricevuto. Se generate pivot su un server e le spedite a persone che le aprono in Excel o le passano ai loro parser, l'unico contratto che conta è lo schema, non quel che per caso perdona il vostro reader

Perché i round trip HotXLS non beccavano mai i token axis sbagliati?

I round trip HotXLS non beccavano mai i token axis sbagliati perché il reader accettava entrambe le grafie. Il vecchio XlsxPivotAxisAttr emetteva axis="rowAxis", colAxis e pageAxis, che si leggono naturale in inglese ma non esistono nello schema; ST_Axis definisce esattamente quattro valori, axisRow, axisCol, axisPage e axisValues. Nel frattempo PivotAxisFromToken in lxPivotXml.pas accettava sia il token dello schema sia quello inventato, quindi ogni self-test passava. Il writer ora emette solo i token dello schema, e il reader continua ad accettare le vecchie grafie così che i file salvati da versioni HotXLS precedenti si carichino ancora col layout intatto

<!-- prima della v2.384.33: valore ST_Axis invalido, CT_Items vuoto -->
<pivotField axis="rowAxis" defaultSubtotal="1"><items count="0"></items></pivotField>

<!-- dalla v2.384.33 -->
<pivotField axis="axisRow" defaultSubtotal="1">
  <items count="4"><item x="0"/><item x="1"/><item x="2" h="1"/><item t="default"/></items>
</pivotField>
XML pivotField di HotXLS prima e dopo la v2.384.33 dove il valore axis inventato rowAxis e un elemento items vuoto violano CT_PivotField finché il writer non emette token ST_Axis come axisRow con vere voci item, un flag hidden mantenuto e un subtotal default in coda che lo schema accetta
Il reader tollerante accettava entrambe le grafie, quindi ogni round trip passava mentre il file falliva qualunque controllo severo dello schema — scrivete solo i quattro token ST_Axis e lasciate che CT_Items porti almeno un item

Che cosa esige CT_PivotField che il vecchio writer saltava?

CT_PivotField esige tre cose che il vecchio BuildPivotTableXml tralasciava o sbagliava. Primo, un campo aggregato nell'area valori deve dirlo sulla propria definizione con dataField="1"; il writer ora imposta quel flag su ogni campo referenziato da una voce in DataFields, non solo nella lista <dataFields>. Secondo, CT_Items ha bisogno di almeno un item, quindi un campo senza item non riceve più un <items count="0"> vuoto e l'intero elemento viene semplicemente omesso. Terzo, ogni item conserva il proprio stato: h="1" per un item nascosto (TXLSPivotItem.IsHidden) e sd="0" per i dettagli collassati (IsDetailHidden), entrambi scartati dal vecchio writer a ogni salvataggio

La parte sottile sono gli item subtotal in coda. Quando un campo ha item, Excel elenca un item extra per funzione subtotal dopo gli item dati, tipizzato con ST_ItemType: <item t="default"/> per il subtotal automatico, poi sum, countA, avg, max, min, product, count, stdDev, stdDevP, var e varP per quelli espliciti. HotXLS ricava quelle voci da TXLSPivotField.Subtotals al salvataggio e le conteggia dentro items count. I campi creati da AddPivotTable partono con un insieme Subtotals vuoto, che scrive defaultSubtotal="0" e nessun item in coda, quindi chiedete esplicitamente i subtotal quando il report li vuole. Notate la trappola sui nomi: xlpsCount mappa a countA (tutte le voci) e xlpsCountNums mappa a count (solo numeri)

Anatomia della lista item pivot di HotXLS dove le voci item dati sono seguite da item subtotal in coda ricavati da TXLSPivotField.Subtotals come t=default e t=avg e conteggiati dentro items count, con la trappola dei nomi xlpsCount verso countA e xlpsCountNums verso count spiegata
I campi da AddPivotTable partono con un insieme Subtotals vuoto, che scrive defaultSubtotal=0 e nessun item in coda — chiedete le funzioni che volete e il writer ricava un item per funzione dentro il conteggio
uses
  lxHandleX, lxPivot;

var
  Book  : TXLSXWorkbook;
  Sheet : TXLSXWorksheet;
  Pivot : TXLSPivotTable;
  Region: TXLSPivotField;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('orders.xlsx');
    Sheet := Book.Sheets[1];                  // in base 1, come l'engine XLS
    Pivot := Sheet.AddPivotTable('Data!$A$1:$D$800', 3, 6, 'RegionTotals');
    if Pivot = nil then
      raise Exception.Create('Bad source range or anchor');

    Region := Pivot.AddRowField('Region');    // nil se il campo non esiste
    if Region <> nil then
      Region.Subtotals := [xlpsDefault, xlpsAverage];  // -> t="default", t="avg"
    Pivot.AddColumnField('Quarter');
    Pivot.AddDataFieldByName('Revenue', xlpaSum);      // imposta dataField="1" su Revenue

    Book.SaveAs('orders-pivot.xlsx');
  finally
    Book.Free;
  end;
end;

Come legge HotXLS ora gli item subtotal e i default dello schema?

Il reader HotXLS ora salta ogni item il cui attributo t è presente e diverso da data, perché le voci subtotal, grand total e vuote non portano alcun indice di cache. Prima della v2.384.34 quelle voci venivano caricate come item ordinari con CacheItemIndex a -1, così una pivot fatta da Excel tornava con membri fantasma che non puntavano a nulla, e qualsiasi codice che percorreva Items doveva filtrarli a mano. Dato che il writer ricostruisce le voci in coda da Subtotals, il compito del reader è tradurle in quell'insieme, non tenerle come dati

La seconda correzione del reader riguarda gli attributi assenti. Nello schema, defaultSubtotal su CT_PivotField e containsString su CT_SharedItems valgono entrambi true per default, ed Excel li omette quando portano quel default. HotXLS leggeva un attributo mancante come false, il che significa che ogni pivot salvata da Excel perdeva in silenzio il suo subtotal predefinito al caricamento, e un campo cache di testo puro veniva classificato come misto invece che stringa. È l'immagine speculare del bug degli axis: un writer che scrive sempre ogni attributo per esteso non esercita mai il percorso dei default, quindi solo file di un altro produttore lo espongono

Perché numFmtId="General" era invalido sui campi cache?

Il valore numFmtId="General" era invalido perché ST_NumFmtId è un intero senza segno, non un nome di formato. Il vecchio writer di cache scriveva quella stringa hard-coded su ogni cacheField, prendendo a prestito il nome che gli utenti vedono nella finestra Formato celle. HotXLS ora scrive il NumberFormat del campo cache come numero, che è 0 (il formato General incorporato) salvo impostazioni diverse. Un parser severo che tipizza gli attributi dallo schema rifiuta seccamente il vecchio valore, ed è esattamente la famiglia di guasti che diventa una finestra di riparazione; l'articolo su le regole OPC e markup dietro la richiesta di riparazione di Excel copre come quelle finestre vengono innescate

Perché le pivot sotto la riga 65535 venivano tagliate?

Le tabelle pivot XLSX piazzate alla riga 65536 o sotto venivano tagliate perché il modello pivot condiviso memorizzava FirstRow, LastRow, FirstHeaderRow, FirstDataRow e le controparti di colonna come Word, e il codice di spostamento righe le clampava con Min(.., High(Word)). È un avanzo del record SxView BIFF8, dove 16 bit bastano, ma un foglio XLSX arriva a 1.048.576 righe. Dalla v2.384.37 quelle proprietà su TXLSPivotTable sono Integer, i clamp sono spariti, e solo il writer BIFF8 restringe i valori. TXLSXWorksheet.AddPivotTable e AddPivotTableCopy ora restituiscono nil per un'ancora fuori da 1..1048576 per 1..16384, o per una copia la cui estensione uscirebbe dalla griglia

Ancora pivot HotXLS alla riga 70001 contro il tetto a 16 bit dove FirstRow e LastRow erano memorizzati come Word e clampati con Min contro High(Word) a 65535, tagliando le pivot alla riga 65536 o sotto finché la v2.384.37 non ha spostato il modello a campi Integer con ritorno nil fuori dalla griglia
I campi Word erano un avanzo del SxView BIFF8 in un formato i cui fogli arrivano a 1048576 righe — un'ancora oltre la riga 65536 finiva nel wrap nel range a 16 bit e perdeva la sua pivot al salvataggio
var
  Pivot: TXLSPivotTable;
  Check: TXLSXWorkbook;
begin
  // La riga 70001 una volta finiva nel wrap del range a 16 bit; ora sopravvive a salvataggio e caricamento
  Pivot := Sheet.AddPivotTable('Data!$A$1:$D$800', 70001, 1, 'LateTotals');
  if Pivot = nil then
    Exit;  // ancora fuori dal foglio o intervallo sorgente irrisolvibile
  Pivot.AddRowField('Region');
  Pivot.AddDataFieldByName('Revenue', xlpaSum);
  Book.SaveAs('late.xlsx');

  Check := TXLSXWorkbook.Create;
  try
    Check.Open('late.xlsx');
    Pivot := Check.Sheets[1].PivotTables.FindByName('LateTotals');
    Assert((Pivot <> nil) and (Pivot.FirstRow = 70001));
  finally
    Check.Free;
  end;
end;

L'engine XLS classico ha ricevuto la correzione corrispondente nella v2.384.38. Il suo modello memorizzava i valori grezzi in base 0 di SxView e DConRef e faceva passare le ancore di AddPivotTable direttamente, mentre la documentazione, le demo e l'engine XLSX usavano tutti celle in base 1 come Cells[Row, Col]. Entrambi gli engine ora tengono posizioni in base 1 nel modello, il reader BIFF8 aggiunge 1 e il writer toglie 1 al confine del record, quindi il codice che ancorava a (0, 0) deve passare a (1, 1), perché il classico AddPivotTable ora restituisce nil per un'ancora fuori da 1..65536 per 1..256; la nuova chiamata scrive gli stessi byte della vecchia. Il layout dei record in sé è invariato ed è descritto in i record SX BIFF8 dietro le pivot dei .xls classici

Validare contro lo schema, non contro il vostro reader

La lezione generalizza oltre le pivot: un reader tollerante nasconde le violazioni del writer, quindi un round trip attraverso il vostro codice prova coerenza, non correttezza. Ogni bug qui è sopravvissuto perché il lato tollerante e il lato difettoso vivevano nella stessa libreria. I controlli che beccano davvero questa classe di difetti sono una validazione dello schema delle parti generate, file prodotti da Excel passati nel vostro reader con attributi omessi ai loro default, e fixture che inchiodano il token esatto anziché il risultato analizzato. Le pivot costruite tramite l'API, compresi i campi calcolati, gli item calcolati e i layout percent-of-total mostrati in costruire e aggiornare tabelle pivot XLSX con campi calcolati, ricevono l'XML corretto senza modifiche al codice, mentre le pivot caricate da file Excel continuano a riprodurre le loro parti originali finché non le modificate

Tutte queste correzioni sono nella versione attuale del componente foglio di calcolo HotXLS per Delphi, che legge e scrive XLS, XLSX e tabelle pivot da Delphi e C++Builder senza Excel né automazione COM sulla macchina