BIFF PivotCache substream ukládá cached dataset PivotTable odděleně od view, které ho zobrazuje, a HotXLS ten substream čte i zapisuje prohlížením těl záznamů, místo aby věřil číslům záznamů. Tohle rozlišení je celý příběh: týž number záznamu nese dvě nekompatibilní rozložení těla podle toho, který writer soubor vyprodukoval, takže reader rozhodne rámování z prvního těla záznamu, které uvidí
Na tuhle vrstvu narazíte v okamžiku, kdy má PivotTable přežít cestu tam a zpět. Pivot view bez své cache je obal a Excel cache znovu postaví ze zdrojového rozsahu, až soubor otevře, což je v pořádku přesně do chvíle, kdy zdrojový rozsah zmizel, data se vložila z dotazu nebo je sešit archivovaná uzávěrka, která se nesmí změnit, když si ji někdo otevře
Dvě struktury, dvě místa v souboru
Cached data i definice cache bydlí v různých částech sešitu a slepit je dohromady je první věc, kterou musíte dát správně. Cached záznamy tvoří vlastní substream, uvedený v [MS-XLS] §2.1.7.12 jako PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Všimněte si, co tam chybí: na hlavě té produkce není žádný BOF
Definice místo toho sedí v workbook globals, jako PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), umístěná po formátovacích záznamech a před záznamy BoundSheet a Country. Jediná cache je tedy popsána na dvou místech, která dělí stovky záznamů, a vazbou mezi nimi je stream identifikátor, který musí souhlasit na třech místech najednou
Každá cache patří do streamu pod _SX_DB_CUR, jehož jméno je čtyřmístný velkými písmeny psaný hexadecimální zápis jejího identifikátoru. SXStreamID.idStm, pole idstm zopakované v headeru SXDB, a tohle jméno streamu se musí všechna schodovat. Když přidělujete nový identifikátor, nejdřív si rezervujte všechna čísla už načtená ze souboru, jinak si nová cache může nárokovat číslo, které patří starší cache, ke které se reader ještě neprošel
Ještě jeden identifikátor lidi chytá. Hodnota iCache v pivot view je pozice odpovídajícího SXStreamID v globální sekvenci od nuly, ne identifikátor cache, který si vyberete. Při zápisu se musí namapovat z cache objektu na jeho skutečnou výstupní pozici a stávající view se musí přečíslovat spolu s ním, jinak upgrade jedné cache tiše nasměruje view na jinou
var
Book: TXLSWorkbook;
Cache: TXLSPivotCache;
Field: TXLSPivotCacheField;
V: TXLSPivotCacheValue;
begin
Book := TXLSWorkbook.Create(nil);
try
Book.LoadFromFile('sales.xls');
Cache := Book.PivotCaches.Add;
Cache.SourceRangeSheet := 'Data';
Cache.SourceFirstRow := 1; Cache.SourceFirstCol := 1;
Cache.SourceLastRow := 500; Cache.SourceLastCol := 6;
Cache.SourceDataType := 1; // SXVS SHEET, MS-XLS 2.4.317
Cache.RefreshOnLoad := False; // věřte cached záznamům
Cache.SaveData := True;
Field := Cache.AddField('Region', xlpcftString);
V.ValueType := xlpcftString;
V.StrValue := 'North';
Field.FindOrAddItem(V);
Cache.SetRecordCount(0); // vynulovat, pak dimenzovat grid záznamů
Cache.SetRecordCount(500);
Book.StorePivotCaches;
finally
Book.Free;
end;
end;
Dvojitý SetRecordCount není pověra. RecordCount je obyčejný zápis vlastnosti, který nic nealokuje, a interní růstová cesta inicializuje jen nově přidané řádky, takže cache, jejíž count byl nastaven přes header cestu, může skončit s nulově dlouhým index gridem. Zápisy do RecordIndices se pak zahodí bez chyby. Nastavit count na nulu a zase zpět grid obnoví a musí se to stát poté, co byl přidán každý field, protože šířka řádku pochází z počtu fields
Proč vám číslo záznamu neřekne rozložení těla?
Protože čísla záznamů a rozložení těl se měnila v různou dobu, takže mapování mezi nimi není funkce. Jedno číslo ze staré sady se objevuje jen v souborech starších writerů, což z něj dělá spolehlivý signál jedním směrem. Jiné číslo je skutečně nejednoznačné: objevuje se jak ve správných souborech, tak v rozsahu mezilehlých verzí, které používaly nové číslo se starým rozložením těla
Rámování se proto musí rozhodnout z těla, a to jednou za cache substream, ne za záznam. HotXLS zachytí dialekt z délky prvního záznamu SXDBB v každém substreamu. V rámování specifikace drží jeden SXDBB přesně jeden cache záznam, takže jeho délka se rovná jedné šířce řádku. Ve starším packed rámování drží první záznam tolik řádků, kolik se vešlo, takže pro jakoukoli cache s víc než jedním řádkem je aspoň dvě šířky řádku. Srovnání je rozhodné, kdykoli se obě predikce rozcházejí
Neliší-li se, bere reader čtení specifikace, na principu, že soubory zapsané Excelem početně převažují nad soubory zapsanými mezilehlým buildem. Tenhle slepý bod je konstrukcí úzký a kdykoli nastane, soubor sám se dál přehraje bajt za bajt. Dotčeny jsou jen typované indexy vystavené volajícím
Šířka indexu bydlí v jiném záznamu
SXDBB (§2.4.276) nese jeden index na každý cache field, který má nastavený distinct-value flag, v pořadí fields, a šířka každého indexu se rozhoduje jinde: odpovídající field záznam SXFDB (§2.4.283) deklaruje short-items flag a tenhle flag říká, zda index okupuje dva bajty, nebo jeden. Dva záznamy, jedna implicitní smlouva a jediná věta ve specifikaci, která je spojuje
Přesně v téhle vazbě se rozchází domácí encoding. Starší writer HotXLS balil každý field do minimálního počtu bitů s doplněním na byte hranici mezi řádky, což je v izolaci obhajitelné a přímo odporuje šířce, kterou tentýž writer před chvílí deklaroval v SXFDB. Field se třemi distinct hodnotami byl v jednom záznamu popsán jako široký jeden bajt a v druhém okupoval dva bity. Oprava nebyla spravit aritmetiku, ale vytknout rozhodnutí o šířce do jedné funkce, kterou volají oba emitery, takže se dva záznamy už nemůžou rozejít. To je táž třída defektů popsaná v článku drift deklarace délky BIFF záznamu, kde se deklarovaná velikost a skutečné tělo rozejdou
Důsledek toho nečíst tyhle záznamy vůbec stojí za vyložení, protože podcenit se dá snadno. Když reader záznamové indexy přeskočil, hlásila každá cache načtená ze souboru index nula pro každý field každého řádku, což znamená, že každý řádek mířil na první hodnotu každého fieldu. Nejde jen o zredukovanou introspekci: pivot vyhodnocovací cesta i cesta plnění cache do buněk konzumují oba tenhle grid. A round-trip test to neodhalí, protože cache pořád na raw replay se zapisuje zpět ze svých původních bajtů
// Provenance flagy vám řeknou, co držíte a co smí být přepsáno
if Cache.FromRawBlobs then
begin
Writeln('stream id : ', IntToHex(Cache.StreamId, 4));
Writeln('legacy framing : ', Cache.RawFramingIsLegacy);
Writeln('own storage : ', Cache.RawHasStorageStream);
Writeln('model complete : ', Cache.RawModelIsComplete);
// Re-emitování je bezestrátové jen tehdy, když má tu model každý záznam
if Cache.CanUpgradeFraming then
Writeln('safe to rewrite with the current emitters');
end;
Kdy je přepsání cache bezestrátové?
Jen když spolu drží tři podmínky, a CanUpgradeFraming je jediná vlastnost, která na tu otázku odpovídá. Cache musí být pořád na raw replay, substream musí být v jednom z rámování, které ta knihovna dřív zapsala špatně, a reader musel postavit kompletní typovaný model každého záznamu v něm. Cache zapsaná Excelem se nekvalifikuje nikdy, protože její substream nese záznamy, na které HotXLS nemá model, a re-emitování z modelu by je upustilo
Test úplnosti je přísnější, než na první pohled vypadá. Záznam ponechaný jen jako neprůhledné bajty model označí za neúplný. Totéž udělá deklarovaný počet formula záznamů, které emitter nedokáže reprodukovat, protože re-emitování by přepsalo deklaraci několika formula záznamů na deklaraci žádného; hodnota v souboru, kterou nelze reprodukovat, je totéž co záznam, který nelze reprodukovat
Záměrný konzervatismus protéká i writerem. Indexy se clampují do legálního rozsahu místo kódování jako out-of-band sentinel, protože specifikace definuje index do sekvence distinct hodnot a nic víc a prázdná buňka je sama hodnotou v té sekvenci. Tělo cache záznamu překračující strop BIFF záznamu se nezapisuje vůbec, což by vyžadovalo tisíce cache fields a v rámci limitu sloupců BIFF8 je to tak jako tak nedosažitelné; fallback je, že Excel refreshne ze zdrojového rozsahu, což je definované chování, ne corrupt file
Datumy nesou poslední cross-záznamovou závislost. Konverze date serialu na datum závisí na datovém systému sešitu a záznamový emitter sešit nevidí, takže volba base date se předává jako parametr, který defaultuje na systém 1900 a dodává ho cesta zápisu na úrovni sešitu. Pod systémem 1900 je serial číslo hodnotou přímo; systém 1904 se liší o 1462 dní. Širší zacházení s date serialy je v článku date serialy, systém 1904 a number formáty
Pracujete-li na vrstvě view místo vrstvy cache, záznamy popisující viditelný pivot pokrývá článek sada záznamů BIFF8 PivotTable a chování na straně výpočtu článek calculated fields, calculated items a refresh. Všechny tři vrstvy se dodávají v HotXLS Delphi spreadsheet komponentě, což je to, co umožňuje načíst legacy sešit, prozkoumat, co jeho cache doopravdy obsahuje, a rozhodnout, zda je přepsání bezpečné, dřív než ho uděláte