Odborný článok

XLSX Shared Formula si expanzia v Delphi: úskalia

Follower shared formula v XLSX nenesie žiadny text vzorca. Jeho element <f t="shared" si="N"/> ukazuje na master bunku inde v hárku, a reader musí rebuildnúť text posunom master vzorca o rozdiel riadku a stĺpca. HotXLS Component pre Delphi a C++Builder robí túto expanziu pri otvorení, takže každý follower hlási kompletný vzorec

Ak ste niekedy načítali skutočný XLSX v knižnici tretej strany a zistili, že stĺpec tisíc vzorcov má text v presne jednej bunke a prázdne reťazce v ostatných 999, stretli ste túto funkciu z nesprávnej strany. Nič nie je poškodené. Súbor robí to, čo mu dovoľuje ECMA-376, a reader jednoducho zastal v bode, kde skončil XML

Prečo je bunka shared formula prázdna?

Pretože formát zámerne ukladá vzorec raz. V ECMA-376 Part 1 a ISO/IEC 29500-1 element <f> (§18.3.1.40) nesie atribút t typu ST_CellFormulaType, a hodnota shared znamená, že táto bunka je súčasťou skupiny identifikovanej atribútom si. Presne jedna bunka v skupine, master, nesie tiež atribút ref, ktorý udáva rozsah, na ktorý sa skupina vzťahuje, a len táto bunka nesie text vzorca ako obsah elementu. Každá ďalšia bunka v skupine je follower. Opakuje t="shared" a to isté si, a jej obsah elementu je prázdny. Excel zapisuje tieto skupiny agresívne, pretože fill-down cez stĺpec s 200 000 riadkami sa zredukuje z 200 000 reťazcov vzorca na jeden reťazec plus 199 999 drobných zástupných elementov. Úspora je reálna a cena dopadá úplne na readera: bez expanzie follower nemá vlastný význam

Posun je preklad, nie kópia textu

HotXLS rieši followera lokalizáciou master registrovaného pod tým istým si, výpočtom delty riadku a stĺpca od master kotvy k aktuálnej bunke, a prekladom každej referencie v master vzorci o túto deltu. Relatívne rozmery sa pohnú, absolútne rozmery nie, a zmiešané referencie pohnú len svoju neabsolútnu polovicu. Reťazcové literály sa úplne preskočia, takže vzorec, ktorý náhodou obsahuje text "A1", ponechá tento text v každom followeri nezmenený

const
  // xl/worksheets/sheet1.xml, trimmed to the interesting cells
  SheetXml: WideString=
    '<row r="1"><c r="A1"><v>1</v></c>'+
    '<c r="B1"><f t="shared" si="4" ref="B1:B3">'+
    'A1+$A$1+A$1+$A1+&quot;A1&quot;+SUM(A1:A2)</f><v>7</v></c></row>'+
    '<row r="2"><c r="B2"><f t="shared" si="4"/><v>8</v></c></row>'+
    '<row r="3"><c r="B3"><f t="shared" si="4"></f><v>9</v></c></row>';

var
  Wb: TXLSXWorkbook;
  Sh: TXLSXWorksheet;
begin
  Wb:= TXLSXWorkbook.Create;
  try
    Wb.Open(FileName);
    Sh:= Wb.Sheets[1];
    // Master, verbatim
    // B1 -> A1+$A$1+A$1+$A1+"A1"+SUM(A1:A2)
    // Follower one row down: relative row moves, absolute row frozen,
    // the mixed A$1 keeps its row, and the literal stays a literal
    // B2 -> A2+$A$1+A$1+$A2+"A1"+SUM(A2:A3)
    ShowMessage(Sh.Cells[2, 2].Formula);
  finally
    Wb.Free;
  end;
end;

Atribút ref je brána, nie dekorácia. Follower, ktorého súradnice padnú mimo platný rozsah master, sa neexpanduje, pretože súbor by inak robil tvrdenie, ktoré skupina nepodporuje. Podobne, keď by posun potlačil referenciu nad riadok jedna alebo naľavo od stĺpca A, HotXLS vydá #REF! pre tento token namiesto potichu ho orezania, čo je to, čo by sám Excel vyprodukoval pri rovnakej úprave. Táto translácia je blízka sesternica, ale nie to isté, čo prepisovanie referencií, ktoré sa deje pri vkladaní alebo mazaní riadkov. Táto cesta má svoje vlastné pravidlá o tom, čo sa deje s rozsahom, keď ním prereže úprava, a je popísaná samostatne v článku o úprave referencií vzorca pri vkladaní a mazaní. Shared expanzia je jednoduchšia: je to čistý offset od známej kotvy, aplikovaný raz, pri parsovaní

Ktoré tvary referencií musí shifter pokryť?

Všetky, alebo je expanzia skrytý bug straty dát. Naivný shifter, ktorý rozumie len A1 a A1:B2, pokazí alebo zahodí exotickejšie formy, a skutočné workbooky sú ich plné. Prekladač shared formula v HotXLS rozpozná celú rodinu A1 skôr, než rozhodne, čo posunúť. Referencie na externý workbook ako [Book.xlsx]Sheet1!A1 a 3D referencie ako Sheet1:Sheet3!A1 si ponechajú svoju predponu nedotknutú, kým sa posunie koncová referencia bunky. Uvádzané mená hárkov prežijú, vrátane nepríjemného prípadu, keď je hárok doslova pomenovaný A1, takže 'A1'!A1 posunie len časť za výkričníkom. Celý stĺpec A:A posunie svoj rozmer stĺpca a nič iné; celý riadok 1:1 posunie svoj rozmer riadku a nič iné; $A:$A sa nepohne vôbec. Štruktúrované referencie tabuľky ako Table[A1] ostanú nedotknuté, pretože časť v zátvorkách je meno stĺpca, nie súradnica

// One master, expanded two columns to the right and zero rows down.
// Master D1: A1+A:A+$A:$A
// F1       : C1+C:C+$A:$A
//
// One master, expanded three rows down and zero columns across.
// Master A1: B1+$C$1+"A1"+A:A+1:1+'Data'!A1+LOG10(A1)+Table[A1]+'A1'!A1
// A3       : B3+$C$1+"A1"+A:A+3:3+'Data'!A3+LOG10(A3)+Table[A1]+'A1'!A3
//
// Note what did NOT move in the second line: the absolute $C$1, the
// string literal "A1", the whole column A:A under a pure row delta,
// the function name LOG10, and the structured reference Table[A1]

Mená funkcií sú tu tichou pascou. Token scanner, ktorý zoberie písmená nasledované číslicami, ochotne prepíše LOG10 na LOG11 o riadok nižšie. HotXLS vyžaduje hranicu referencie pred kandidátnym tokenom a za ním, takže identifikátor, ktorý pokračuje do písmena, číslice, podčiarkovníka, bodky, alebo otváracej zátvorky, nie je referencia bunky. Ak pracujete v druhej notačnej rodine, rovnaký problém hranice sa prejaví inak, a článok o notácii R1C1 pokrýva, kde sa tieto dva modely rozchádzajú

Prečo self-closing element f pohltí ďalšiu hodnotu?

Pretože self-closing element neprodukuje žiadnu udalosť end-element. Toto je jednoznačne najdrahší bug v celej funkcii, a nie je špecifický pre žiadny konkrétny XML parser. V TXMLReader <f t="shared" si="4"/> vyvolá presne jednu udalosť Element s IsEmptyElement nastaveným na True, a nikdy nevyvolá zodpovedajúci EndElement. Parser, ktorý zatvára svoj stav zachytávania vzorca len na EndElement, teda ostane vnútri vzorca, a ďalší text, ktorý uvidí, čo je cachovaný výsledok vnútri <v>, sa pripojí do bufferu vzorca. Ešte horšie, stav prežije hranicu bunky, takže ďalšia bunka, ktorá vlastní skutočný <f>, má svoj text vzorca pohltený predchádzajúcou bunkou. Oprava je ukončiť stav vzorca na samotnej udalosti Element vždy, keď je IsEmptyElement True, a spustiť celé riešenie followera tam namiesto čakania. To znamená prečítať t, si, ref, aca a ca z atribútov, aplikovať shared expanziu, zapísať atribúty prepočtu na bunku, a vyčistiť shared stav, všetko vnútri vetvy, ktorá spracúva prázdny element. Všimnite si, že formát povoľuje oba spôsoby zápisu, <f t="shared" si="4"/> a <f t="shared" si="4"></f>, a druhý naozaj vyvolá EndElement. Správny reader musí spracovať tento pár identicky, čo je dôvod, prečo HotXLS pokrýva oba spôsoby zápisu v tom istom regresnom súbore

Riedke, neusporiadané hodnoty si a fronta čakajúcich

Atribút si je súborom dodané celé číslo bez znamienka, nie pozícia poľa, ktorú kontrolujete. Nič v schéme nevyžaduje, aby shared indexy boli husté, začínali od nuly, alebo sa objavovali vo vzostupnom poradí, a nič nebráni nepriateľskému alebo len zvláštnemu súboru použiť si="4294967290" na prvej bunke. Dimenzovanie vyhľadávacieho poľa podľa najväčšieho pozorovaného si je teda primitívum na vyčerpanie pamäte, nie optimalizácia. HotXLS udržuje cestu otvorenia workbooku namiesto toho na zoradenej riedkej tabuľke: shared skupiny sa registrujú pod svojím celočíselným kľúčom v zoradenom TStringList, čo robí vyhľadávanie binárnym hľadaním cez toľko skupín, koľko ich skutočne existuje, bez vzťahu k číselnej veľkosti indexov. Poradie je druhá polovica problému. Master normálne predchádza svojich followerov v poradí dokumentu, ale to je konvencia, nie pravidlo, takže akýkoľvek follower, ktorý nedokáže vyriešiť svoje si v okamihu, keď sa parsuje, ide do fronty čakajúcich. Keď sa hárok dokončí, fronta sa prehrá voči teraz kompletnej tabuľke, a neskoré mastery vyriešia svoje siroty. Bunky, ktoré nikdy nenájdu master, si ponechajú prázdny vzorec, čo je poctivý výsledok pre súbor, ktorý odkazuje na skupinu, ktorú nikdy nedefinoval

Expanzia shared formula bez načítania workbooku

Streamovacie readery čelia rovnakej požiadavke pod oveľa tesnejším pamäťovým rozpočtom, a riešia to hárku-lokálnou tabuľkou. TXLSDirectReader aj TXLSRowCursor obidva expandujú followerov na kompletné vzorce per bunka pri zachovaní svojho ohraničeného pamäťou a projekčného správania, takže forward-only prechod cez 300 MB hárok vám stále odovzdá skutočný text vzorca

var
  Reader: TXLSDirectReader;
  Cursor: TXLSRowCursor;
begin
  // Projection: only rows 2..3, only column A. The master lives in row 1,
  // outside the projection, and is still parsed so the followers resolve
  Reader:= TXLSDirectReader.Create;
  try
    Reader.FirstRow:= 2;
    Reader.LastRow:= 3;
    Reader.IncludeColumn(1);
    Reader.OnCell:= HandleCell;   // Cell.Formula is fully expanded here
    Reader.ReadFile(FileName);
  finally
    Reader.Free;
  end;

  // Forward-only row traversal, same expansion
  Cursor:= TXLSRowCursor.Create;
  try
    Cursor.Open(FileName);
    if Cursor.FindFirst then
      repeat
        if Cursor.CellCount > 0 then
          WriteLn(Cursor.RowIndex, ': ', Cursor.Cells[0].Formula);
      until not Cursor.FindNext;
  finally
    Cursor.Free;
  end;
end;

Z tohto dizajnu vyplývajú dve obmedzenia. Po prvé, projekcia nikdy nemôže preskočiť master. Filter riadkov nastavený pomocou FirstRow a LastRow, alebo filter stĺpcov zostavený pomocou IncludeColumn, môže preskočiť vydanie master bunky vášmu callbacku, ale parser stále musí zaznamenať jeho si, súradnice kotvy, platný rozsah a text vzorca, inak sa každý follower vnútri projekcie vyrieši na nič. Bezpečné preskočiť je len práca na strane followera, posun a dekódovanie hodnoty. Po druhé, tabuľka je per hárok a jej životný cyklus sa musí spravovať explicitne: TXLSRowCursor drží jednu inštanciu počas trvania prechodu hárkom a vyčistí ju pri reštarte, prepnutí hárka, konci súboru, výnimke a zatvorení, takže skupina definovaná na hárku jeden nikdy nemôže uniknúť na hárok dva. Pretože streamovacia cesta je horúca slučka, používa open-addressing celočíselný hash namiesto zoradenej reťazcovej tabuľky, čo sa vyhne konverzii celé číslo-na-reťazec na bunku

Čo sa stane pri uložení a kde sú hranice

Keď je raz follower expandovaný, je to obyčajný vzorec, a HotXLS ho zapíše späť ako samostatný element <f> bez t="shared" a bez si. Round trip je stabilný a cachované výsledky <v> prežijú, ale výstup je väčší než vstup pre silne zdieľaný hárok, a zoskupenie, ktoré vytvoril Excel, sa pri uložení nerekonštruuje. Ak je pre vás byte-level vernosť shared skupín dôležitejšia než mať skutočný text vzorca v každej bunke, toto je obchod, ktorý akceptujete. Strana XLS je mimochodom iná: BIFF8 záznam SHRFMLA má vlastné kódovanie a vlastný writer, s prepínačom shared-group na workbooku

Dve súvisiace veci sú výslovne nie shared formula, hoci zdieľajú element <f>. Legacy CSE array formuly používajú t="array" s ref pokrývajúcim ukotvený rozsah, a dynamic arrays používajú rovnaký spôsob zápisu t="array", ale sú identifikované atribútom cm, ktorý sa reťazí cez cellMetadata na záznam XLDAPR. Traktovanie spill bunky dynamického poľa ako shared alebo CSE followera je skutočná chyba správnosti, a toto rozlíšenie je pokryté v článku o dynamic array a spill vzorcoch. Čítajte tieto tri prípady ako tri parsery, ktoré náhodou zdieľajú meno tagu, a kód ostane poctivý

Expanzia shared formula, streamovacie readery a prekladač referencií tu popísané sa dodávajú ako súčasť HotXLS Excel komponenty pre Delphi a C++Builder; produktová stránka nesie kompletnú referenciu API vzorcov a priameho čítania, vrátane vyššie použitých projekčných vlastností