Substream-ul BIFF PivotCache stochează setul de date din cache al unui PivotTable separat de vederea care îl afișează, iar HotXLS citește și scrie substream-ul acela inspectând corpurile înregistrărilor, nu având încredere în numerele de înregistrare. Distincția aceasta e întreaga poveste: același număr de înregistrare poartă două aranjamente de corp incompatibile, în funcție de ce scriitor a produs fișierul, deci cititorul decide încadrarea din primul corp de înregistrare pe care îl vede
Ajungeți la stratul acesta în momentul în care un PivotTable trebuie să supraviețuiască unui du-te-vino. O vedere pivot fără cache-ul ei e o coajă goală, iar Excel va reconstrui cache-ul din intervalul sursă când deschide fișierul, ceea ce merge foarte bine până în punctul în care intervalul sursă a dispărut, datele au fost lipite dintr-o interogare sau registrul de lucru e o închidere arhivată care nu are voie să se schimbe când cineva îl deschide
Două structuri, două locuri în fișier
Datele din cache și definiția cache-ului trăiesc în părți diferite ale registrului de lucru, iar a le amesteca e primul lucru de evitat. Înregistrările din cache formează propriul lor substream, dat în [MS-XLS] §2.1.7.12 ca PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Observați ce lipsește: nu există niciun BOF la capul acelei producții
Definiția stă în schimb în globurile registrului de lucru, ca PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), poziționată după înregistrările de formatare și înainte de înregistrările BoundSheet și Country. Deci un singur cache e descris în două locuri aflate la sute de înregistrări distanță, iar legătura dintre ele e un identificator de stream care trebuie să fie de acord în trei locuri deodată
Fiecare cache aparține într-un stream sub _SX_DB_CUR al cărui nume e scrierea hexazecimală cu patru cifre și majuscule a identificatorului lui. SXStreamID.idStm, câmpul idstm repetat în antetul SXDB și numele acela de stream trebuie toate să se potrivească. Când alocați un identificator nou, rezervați mai întâi fiecare număr deja citit din fișier, altfel un cache nou poate revendica un număr care aparține unui cache mai vechi, până la care cititorul nu a ajuns încă
Încă un identificator îi prinde pe oameni. Valoarea iCache dintr-o vedere pivot e poziția cu bază zero a SXStreamID-ului corespunzător în secvența globală, nu un identificator de cache pe care îl alegeți. La scriere trebuie mapată din obiectul cache la poziția lui efectivă de ieșire, iar vederea existentă trebuie renumerotată odată cu ea, altfel upgrade-ul unui cache îndreaptă tăcut o vedere spre un alt cache
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; // ai încredere în înregistrările din cache
Cache.SaveData := True;
Field := Cache.AddField('Region', xlpcftString);
V.ValueType := xlpcftString;
V.StrValue := 'North';
Field.FindOrAddItem(V);
Cache.SetRecordCount(0); // curăță, apoi dimensionează grila de înregistrări
Cache.SetRecordCount(500);
Book.StorePivotCaches;
finally
Book.Free;
end;
end;
Dublul SetRecordCount nu e superstiție. RecordCount e o simplă scriere de proprietate care nu alocă, iar calea internă de creștere inițializează doar rândurile nou adăugate, deci un cache al cărui contor a fost stabilit pe calea antetului poate ajunge cu o grilă de index de lungime zero. Scrierile în RecordIndices sunt apoi aruncate fără eroare. Stabilirea contorului pe zero și înapoi reînființează grila, și trebuie să se întâmple după ce fiecare câmp a fost adăugat, pentru că lățimea rândului vine din numărul de câmpuri
De ce nu îți poate spune un număr de înregistrare aranjamentul corpului?
Pentru că numerele de înregistrare și aranjamentele de corp s-au schimbat în momente diferite, deci maparea dintre ele nu e o funcție. Un număr din setul legacy apare doar în fișiere de la scriitori mai vechi, ceea ce îl face un semnal fiabil într-un singur sens. Alt număr e cu adevărat ambiguu: apare atât în fișiere corecte, cât și într-o gamă de versiuni intermediare care foloseau numărul nou cu aranjamentul de corp vechi
Încadrarea trebuie decisă deci din corp, o dată per substream de cache, nu per înregistrare. HotXLS fixează dialectul din lungimea primei înregistrări SXDBB din fiecare substream. În încadrarea specificației, un SXDBB ține exact o înregistrare de cache, deci lungimea lui egalează o lățime de rând. În încadrarea legacy împachetată, prima înregistrare ține atâtea rânduri câte încap, deci pentru orice cache cu mai mult de un rând ea e de cel puțin două lățimi de rând. Comparația e decisivă ori de câte ori cele două predicții diferă
Când nu diferă, cititorul ia lectura specificației, pe principiul că fișierele scrise de Excel le depășesc pe cele scrise de un build intermediar. Punctul acela orb e îngust prin construcție și, când totuși apare, fișierul în sine se redă în continuare octet cu octet. Sunt afectați doar indecșii tipizați expuși apelanților
Lățimea indexului stă într-o altă înregistrare
SXDBB (§2.4.276) poartă un index per câmp de cache cu fanionul de valori distincte setat, în ordinea câmpurilor, iar lățimea fiecărui index se decide în altă parte: înregistrarea de câmp SXFDB corespunzătoare (§2.4.283) declară un fanion de elemente scurte, iar fanionul acela spune dacă indexul ocupă doi octeți sau unul. Două înregistrări, un contract implicit și o singură frază în specificație care le leagă
Cuplarea aceea e exact locul în care un encodare de casă greșește. Un scriitor HotXLS mai vechi împacheta fiecare câmp în numărul minim de biți, umplând până la o graniță de octet între rânduri, ceea ce e apărabil în izolare și contrazice direct lățimea pe care același scriitor o declarase abia în SXFDB. Un câmp cu trei valori distincte era descris cu o lățime de un octet într-o înregistrare și ocupa doi biți în cealaltă. Remedierea n-a fost corectarea aritmeticii, ci extragerea deciziei de lățime într-o singură funcție apelată de ambii emițători, astfel încât cele două înregistrări să nu mai poată deriva una de la alta. E aceeași clasă de defect descrisă în deriva declarațiilor de lungime de înregistrare BIFF, unde o mărime declarată și un corp efectiv se despart
Consecința de a nu citi deloc aceste înregistrări merită enunțată, pentru că e ușor de subestimat. Când cititorul sărea indecșii de înregistrare, fiecare cache încărcat dintr-un fișier raporta indexul zero pentru fiecare câmp al fiecărui rând, adică fiecare rând arăta spre prima valoare a fiecărui câmp. Nu e doar introspecție redusă: atât calea de evaluare pivot, cât și calea de umplere cache-celulă consumă grila aceea. Iar un test de du-te-vino nu o poate detecta, pentru că un cache aflat încă pe redarea brută se scrie înapoi din octeții lui originali
// Fanioanele de proveniență îți spun ce ții în mână și ce poate fi rescris
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);
// Reemiterea e fără pierderi doar când fiecare înregistrare are un model aici
if Cache.CanUpgradeFraming then
Writeln('safe to rewrite with the current emitters');
end;
Când e rescrierea unui cache fără pierderi?
Doar când trei condiții se țin împreună, iar CanUpgradeFraming e singura proprietate care răspunde la întrebare. Cache-ul trebuie să fie încă pe redare brută, substream-ul trebuie să fie într-una dintre încadrările pe care această bibliotecă le-a scris anterior incorect, iar cititorul trebuie să fi construit un model tipizat complet al fiecărei înregistrări din el. Un cache scris de Excel nu se califică niciodată, pentru că substream-ul lui poartă înregistrări pentru care HotXLS nu are model, iar reemiterea din model le-ar pierde
Testul de completitudine e mai strict decât pare la prima vedere. O înregistrare pe care cititorul a ținut-o doar ca octeți opaci marchează modelul incomplet. La fel și un contor declarat de înregistrări de formule pe care emițătorul nu le poate reproduce, pentru că reemiterea ar rescrie o declarație de câteva înregistrări de formule într-o declarație de niciuna, iar o valoare din fișier care nu poate fi reprodusă echivalează cu o înregistrare care nu poate fi reprodusă
Conservatorismul deliberat străbate și scriitorul. Indecșii sunt strânși în intervalul legal, nu encodați ca un sentinele în afara benzii, pentru că specificația definește un index în secvența valorilor distincte și nimic altceva, iar o celulă goală e ea însăși o valoare în secvența aceea. Un corp de înregistrare de cache care depășește plafonul înregistrărilor BIFF nu se scrie deloc, ceea ce ar cere mii de câmpuri de cache și e oricum de neatins în limita de coloane BIFF8; rezerva e că Excel reîmprospătează din intervalul sursă, comportament definit, nu un fișier corupt
Datele poartă ultima dependență între înregistrări. Conversia serial-la-dată depinde de sistemul de date al registrului de lucru, iar emițătorul de înregistrări nu poate vedea registrul de lucru, deci alegerea datei de bază se transmite ca un parametru care are implicit sistemul 1900 și e furnizată de calea de salvare la nivel de registru de lucru. Sub sistemul 1900 numărul serial e valoarea directă; sistemul 1904 diferă cu 1462 de zile. Tratarea mai largă a serialelor de dată stă în serialele de dată, sistemul 1904 și formatele de număr
Dacă lucrați la stratul de vedere, nu la cel de cache, înregistrările care descriu pivotul vizibil sunt acoperite în setul de înregistrări PivotTable BIFF8, iar comportamentul din partea calculului în câmpurile calculate, elementele calculate și refresh. Toate cele trei straturi se livrează în HotXLS Delphi spreadsheet component, ceea ce face posibil să încărcați un registru de lucru legacy, să inspectați ce conține efectiv cache-ul lui și să decideți dacă rescrierea lui e sigură înainte să o faceți