Teknisk artikkel

Rammesetting av BIFF PivotCache-poster med HotXLS i Delphi

BIFF PivotCache-substreamen lagrer et PivotTable-cachet datasett separat fra visningen som viser det, og HotXLS leser og skriver den substreamen ved å inspisere postkropper snarere enn å stole på postnummer. Det skillet er hele historien: samme postnummer bærer to inkompatible kroppsoppsett avhengig av hvilken skriver som produserte filen, så leseren bestemmer rammesettingen fra den første postkroppen den ser

Du møter dette laget i det øyeblikket et PivotTable må overleve en rundtur. En pivotvisning uten cachen sin er et skall, og Excel bygger cachen på nytt fra kildeområdet når den åpner filen, noe som er fint helt til kildeområdet er borte, dataene ble limt inn fra en spørring, eller arbeidsboken er en arkivert avslutning som ikke må endre seg når noen åpner den

To strukturer, to steder i filen

De cachete dataene og cachedefinisjonen bor i ulike deler av arbeidsboken, og å blande dem sammen er det første som må bli riktig. De cachete postene danner sin egen substream, gitt i [MS-XLS] §2.1.7.12 som PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Legg merke til hva som er fraværende: det finnes ingen BOF i hodet på den produksjonen

Definisjonen sitter i stedet i arbeidsbokens globaler, som PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), plassert etter formateringspostene og før BoundSheet- og Country-postene. Så en enkelt cache beskrives i to steder som ligger hundrevis av poster fra hverandre, og koblingen mellom dem er en strømidentifikator som må stemme på tre steder samtidig

HotXLS PivotCache-rammesetting i BIFF8: PIVOTCACHEDEFINITION med sin SXStreamID sitter i arbeidsbokens globaler etter formatering og før BoundSheet, mens cachete poster bor i en strøm under _SX_DB_CUR-lageret navngitt med firedisifret store hex som holder SXDB-, SXDBEx-, SXFORMULA-, FDB- og DBB-poster uten BOF, og SXStreamID.idStm, idstm-feltet i SXDB og strømnavnet må stemme
En enkelt pivot-cache beskrives i to steder hundrevis av poster fra hverandre, forent av en strømidentifikator som må stemme i globalene, SXDB-hodet og substream-navnet samtidig

Hver cache hører hjemme i en strøm under _SX_DB_CUR hvis navn er den firedisifre store hexadecimal-stavingen av identifikatoren dens. SXStreamID.idStm, idstm-feltet gjentatt i SXDB-hodet, og det strømnavnet må alle matche. Når du allokerer en ny identifikator, reserver alle tall som allerede er lest fra filen først, ellers kan en ny cache kreve et tall som tilhører en eldre cache leseren ennå ikke har vandret til

Enda en identifikator fanger folk. iCache-verdien i en pivotvisning er den nullbaserte posisjonen til tilhørende SXStreamID i den globale sekvensen, ikke en cache-identifikator du får velge. Ved skriving må den mappes fra cache-objektet til sin faktiske utdataposisjon, og eksisterende visninger må nummereres om sammen med den, ellers peker oppgradering av én cache i stillhet en visning mot en annen

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;     // stol på de cachete postene
    Cache.SaveData := True;

    Field := Cache.AddField('Region', xlpcftString);
    V.ValueType := xlpcftString;
    V.StrValue := 'North';
    Field.FindOrAddItem(V);

    Cache.SetRecordCount(0);          // nullstill, så størrelsesordne postnettet
    Cache.SetRecordCount(500);
    Book.StorePivotCaches;
  finally
    Book.Free;
  end;
end;

Den doble SetRecordCount er ikke overtro. RecordCount er en enkel egenskapsskriving som ikke allokerer, og den interne vekstveien initialiserer bare de nylig tillagte radene, så en cache hvis antall ble satt gjennom hodeveien kan ende opp med et indeksnett med null lengde. Skrivinger til RecordIndices forkastes da uten feil. Å sette antallet til null og tilbake etablerer nettet på nytt, og det må skje etter at hvert felt er lagt til, fordi radbredden kommer fra feltantallet

Hvorfor kan et postnummer ikke fortelle deg kroppsoppsettet?

Fordi postnummer og kroppsoppsett endret seg til ulike tider, er mappingen mellom dem ikke en funksjon. Ett tall i det legacy-settet viser seg bare i filer fra eldre skrivere, noe som gjør det til et pålitelig signal i én retning. Et annet tall er genuint tvetydig: det opptrer både i korrekte filer og i en rekke mellomversjoner som brukte det nye tallet med det gamle kroppsoppsettet

Rammesettingen må derfor bestemmes fra kroppen, og én gang per cache-substream snarere enn per post. HotXLS låser dialekten fra lengden på den første SXDBB-posten i hver substream. I spesifikasjonsrammesettingen holder én SXDBB nøyaktig én cachepost, så lengden dens er lik én radbredde. I den eldre pakkede rammesettingen holder den første posten så mange rader som får plass, så for enhver cache med mer enn én rad er den minst to radbredder. Sammenligningen er avgjørende når som helst de to anslagene skiller seg

HotXLS SXDBB-rammelås: ett postnummer bærer to inkompatible kroppsoppsett, så leseren sammenligner lengden på den første SXDBB-posten mot radbredden, én radbredde låser spesifikasjonsdialekten mens to eller flere radbredder låser den legacy pakkede rammesettingen, likheter tar spesifikasjonslesningen, og dialekten låses én gang per cache-substream, ikke per post
Postnummer kan ikke bestemme kroppsoppsett fordi de to endret seg til ulike tider, så HotXLS låser dialekten én gang per substream fra den første SXDBB-lengden og tar spesifikasjonslesningen ved likhet

Når de ikke skiller seg, tar leseren spesifikasjonslesningen, etter prinsippet om at filer skrevet av Excel overteller filer skrevet av et mellombygg. Den blinde flekken er smal av konstruksjon, og når den oppstår, spiller filen seg selv fortsatt av byte for byte. Bare de typede indeksene eksponert mot kallere påvirkes

Indeksbredden bor i en annen post

SXDBB (§2.4.276) bærer én indeks per cachefelt hvis distinct-value-flagg er satt, i feltrekkefølge, og bredden på hver indeks bestemmes et annet sted: den tilhørende SXFDB-feltposten (§2.4.283) deklarerer et short-items-flagg, og det flagget sier om indeksen opptar to byte eller én. To poster, én implisitt kontrakt, og én eneste setning i spesifikasjonen som forbinder dem

Den koblingen er nøyaktig der en hjemmelaget koding går galt. En tidligere HotXLS-skriver pakket hvert felt inn i minimum antall biter, med fyll til en bytegrense mellom rader, noe som er forsvarlig isolert sett og direkte motsier bredden samme skriver nettopp hadde deklarert i SXFDB. Et felt med tre distinkte verdier ble beskrevet som én byte bred i én post og opptok to biter i den andre. Fiksen var ikke å korrigere aritmetikken, men å løfte ut breddebeslutningen til én funksjon som begge emitterne kaller, slik at de to postene ikke lenger kan drive fra hverandre. Det er samme klasse av defekt beskrevet i BIFF-postlengde-deklarasjonsdrift, der en deklarert størrelse og en faktisk kropp skilles lag

Konsekvensen av å ikke lese disse postene i det hele tatt, er verdt å stave ut, for den er lett å undervurdere. Da leseren hoppet over postindeksene, rapporterte hver cache lastet fra en fil indeks null for hvert felt i hver rad, noe som betyr at hver rad pekte på den første verdien av hvert felt. Det er ikke bare redusert introspeksjon: pivot-evalueringsveien og cache-til-celle-fyllveien konsumerer begge det nettet. Og en rundturstest kan ikke oppdage det, for en cache fortsatt på rå avspilling skrives tilbake fra sine opprinnelige byte

// Opphavsflagg forteller deg hva du holder og hva som kan skrives om
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-emittering er bare tapsfri når hver post har en modell her
  if Cache.CanUpgradeFraming then
    Writeln('safe to rewrite with the current emitters');
end;

Når er omskriving av en cache tapsfri?

Bare når tre betingelser holder samtidig, og CanUpgradeFraming er den eneste egenskapen som svarer på spørsmålet. Cachen må fortsatt være på rå avspilling, substreamen må være i en av rammesettingene dette biblioteket tidligere skrev feil, og leseren må ha bygget en fullstendig typet modell av hver post i den. En cache Excel skrev, kvalifiserer aldri, for substreamen dens bærer poster HotXLS ikke har noen modell for, og re-emittering fra modellen ville miste dem

Fullstendighetstesten er strengere enn den først fremstår. En post leseren bare beholdt som opake byte, markerer modellen som ufullstendig. Det gjør også et deklarert antall formelposter som emitteren ikke kan reprodusere, for re-emittering ville omskrive en deklarasjon av flere formelposter til en deklarasjon av ingen, og en verdi i filen som ikke kan reproduseres, er ekvivalent med en post som ikke kan reproduseres

Bevisst konservatisme løper gjennom skriveren også. Indekser klemmes inn i det lovlige området i stedet for å kodes som en out-of-band-sentinel, fordi spesifikasjonen definerer en indeks inn i distinct-value-sekvensen og ingenting annet, og en tom celle er selv en verdi i den sekvensen. En cachepostkropp som overskrider BIFF-posttaket, skrives ikke i det hele tatt, noe som ville kreve tusenvis av cachefelter og er uoppnåelig innenfor BIFF8-kolonnegrensen uansett; fallbacken er at Excel oppfrisker fra kildeområdet, noe som er definert atferd snarere enn en korrupt fil

Datoer bærer den siste tverrpost-avhengigheten. Konverteringen fra serienummer til dato avhenger av arbeidsbokens datosystem, og postemitteren kan ikke se arbeidsboken, så basisdatovalget sendes inn som en parameter som standarder til 1900-systemet og leveres av lagringsveien på arbeidsboknivå. Under 1900-systemet er serienummeret verdien direkte; 1904-systemet skiller seg med 1462 dager. Den bredere behandlingen av datoserier er i dateserier, 1904-systemet og tallformater

Hvis du jobber på visningslaget snarere enn cachelaget, er postene som beskriver den synlige pivoten dekket i BIFF8 PivotTable-postsettet, og atferden på beregningssiden i beregnete felt, beregnede elementer og oppfriskning. Alle tre lag følger med i HotXLS Delphi regnearkkomponent, noe som er det som gjør det mulig å laste en legacy-arbeidsbok, inspisere hva cachen faktisk inneholder, og avgjøre om omskriving av den er trygg før du gjør det