Odborný článok

Platnosť schémy pivot polí XLSX v Delphi s HotXLS

HotXLS zapisuje definície pivot tabuliek XLSX, ktorých elementy pivotField a cacheField prejdú schémou ECMA-376 Part 1 §18.10: axis atribúty používajú tokeny ST_Axis axisRow, axisCol a axisPage, polia vo value area nesú dataField="1", zoznamy items nikdy nie sú prázdne a cache polia ukladajú číselné numFmtId. Od v2.384.33 reader tiež rešpektuje schémové predvolené hodnoty, ktoré dovtedy čítal zle

Bugy za týmto upratovaním zdieľajú nelichotivú črtu: žiaden z nich nikdy neporušil test. HotXLS napísal pivot, HotXLS si ho načítal späť, každé pole pristalo na správnej osi a round-trip suite bola zelená roky. Problém bol v tom, že writer a reader sa poticho dohodli na súkromnom dialekte. Pivot postavený z Delphi vyzeral pre komponent, ktorý ho vytvoril, dobre, zatiaľ čo kontrola proti CT_PivotField a CT_CacheField vyzdvihla neplatné enumeračné tokeny, prázdny element, ktorý schéma zakazuje, a flagy, ktoré Excel očakáva, ale nikdy ich nedostal. Ak pivots generujete na serveri a posielate ľuďom, ktorí ich otvárajú v Exceli alebo ich púšťajú do vlastných parserov, jediná zmluva, ktorá sa počíta, je schéma, nie to, čomu váš vlastný reader prípadne odpustí

Prečo round tripy HotXLS nikdy nezachytili zlé axis tokeny?

Round tripy HotXLS nikdy nezachytili zlé axis tokeny preto, lebo reader prijímal oba hláskovania. Starý XlsxPivotAxisAttr emitoval axis="rowAxis", colAxis a pageAxis, čo sa v angličtine čítalo prirodzene, ale v schéme neexistujú; ST_Axis definuje presne štyri hodnoty, axisRow, axisCol, axisPage a axisValues. Zároveň PivotAxisFromToken v lxPivotXml.pas zachytával aj schémový token aj vymyslený, takže každý self-test prešiel. Writer teraz emituje len schémové tokeny a reader naďalej prijíma staré hláskovania, aby súbory uložené staršími verziami HotXLS stále načítali so zachovaným rozložením

<!-- pred v2.384.33: neplatná hodnota ST_Axis, prázdne CT_Items -->
<pivotField axis="rowAxis" defaultSubtotal="1"><items count="0"></items></pivotField>

<!-- od 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>
pivotField XML v HotXLS pred a po v2.384.33, kde vymyslená axis hodnota rowAxis a prázdny element items porušujú CT_PivotField, kým writer neemituje tokeny ST_Axis ako axisRow so skutočnými položkami item, zachovaným flagom skrytia a trailing default subtotal, ktoré schéma prijíma
Zhovievavý reader prijímal obe hláskovania, takže každý round trip prešiel, kým súbor padol na ľubovoľnej prísnej schémovej kontrole — píšte len štyri tokeny ST_Axis a nechajte CT_Items niesť aspoň jeden item

Čo vyžaduje CT_PivotField, čo starý writer vynechával?

CT_PivotField vyžaduje tri veci, ktoré starý BuildPivotTableXml vynechal alebo mal zle. Po prvé, pole agregované vo value area to musí povedať na vlastnej definícii cez dataField="1"; writer teraz nastavuje tento flag na každom poli referencovanom položkou v DataFields, nie len v zozname <dataFields>. Po druhé, CT_Items potrebuje aspoň jeden item, takže pole bez itemov už nedostane prázdne <items count="0"> a celý element sa jednoducho vynechá. Po tretie, každý item si drží stav: h="1" pre skrytý item (TXLSPivotItem.IsHidden) a sd="0" pre skryté detaily (IsDetailHidden), čo starý writer pri každom uložení zahodil

Jemnejšia časť sú trailing subtotal itemy. Keď pole má itemy, Excel vypíše po dátových itemoch jeden extra item na každú subtotal funkciu, typovaný cez ST_ItemType: <item t="default"/> pre automatický subtotal, potom sum, countA, avg, max, min, product, count, stdDev, stdDevP, var a varP pre explicitné. HotXLS odvodzuje tieto položky z TXLSPivotField.Subtotals pri ukladaní a počíta ich do items count. Polia vytvorené cez AddPivotTable začínajú s prázdnou množinou Subtotals, čo zapisuje defaultSubtotal="0" bez žiadneho trailing itemu, takže si subtotals explicitne vyžiadajte, keď ich report potrebuje. Všímajte si pascu v pomenovaní: xlpsCount sa mapuje na countA (všetky položky) a xlpsCountNums na count (len čísla)

Anatómia zoznamu pivot items v HotXLS, kde položky dátových itemov nasledujú trailing subtotal itemy odvodené z TXLSPivotField.Subtotals, ako t=default a t=avg, a počítajú sa do items count, s rozpísanou pastou pomenovania xlpsCount na countA a xlpsCountNums na count
Polia z AddPivotTable zaínajú s prázdnou množinou Subtotals, čo zapisuje defaultSubtotal=0 bez žiadneho trailing itemu — požiadajte o funkcie, ktoré chcete, a writer odvodí jeden item na funkciu do počtu
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];                  // 1-based, ako XLS engine
    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, ak také pole neexistuje
    if Region <> nil then
      Region.Subtotals := [xlpsDefault, xlpsAverage];  // -> t="default", t="avg"
    Pivot.AddColumnField('Quarter');
    Pivot.AddDataFieldByName('Revenue', xlpaSum);      // nastaví Revenue dataField="1"

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

Ako HotXLS teraz číta subtotal itemy a schémové predvolené hodnoty?

Reader HotXLS teraz preskočí každý item, ktorého atribút t je prítomný a nie je data, lebo položky subtotal, grand-total a blank nesú žiadny cache index. Pred v2.384.34 sa tieto položky načítali ako obyčajné itemy s CacheItemIndex nastaveným na -1, takže pivot od Excelu sa vrátil s fantomovými členmi ukazujúcimi do prázdna a každý kód prechádzajúci Items ich musel odfiltrovať ručne. Keďže writer prestavia trailing položky z Subtotals, úlohou readera ich preložiť do tejto množiny, nie ich držať ako dáta

Druhá oprava readera sa týka atribútov, ktoré chýbajú. V schéme majú defaultSubtotal na CT_PivotField a containsString na CT_SharedItems predvolenú hodnotu true a Excel ich vynecháva, keď ju nesú. HotXLS čítal chýbajúci atribút ako false, čo znamenalo, že každý pivot uložený Excelom poticho stratil pri načítaní default subtotal a čisté textové cache pole sa klasifikovalo ako mixed namiesto string. Toto je zrkadlový obraz bugu s osami: writer, ktorý vždy vypíše každý atribút, nikdy nevyužije cestu s predvolenou hodnotou, takže ju odhalia len súbory od iného producenta

Prečo bolo numFmtId="General" na cache poliach neplatné?

Hodnota numFmtId="General" bola neplatná, pretože ST_NumFmtId je bezznamienkové celé číslo, nie meno formátu. Starý cache writer túto hodnotu hard-codoval na každý cacheField a požičiaval si meno, ktoré užívatelia vidia v dialógu Format Cells. HotXLS teraz zapisuje NumberFormat cache poľa ako číslo, ktoré je 0 (vstavaný General formát), pokiaľ ho niečo nenastavilo. Prísny parser, ktorý typuje atribúty zo schémy, starú hodnotu odmietne rovno a presne táto trieda zlyhaní sa mení na repair dialóg; článok o pravidlách OPC a markup za repair dialógom Excelu popisuje, ako sa tie dialógy spúšťajú

Prečo boli pivot tabuľky pod riadkom 65535 odrezané?

Pivot tabuľky XLSX umiestnené na riadku 65536 a nižšie sa odrezali, pretože zdieľaný pivot model ukladal FirstRow, LastRow, FirstHeaderRow, FirstDataRow a ich stĺpcové náprotivky ako Word a kód posúvajúci riadky ich zvieral cez Min(.., High(Word)). To je pozostatok BIFF8 recordu SxView, kde 16 bitov stačí, ale hárok XLSX beží do 1 048 576 riadkov. Od v2.384.37 sú tieto vlastnosti na TXLSPivotTable typu Integer, zvieranie zmizlo a zužuje už len BIFF8 writer. TXLSXWorksheet.AddPivotTable a AddPivotTableCopy teraz vracajú nil pre kotvu mimo 1..1048576 × 1..16384 alebo pre kópiu, ktorej rozsah by prebehol mimo mriežky

Kotva pivotu HotXLS na riadku 70001 proti 16-bitovému stropu, kde FirstRow a LastRow boli uložené ako Word a zvreté cez Min proti High(Word) na 65535, čo odrezalo pivots na riadku 65536 a nižšie, kým v2.384.37 nepresunula model na polia Integer s návratom nil mimo mriežky
Polia Word boli pozostatok BIFF8 SxView vo formáte, ktorej hárky bežia do 1048576 riadkov — kotva za riadkom 65536 sa zabala do 16-bitového rozsahu a prišla o svoj pivot pri uložení
var
  Pivot: TXLSPivotTable;
  Check: TXLSXWorkbook;
begin
  // Riadok 70001 sa predtým zabál do 16-bitového rozsahu; teraz prežije uloženie aj načítanie
  Pivot := Sheet.AddPivotTable('Data!$A$1:$D$800', 70001, 1, 'LateTotals');
  if Pivot = nil then
    Exit;  // kotva mimo hárka alebo nevyriešiteľný zdrojový range
  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;

Klasický XLS engine dostal zodpovedajúcu opravu vo v2.384.38. Jeho model ukladal surové 0-based hodnoty SxView a DConRef a kotvy AddPivotTable prenášal priamo, zatiaľ čo dokumentácia, demá aj XLSX engine používali 1-based bunky ako Cells[Row, Col]. Oba enginey teraz držia v modeli 1-based pozície, BIFF8 reader pripočíta 1 a writer odpočíta 1 na hranici recordu, takže kód kotviaci na (0, 0) sa musí presunúť na (1, 1), lebo klasické AddPivotTable vracia teraz nil pre kotvu mimo 1..65536 × 1..256; nové volanie zapisuje tie isté bajty ako staré. Samotné rozloženie recordov sa nezmenilo a je popísané v BIFF8 SX recordoch za klasickými pivot tabuľkami .xls

Validujte proti schéme, nie proti vlastnému readeru

Lekcia sa generalizeje za pivots: zhovievavý reader skrýva previnenia writeru, takže round trip cez vlastný kód dokazuje konzistentnosť, nie správnosť. Každý bug tu prežil, pretože tolerantná a pokazená strana žili v tej istej knižnici. Kontroly, ktoré túto triedu defektov naozaj chytia, sú schémová validácia generovaných častí, súbory od Excelu pustené cez váš reader s atribútmi vynechanými na predvolených hodnotách a fixtúry pripichujúce presný token namiesto vyparsovaného výsledku. Pivots postavené cez API, vrátane calculated fields, calculated items a rozložení percent-of-total ukázaných v stavbe a refreshovaní pivot tabuliek XLSX s calculated fields, dostanú opravené XML bez zmeny kódu, zatiaľ čo pivots načítané z excelových súborov prehrávajú svoje pôvodné časti, dokiaľ ich nezmeníte

Všetky tieto opravy idú v aktuálnom HotXLS Delphi spreadsheet component, ktorý číta a zapisuje XLS, XLSX a pivot tabuľky z Delphi a C++Builderu bez Excelu alebo COM automation na stroji