Articol tehnic

Încadrarea înregistrărilor PivotCache BIFF cu HotXLS

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ă

Încadrarea PivotCache HotXLS în BIFF8: PIVOTCACHEDEFINITION cu SXStreamID-ul ei stă în globurile registrului de lucru după formatare și înainte de BoundSheet, în timp ce înregistrările din cache trăiesc într-un stream sub stocarea _SX_DB_CUR numită cu hexazecimal de patru cifre cu majuscule, ținând înregistrările SXDB, SXDBEx, SXFORMULA, FDB și DBB fără BOF, iar SXStreamID.idStm, câmpul idstm din SXDB și numele stream-ului trebuie să fie de acord
Un singur cache pivot e descris în două locuri aflate la sute de înregistrări distanță, unite printr-un identificator de stream care trebuie să fie de acord deodată în globuri, în antetul SXDB și în numele substream-ului

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ă

Fixarea încadrării SXDBB în HotXLS: un număr de înregistrare poartă două aranjamente de corp incompatibile, deci cititorul compară lungimea primei înregistrări SXDBB cu lățimea rândului, o lățime de rând fixează dialectul specificației, două sau mai multe lățimi de rând fixează încadrarea legacy împachetată, egalitățile iau lectura specificației, iar dialectul se fixează o dată per substream de cache, nu per înregistrare
Numerele de înregistrare nu pot decide aranjamentul corpului pentru că cele două s-au schimbat în momente diferite, deci HotXLS fixează dialectul o dată per substream, din prima lungime SXDBB, și ia lectura specificației la egalitate

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