Technical Article

BIFF8 PivotTable-ök írása Delphiben: SXDB és SXLI

Az örökölt Excel bináris formátum szinte minden része egyetlen rekord, tiszta kétbájtos típussal és kétbájtos hosszal. Egy cella LABELSST vagy NUMBER. Egy összevont tartomány a MERGEDCELLS. A munkalap nagy részét beolvashatja a rekordok egyenkénti bejárásával és a típus szava szerinti elosztásával. A kimutatások (PivotTables) megtörik ezt a ritmust. Egyetlen kimutatás nem egy rekord, hanem egy kicsi program, amely több tucat együttműködő rekordból áll, amelyek ugyanazon OLE összetett dokumentumfolyam (OLE compound document stream) két különböző helyén oszlanak el, és a köztük lévő kapcsolatok pozicionálisak, bit-csomagoltak és megbocsáthatatlanok. Ez az a struktúra, amelyet a legtöbb BIFF8 olvasó teljesen kihagy, vagy átlátszatlan bájtként őriz meg, mert a semmiből való megírása azt jelenti, hogy reprodukálni kell minden olyan kereszthivatkozást, amelyet maga az Excel tart fenn.

A kimutatás nehézségének oka az, hogy valójában két összehegesztett műtárgyról van szó. Létezik a pivot gyorsítótár (pivot cache), a forrásadatok önálló pillanatképe a saját alfolyamával (substream), és létezik a táblázat nézet (table view), az elrendezés, amely megmondja, mely mezők melyik tengelyen ülnek. A gyorsítótár és a nézet index alapján hivatkoznak egymásra. Rontson el egyetlen indexet, és a fájl frissítési hibával vagy csendben üres ráccsal nyílik meg.

A pivot gyorsítótár egy saját alfolyam

A gyorsítótár a munkafüzet globális folyamában él teljes BIFF alfolyamként (substream), amelyet egy BOF rekord keretez, amelynek dokumentumtípusa 0x0006 (ez a pivot gyorsítótárat jelölő érték, szemben a munkafüzet 0x0005 vagy a munkalap 0x0010 értékével), és a hozzá tartozó EOF zár le. Ezen a kereten belül a struktúra rögzített. Egy SXDB rekord a gyorsítótár fejléce. Hordozza a rekordszámot, a gyorsítótár-mezők számát és az adatfolyam-azonosítót, amelyet a táblázat nézet fog idézni, hogy hozzákötődjön ehhez a gyorsítótárhoz. Minden forrásoszlop ezután hozzájárul egy SXFDB meződefiníciós rekorddal, amelyet az azt osztályozó SXFDBType követ, majd az adott oszlop által felvett egyedi értékek következnek, amelyeket különálló értékenként egy gépelt elemrekordként bocsátanak ki.

Az elemrekordok azok, ahol a gyorsítótár megszolgálja a helyét. A szöveges értékből SXSTRING lesz, a numerikusból SXNUM, a logikai értékből SXBOOLEAN, a képlethibából pedig SXERR. A gyorsítótár nem a forrásrácsot tárolja, hanem a mezőnkénti egyedi értékeket, valamint egy indextáblát, amely megmondja, hogy az n-edik rekordhoz melyik egyedi elemet vette fel az egyes mező. Ezért a kimutatás programozott felépítése nem cellák másolásának kérdése. Be kell szkennelnie a forrástartományt, kikövetkeztetnie az egyes mezők típusát az általuk tartott értékekből, deduplikálnia kell őket egy gépelt elemlistává, és minden sort elem-indexek párjaként kell rögzítenie. A HotXLS pontosan ezt teszi: a csupa számot tartalmazó oszlopot SXNUM elemekkel bocsátja ki, a vegyes szöveges oszlopból SXSTRING elemek lesznek, a dátumok pedig sorozatos értékként utaznak ugyanazon a numerikus útvonalon.

SXDBB és a bit-csomagolás, ami érdekessé teszi

A rekordonkénti indextábla a teljes struktúra leginkább technikailag különös része, és a SXDBB rekordban él. A naiv kódolás minden mező elem-indexét 16 bites szóként tárolná. Az Excel nem ezt teszi. Pontosan annyi bitbe csomagolja az egyes mezők indexét, amennyi a mező elemeinek címzéséhez szükséges, és nem többe. A szélesség ceil(log2(itemCount + 1)) bit. A + 1 számít: a plusz érték egy sentinel (jelzőőr), ami azt jelenti, hogy "üres, nincs érték ehhez a mezőhöz ebben a rekordban", így egy három egyedi elemet tartalmazó mezőnek négy állapotot kell képviselnie, ezért két bitet igényel, nem pedig azt az egy bitet, amit a három elem önmagában sugallna. Az elemeket egyáltalán nem tartalmazó mező nulla bittel járul hozzá, és a csomagolás során teljesen kimarad.

Egy rekord bitjei összefűződnek az összes mezőben, majd a következő rekord új bájthatáron kezdődik. A rekordok bájt-igazítottak, nem bit-csomagoltak a végétől a végéig, ami a táblázatba való tetszőleges elérést kezelhetővé teszi, soronként néhány kitöltő bit (padding bits) árán. A bájton belüli csomagolás a legkisebb helyiértékű bittel (least-significant-bit) kezdődik. Amint elfogadja ezt a két szabályt, a kódoló egy egyszerű bit-pumpa, a dekódoló pedig ennek a tükre.

// Width of one field's index in the SXDBB stream.
// citmTotal distinct items need ceil(log2(citmTotal + 1)) bits,
// the +1 reserving a "blank" sentinel value.
function BitsForFieldItems(itemCount: Integer): Integer;
var
  capacity: Integer;
begin
  Result := 0;
  if itemCount <= 0 then
    Exit;            // empty field contributes zero bits
  Result := 1;
  capacity := 2;
  while capacity < itemCount + 1 do
  begin
    Inc(Result);
    capacity := capacity * 2;
  end;
end;

Azért nem szabad figyelmen kívül hagyni ezt a részletet, mert egyetlen BIFF rekordon 8224 bájtos felső határ van. A formátum minden rekordjának, beleértve a pivot rekordokat is, legfeljebb 8224 bájtba kell beleférnie, és egy sűrű, több ezer forrássorral rendelkező pivot gyorsítótár jóval azelőtt túllépi ezt, hogy minden sort kibocsátott volna. Ezért az indextáblát felosztják. A HotXLS egyetlen SXDBB törzsét 8220 bájtban korlátozza, ami a 8224-es rekordkorlát mímusz a típus és hossz négybájtos rekordfejléce, elosztja ezt az egy csomagolt rekord bájtszélességével, hogy megtudja, hány teljes sor fér el, majd annyi folytatólagos SXDBB rekordot bocsát ki, amennyit a sorszám megkövetel. Minden folytatás tisztán indul újra a rekord határán, így egyetlen sor sem vágódik ketté két rekord között. Az az olvasó, amely ismeri a rekordonkénti bitszélességet, sorban végig tud lépkedni minden SXDBB rekordon, mintha azok egyetlen összefüggő bittömböt alkotnának.

A nézet elrendezése: SXLI a törzshöz, SXPI az oldalhoz

A gyorsítótár felépítésével a táblázat nézet a második fél. Ennek magját a tengelysor-elemek (axis line items) alkotják, a pivot törzs azon sorai, amelyek felsorolják a sor-mező és oszlop-mező értékek minden olyan kombinációját, amelyet a táblázat rajzol. Ezek az SXLI rekordokban utaznak (0x00B5 rekordtípus, leírva az [MS-XLS] §2.4.275). Egyetlen SXLI sok sort tartalmazhat, ismét addig, amíg a 8224 bájtos korlát új rekordot nem kényszerít ki, és egy kis tömörítési trükköt használ: minden sor csak azt tárolja, hogy miben tér el a felette lévő sortól, közös előtag-számlálóként (common-prefix count) kifejezve, így egy mélyen egymásba ágyazott tengely nem ismétli meg a külső mezőértékeket minden sorban. A végösszeg sor és a rekordok első sora mindig nullára állítja vissza ezt az előtag-számlálót, így az olvasónak soha nem kell visszanéznie a rekordhatáron túlra a sor rekonstruálásához.

Az oldaltengely, vagyis a kimutatás felett elhelyezkedő szűrő legördülő menük, egy különálló rekord. Az SXPI (0x00B6 rekordtípus, [MS-XLS] §2.4.276) egy tízbájtos bejegyzést hordoz oldalmezőnként: a pivot mezőindexet (isxvd), a kiválasztott gyorsítótár-elemet (iCache), egy pozíciós szót (ipos) és egy örökölt objektum-azonosítót (objId). Az iCache értékre érdemes figyelni. Az az oldalmező, amely "(All)"-t mutat, vagyis semmit sem szűr, az 0x7FFD sentinel értéket tárolja a valós elemindex helyett. A programozottan felépített pivot minden oldalmezőnél "(All)" értékkel nyílik meg, amíg a hívó előre ki nem választ egy elemet, ekkor az adott elem gyorsítótár-indexe felváltja a sentinelt, és az Excel már a szűrő alkalmazásával nyílik meg. E mellett helyezkednek el az egyes mezőket és azok formázását leíró támogató rekordok: SXVD és SXVDEx a mezőnézet definícióihoz, SXIVD a tengelyeket rendező mezőindex listákhoz, valamint az SXFormat a számformázáshoz, és mindegyik visszahivatkozik ugyanarra a gyorsítótárra, amelyet a törzssorok említenek.

Két író egyben: nyers blobok és a gépelt modell

Strukturális oka van annak, hogy a HotXLS két teljesen különálló utat tart fenn a kimutatások írására, és ez közvetlenül a hűség (fidelity) követelményeiből fakad. Amikor egy munkafüzetet beolvasunk a lemezről, a pivot rekordjait az Excel vagy más gyártó írta, és olyan rekordváltozatokat, rendezési sajátosságokat vagy kiterjesztési rekordokat használhatnak, amelyeket egyetlen harmadik féltől származó író sem modellez le teljesen. Az egyetlen biztonságos dolog ezekkel a bájtokkal az, ha változatlanul visszaadjuk őket. Így a fájlból érkező kimutatás a FromRawBlobs = True jelzőt kapja, és mentéskor az író szó szerint lejátsza a megőrzött rekordblobokat. Semmi sem generálódik újra, semmi sem értelmeződik újra, és a megnyitás és mentés közötti oda-vissza út bájtstabil.

A program által felépített kimutatás az ellenkező eset. Itt nincsenek megőrzendő eredeti bájtok, csak a gépelt objektummodell: a TXLSPivotCache a mezőivel és elemlistáival, valamint a TXLSPivotTable a tengely-hozzárendeléseivel. Ez a tábla FromRawBlobs = False jelölést kap, és az író a nehezebb úton szerializálja azt: kibocsát egy friss BOF = 0x0006 gyorsítótár-alfolyamot, becsomagolja az SXDBB indextáblát a gépelt modellben lévő elemindexekből, és elrendezi az SXLI és SXPI rekordokat a tengelykonfiguráció alapján. A flag az, ami lehetővé teszi, hogy mindkét típus együtt létezzen egyetlen munkafüzetben. Enélkül az írónak el kellene dobnia a beolvasott táblák hűségét, vagy meg kellene tagadnia az újak létrehozását. A beolvasott táblában lévő bármely gyártó-specifikus kiterjesztési rekord kiegészítő rekordként megmarad, elérhetővé válva a tábla SupplementalRecords listáján keresztül, így a gépelt modellen keresztül megvizsgált tábla nem veszíti el azokat a részeit, amelyeket a modell nem ír le.

Kimutatás építése kódban

A fenti gépezet egésze egyetlen hívás mögött rejlik. Az AddPivotTable fogadja a forrástartományt A1 jelöléssel, a célcellát, ahol a táblázat bal felső sarka rögzül, valamint egy nevet. Elemzi a tartományt, beolvassa azt a mezőtípusok kikövetkeztetéséhez és a gyorsítótár felépítéséhez (újrafelhasználva a meglévő gyorsítótárat, ha egy másik tábla már ugyanerre a tartományra hivatkozik), és visszaad egy gépelt TXLSPivotTable objektumot, amelyben minden forrásoszlophoz egy-egy mező tartozik, kezdetben minden mező a tengelyen kívül. Ezután elhelyezi a mezőket a tengelyeken, és kiválaszt egy aggregációt. A szignatúra pontosan ilyen, és a gyorsítótár, az SXDBB csomagolás és a nézetrekordok mind elkészülnek Önnek mentéskor.

uses
  lxHandle, lxPivot;

var
  Book : TXLSWorkbook;
  Sheet: IXLSWorkSheet;
  Pivot: TXLSPivotTable;
begin
  Book := TXLSWorkbook.Create;
  try
    Book.Open('Sales.xls');
    Sheet := Book.Sheets[1];

    // Source A1:E500 on 'Data'; anchor the pivot at row 3, col 1.
    Pivot := Sheet.AddPivotTable('Data!$A$1:$E$500', 3, 1, 'SalesByRegion');
    if Pivot <> nil then
    begin
      Pivot.AddRowField('Region');
      Pivot.AddColumnField('Quarter');
      Pivot.AddDataFieldByName('Revenue', xlpaSum);
    end;

    Book.SaveAs('Sales-Pivot.xls');
  finally
    Book.Free;
  end;
end;

A forrástartomány első sorát fejlécként olvassa be a rendszer, amely megnevezi a gyorsítótár-mezőket, így a AddRowField('Region') név alapján egyezik meg egy oszloppal a fejléc szövegén keresztül, nem pedig pozíció szerint. Mivel a visszakapott tábla egy gépelt modell FromRawBlobs = False értékkel, az író a semmiből indított útvonalat választja: felépít egy önálló gyorsítótárat, amely nem függ attól, hogy a forrástartomány a frissítés idején is jelen legyen, ami pontosan az a tulajdonság, amelyet akkor szeretne, ha a kimutatást olyan címzettnek küldi el, aki áthelyezheti vagy törölheti a mögöttes adatokat.

A pivot és gyorsítótár rekordok beolvasása és egyeztetése egy olyan fájlban, amelyet nem Ön állított elő – beleértve a nyers blob megőrzési útvonalat is –, a munkafüzet auditálása és a konverziós munkaasztal bemutatója cikkünkben található. Ha a forrástartomány több tízezer sorból áll, és a SXDBB folyam sok folytatólagos rekordon terül el, a nagy munkafüzet-teljesítményről szóló feljegyzések megakadályozzák, hogy a gyorsítótár felépítése uralja a futásidőt. Mindkettő kapcsolódik a HotXLS spreadsheet component részeként szállított pivot-íróhoz Delphi és C++Builder platformokon, a cella, képlet, diagram és formázási API-k mellett.