Teknisk artikel

Framing af BIFF PivotCache-records med HotXLS i Delphi

BIFF PivotCache substream gemmer et PivotTable cached dataset separat fra den visning, der viser det, og HotXLS læser og skriver det substream ved at inspicere record bodies snarere end at stole på recordnumre. Den skelnen er hele historien: samme recordnummer bærer to inkompatible body-layouts afhængigt af, hvilken writer der producerede filen, så readeren afgør framen ud fra den første record body, den ser

Du møder dette lag i det øjeblik, et PivotTable skal overleve en round trip. En pivotvisning uden sin cache er en skal, og Excel genbygger cachen fra source range, når den åbner filen, hvilket er fint helt frem til det punkt, hvor source range er væk, dataene var indsat fra en forespørgsel, eller projektmappen er et arkiveret luk, der ikke må ændres, når nogen åbner den

To strukturer, to steder i filen

De cachede data og cachedefinitionen bor i forskellige dele af projektmappen, og at blande dem sammen er det første, der skal sidde rigtigt. De cachede records danner deres eget substream, angivet i [MS-XLS] §2.1.7.12 som PIVOTCACHE = SXDB SXDBEx *SXFORMULA *FDB *DBB EOF. Læg mærke til, hvad der er fraværende: der er ingen BOF i hovedet på den produktion

Definitionen sidder i stedet i workbook globals, som PIVOTCACHEDEFINITION = SXStreamID SXVS [SXSRC] [SXADDLCACHE] (§2.1.7.20.3), placeret efter formatteringsrecordene og før BoundSheet- og Country-recordene. Så en enkelt cache beskrives to steder, der ligger hundredvis af records fra hinanden, og lænken mellem dem er en stream-identifikator, der skal være enig tre steder på én gang

HotXLS PivotCache framing i BIFF8: PIVOTCACHEDEFINITION med sin SXStreamID sidder i workbook globals efter formattering og før BoundSheet, mens cachede records bor i en stream under _SX_DB_CUR storage navngivet med firecifret versal hex, der indeholder SXDB-, SXDBEx-, SXFORMULA-, FDB- og DBB-records uden BOF, og SXStreamID.idStm, SXDB idstm-feltet og stream-navnet skal være enige
Én enkelt pivot-cache beskrives to steder hundredvis af records fra hinanden, forbundet af en stream-identifikator, der skal være enig i globals, SXDB-headeren og substream-navnet på én gang

Hver cache hører hjemme i en stream under _SX_DB_CUR, hvis navn er den firecifrede versal hexadecimalstafning af dens identifikator. SXStreamID.idStm, idstm-feltet gentaget i SXDB-headeren, og det stream-navn skal alle matche. Når du allokerer en ny identifikator, så reservér først hvert tal, der allerede er læst fra filen, ellers kan en ny cache gøre krav på et tal, der tilhører en ældre cache, readeren endnu ikke er vandret til

Én identifikator mere fanger folk. iCache-værdien i en pivotvisning er den nul-baserede position af den tilsvarende SXStreamID i den globale sekvens, ikke en cache-identifikator, du får lov at vælge. Ved skrivning skal den mappes fra cache-objektet til dets faktiske outputposition, og eksisterende visninger skal omnummereres sammen med den, ellers peger opgradering af én cache lydløst en visning på en anden

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 cachede records
    Cache.SaveData := True;

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

    Cache.SetRecordCount(0);          // nulstil, og dimensionér så recordgitteret
    Cache.SetRecordCount(500);
    Book.StorePivotCaches;
  finally
    Book.Free;
  end;
end;

Den dobbelte SetRecordCount er ikke overtro. RecordCount er en almindelig property-skrivning, der ikke allokerer, og den interne vækstvej initialiserer kun de nyligt tilføjede rækker, så en cache, hvis antal blev sat gennem header-vejen, kan ende med et nollængde indeksgrid. Skrivninger til RecordIndices kasseres derefter uden fejl. At sætte antallet til nul og tilbage genopretter gitteret, og det skal ske, efter hvert felt er tilføjet, for rækkebredden kommer fra feltantallet

Hvorfor kan et recordnummer ikke fortælle dig body-layoutet?

Fordi recordnumre og body-layouts ændredes på forskellige tidspunkter, så afbildningen mellem dem ikke er en funktion. Ét tal i legacy-sættet optræder kun i filer fra ældre writers, hvilket gør det til et pålideligt signal i én retning. Et andet tal er reelt tvetydigt: det optræder både i korrekte filer og i en række mellemversioner, der brugte det nye tal med det gamle body-layout

Framingen må derfor afgøres ud fra bodyen, og én gang per cache substream snarere end per record. HotXLS latcher dialecten ud fra længden af den første SXDBB-record i hvert substream. I specifikationsframen holder én SXDBB præcis én cache-record, så dens længde er lig én rækkebredde. I den ældre packede frame holder den første record så mange rækker, som der er plads til, så for enhver cache med mere end én række er den mindst to rækkebredder. Sammenligningen er afgørende, når de to forudsigelser adskiller sig

HotXLS SXDBB framing-latch: ét recordnummer bærer to inkompatible body-layouts, så readeren sammenligner den første SXDBB-recordlængde med rækkebredden, én rækkebredde latcher specifikationsdialecten, mens to eller flere rækkebredder latcher den legacyede packede frame, lige tilfælde tager specifikationsaflæsningen, og dialecten latches én gang per cache substream, ikke per record
Recordnumre kan ikke afgøre body-layout, for de to ændredes på forskellige tidspunkter, så HotXLS latcher dialecten én gang per substream ud fra den første SXDBB-længde og tager specifikationsaflæsningen ved uafgjort

Når de ikke adskiller sig, tager readeren specifikationsaflæsningen efter princippet om, at filer skrevet af Excel overdøver filer skrevet af en mellembygning. Det blinde vinkel er smalt af konstruktion, og når det opstår, spiller filen selv stadig af byte for byte. Kun de typede indekser, der eksponeres for kaldere, påvirkes

Indeksbredden bor i en anden record

SXDBB (§2.4.276) bærer ét indeks per cache-felt, hvis distinct-value-flag er sat, i feltrækkefølge, og bredden af hvert indeks besluttes et andet sted: den tilsvarende SXFDB-feltrecord (§2.4.283) deklarerer et short-items-flag, og det flag siger, om indekset optager to byte eller én. To records, én implicit kontrakt og en enkelt sætning i specifikationen, der forbinder dem

Den kobling er præcis der, hvor en hjemmelavet kodning tager fejl. En tidligere HotXLS writer pakkede hvert felt i det minimale antal bit med padding til en bytegrænse mellem rækker, hvilket er forsvarligt isoleret set og direkte i modstrid med den bredde, samme writer lige havde deklareret i SXFDB. Et felt med tre distinkte værdier var beskrevet som én byte bred i den ene record og optog to bit i den anden. Fixet var ikke at korrigere aritmetikken, men at udtrække breddebeslutningen til én funktion, som begge emitere kalder, så de to records ikke længere kan drive fra hinanden. Det er samme klasse af defekt beskrevet i BIFF record length declaration drift, hvor en deklareret størrelse og en faktisk body skilles ad

Konsekvensen af slet ikke at læse disse records er værd at stave ud, for den er let at undervurdere. Da readeren skipede recordindeksene, meldte enhver cache indlæst fra en fil indeks nul for hvert felt i hver række, hvilket betyder, at hver række pegede på den første værdi af hvert felt. Det er ikke blot reduceret introspektion: pivot-evalueringsvejen og cache-til-celle-udfyldningsvejen indtager begge det gitter. Og en round-trip-test kan ikke opdage det, for en cache stadig på raw replay skrives tilbage fra sine originale byte

// Provenance-flags fortæller dig, hvad du holder, og hvad der må omskrives
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 kun lossless, når hver record har en model her
  if Cache.CanUpgradeFraming then
    Writeln('safe to rewrite with the current emitters');
end;

Hvornår er omskrivning af en cache lossless?

Kun når tre betingelser holder sammen, og CanUpgradeFraming er den eneste egenskab, der besvarer spørgsmålet. Cachen skal stadig være på raw replay, substreamet skal være i én af de frames, dette bibliotek tidligere skrev forkert, og readeren skal have bygget en komplet typet model af hver record i den. En cache, Excel skrev, kvalificerer aldrig, for dens substream bærer records, HotXLS ingen model har for, og re-emittering fra modellen ville droppe dem

Kompletshedstesten er strengere, end den først ser ud. En record, readeren kun beholdt som opake byte, markerer modellen ufuldstændig. Det gør også et deklareret antal formel-records, som emitteren ikke kan reproducere, for re-emittering ville omskrive en deklaration af flere formel-records til en deklaration af ingen, og en værdi i filen, der ikke kan reproduceres, er ækvivalent med en record, der ikke kan reproduceres

Bevidst konservatisme løber gennem writeren også. Indekser clampes ind i det lovlige område i stedet for at blive kodet som en out-of-band sentinel, for specifikationen definerer et indeks ind i distinct-value-sekvensen og intet andet, og en tom celle er selv en værdi i den sekvens. En cache-record body, der overstiger BIFF-recordloftet, skrives slet ikke, hvilket ville behøve tusindvis af cache-felter og alligevel er uopnåeligt inden for BIFF8-kolonnegrænsen; fallback er, at Excel refresher fra source range, hvilket er defineret adfærd snarere end en korrupt fil

Datoer bærer den sidste cross-record-afhængighed. Serial-til-dato-konverteringen afhænger af projektmappens datesystem, og record-emitteren kan ikke se projektmappen, så base-datovalget sendes ind som en parameter, der som default er 1900-systemet og leveres af save-vejen på projektmappeniveau. Under 1900-systemet er serienummeret værdien direkte; 1904-systemet adskiller sig med 1462 dage. Den bredere behandling af dato-serials er i dato-serials, 1904-systemet og talformater

Arbejder du i visningslaget snarere end cache-laget, er de records, der beskriver den synlige pivot, dækket i BIFF8 PivotTable-record-sættet, og beregningssidens adfærd i beregnede felter, beregnede elementer og refresh. Alle tre lag følger med i HotXLS Delphi spreadsheet-komponenten, hvilket er det, der gør det muligt at indlæse en legacy-projektmappe, inspicere, hvad dens cache reelt indeholder, og beslutte, om omskrivning er sikker, før du gør det