Teknisk artikel

Læs Excels cachede formelværdier i Delphi uden genberegning

HotXLS, det native Delphi- og C++Builder-Excelbibliotek, læser den værdi, Excel allerede har gemt ved siden af en formel, gennem TryGetCachedFormulaValue og IXLSFormulaCacheReader. Ingen af indgangspunkterne kalder lommeregneren, dekompilerer formeltokens, opdaterer dirty-tilstand eller skriver noget tilbage i modellen, så en arbejdsbog, du kun læser, forbliver præcis, som du åbnede den

Scenariet, der driver dette, er kedeligt og ekstremt almindeligt. Et natligt job åbner et par hundrede arbejdsbøger produceret af andre, trækker én kolonne af totaler ud af hver og skubber tallene ind i et data warehouse. Totalerne ligger allerede i filerne — Excel beregnede dem og gemte dem. Alligevel, i det øjeblik jobbet beder en formelcelle om sin værdi, bygger et bibliotek, der kun har ét svar på det spørgsmål, en afhængighedsgraf og evaluerer hele arket, og et job, der burde være I/O-bundet, forvandles til en beregningsbenchmark

Hvorfor koster læsning af en formelcelle en fuld genberegning?

Fordi en værdi-getter på en formelcelle er en anmodning om at producere en værdi, og den eneste universelt korrekte måde at producere én på er at evaluere formlen. Det er det rigtige default for en applikation, der redigerer arbejdsbøger, og det forkerte default for en pipeline, der udtrækker dem. Værre er, at evaluering ikke er fri for sideeffekter: den skriver resultater tilbage i celler, vender dirty-flag, og den kan løse sig anderledes end den producerende applikation, når en funktion er uden understøttelse, eller en ekstern reference er brudt. Et job, du beskrev for din driftsafdeling som read-only, producerer stille en arbejdsbog, der ikke længere matcher den på disken, og hvis noget senere gemmer den, ændres filen på disken også

Cached-value-læsning er den anden halvdel af kontrakten. Den besvarer et snævrere spørgsmål — hvad gemte den producerende applikation her? — og nægter at besvare noget andet. Når du reelt vil have friske tal, giver HotXLS dig stadig inkrementel genberegning drevet af en afhængighedsgraf; pointen er, at udtrækning og evaluering bør være to forskellige kald, ikke ét kald med to humører

Tre ortogonale fakta om én celle

Konklusionen først: en cached formelværdi bærer tre uafhængige fakta, og at skjule dem i en enkelt Variant mister information, du behøver. TXLSFormulaCacheInfo holder dem adskilt som State, Kind og Value. TXLSFormulaCacheState noterer proveniens på tværs af fem tilfælde — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated og xlfcsInvalidated — mens TXLSFormulaCacheValueKind klassificerer nyttelasten som xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean eller xlfcvError. Denne adskillelse er det, der lader tilstedeværelse rapporteres ærligt: en cached blank, en cached tom streng, en cached False, en cached nul og en cached fejl er alle rigtige værdier, så tilstedeværelse kan aldrig udledes af VarIsEmpty eller VarIsNull. TryGetCachedFormulaValue returnerer True kun for xlfcsLoaded og xlfcsCalculated og udfylder stadig en diagnosticerbar tilstand, når den returnerer False

HotXLS-recorden TXLSFormulaCacheInfo holder tre ortogonale fakta om én formelcelle adskilt: proveniens-State på tværs af fem tilfælde, nyttelast-Kind på tværs af seks og Variant-Value, så en cached blank eller False aldrig forveksles med en fraværende cache
Proveniens, nyttelasttype og nyttelastværdi forbliver adskilt, hvilket er den eneste måde, en cached blank, nul, tom streng eller fejl kan rapporteres som den rigtige værdi, den er
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row og Col er alle enbaserede her
    if Book.TryGetCachedFormulaValue(1, 12, 5, Info) then
      Writeln('cached value: ', VarToStr(Info.Value))
    else
      Writeln('no usable cache, state ordinal ', Ord(Info.State));
  finally
    Book.Free;
  end;
end;

Hvorfor mangler den cachede værdi?

Der er præcis fire grunde til, at TryGetCachedFormulaValue overrækker False, og tilstanden fortæller dig, hvilken der gælder. xlfcsNotFormula betyder, at cellen holder en literal eller slet intet, og at koordinaterne er uden for intervallet skjuler sig i samme svar. xlfcsMissing betyder, at cellen reelt er en formel, men at producenten ikke gemte nogen værdinyttelast til den — et almindeligt udfald, når en generator skriver formler og lader Excel udfylde resultater ved første åbning. xlfcsInvalidated betyder, at formelteksten blev udskiftet efter indlæsning, så den værdi, der plejede at være der, beskriver et udtryk, der ikke længere findes. xlfcsCalculated er derimod et succesfelt: det markerer en værdi, din egen kode eller HotXLS-evaluatoren producerede i denne session, i modsætning til xlfcsLoaded, som kom fra filen

Ærlighed om en manglende cache betyder mere end at klister over den. HotXLS nægter at opfinde en værdi, og ved gemning er den lige så streng — kun xlfcsLoaded og xlfcsCalculated emitterer en cached værdi, mens xlfcsMissing og xlfcsInvalidated skriver formlen alene frem for at fryse et forældet tal ind i filen. Det efterlader dig tre fornuftige reaktioner i en pipeline: spring rækken over og notér hullet, genberegn dén arbejdsbog bevidst og accepter omkostningen, eller evaluér og forlig. Hvis det beregnede tal er uenigt med, hvad den producerende applikation ville have skrevet, er formevalueringstraceren værktøjet til at finde ud af, hvor de to beregninger divergerer, frem for at gætte ud fra resultatet

Én læser på tværs af de klassiske, OOXML- og ODF-motorer

En pipeline bør ikke bryde sig om, hvorvidt filen, den lige åbnede, var BIFF, OOXML eller ODF. IXLSFormulaCacheReader er det eneste read-only-indgangspunkt for alle tre: både TXLSWorkbook.CreateFormulaCacheReader og TXLSXWorkbook.CreateFormulaCacheReader returnerer en letvægtsadapter over det sparse celleopslag, hver motor allerede bruger, med identiske enbaserede ark-, række- og kolonnekoordinater. Arbejdsbogsklasserne implementerer bevidst ikke interfacet selv — en interfacereference til arbejdsbogen ville ændre dens ownership-semantik og lade kaldere glide forbi lifetime-leasen. I stedet rydder destruktion af arbejdsbogen den rå pointer inde i den leasen, og enhver læser, din kode stadig holder, udløser EXLSFormulaCacheReaderInvalidated ved sin næste forespørgsel i stedet for at dereferencere frigivet hukommelse. Det er fail-fast lifetime-kontrol, ikke en garanti for samtidighed

var
  Reader: IXLSFormulaCacheReader;
  Info: TXLSFormulaCacheInfo;
  Row, Missing, Errors: Integer;
  Total: Double;
begin
  Reader := Book.CreateFormulaCacheReader;
  Total := 0;
  Missing := 0;
  Errors := 0;
  for Row := 2 to LastRow do
    if Reader.TryGetCachedFormulaValue(1, Row, 7, Info) then
    begin
      case Info.Kind of
        xlfcvNumber: Total := Total + Double(Info.Value);
        xlfcvError:  Inc(Errors);
      end;
    end
    else if Info.State = xlfcsMissing then
      Inc(Missing);
  // Ingen lommeregner kørte, intet dirty-flag flyttede sig, Book er uændret
end;

Hvor de cachede bytes faktisk bor

For klassiske .xls-filer er cachen FormulaValue-feltet i Formula-recorden, otte byte beskrevet af [MS-XLS] §2.5.133. Når det høje ord er lig $FFFF, er nyttelasten ikke en IEEE 754 double, men en tagget variant, og layoutet er let at tage fejl af på den subtile måde: varianttypen ligger i val[0], og boolean- eller BErr-nyttelasten ligger i val[2], med val[1] udefineret. HotXLS læste tidligere nyttelasten fra val[1], hvilket er den slags off-by-one, der kun viser sig på de specifikke filer, der cacher en boolean eller en fejl frem for et tal. Læseren og writeren til delte formler er nu enige om de samme offsets, så en cached TRUE overlever en load og save intakt i stedet for at forfalde til støj

Det otte byte store FormulaValue-felt i en klassisk XLS Formula-record, som HotXLS læser det: en IEEE 754 double, medmindre det høje ord er lig FFFF, i hvilket fald varianttypen ligger i val nul og boolean- eller fejl-nyttelasten i val to
Når det høje ord er FFFF, er feltet en tagget variant, og nyttelasten ligger i val[2] med val[1] udefineret, hvilket er præcis den byte, læseren plejede at tage

Typefidelity i pakkformaterne er et separat problem med sin egen fælde. I OOXML hænger den cachede værdi på c-elementet som <v>, med t-attributten, der navngiver typen pr. ECMA-376 Part 1 §18.3.1.4. HotXLS læser t="e" direkte ind i en varError-Variant og mapper den tilbage til standardfejlteksten ved gemning, så fejl aldrig udgiver sig for almindelige heltal — men Delphi-RTL hjælper dig ikke her, fordi VarAsType(Integer, varError) udløser en konverteringsundtagelse. Den virkende konstruktion sætter TVarData.VType og TVarData.VError direkte. Datoer følger samme disciplin i den modsatte retning: t="d" og ODF-dato-værditypen er eksplicitte typede deklarationer og bliver varDate, mens en BIFF-numerisk cache slet ikke bærer et dato-flag og derfor forbliver en Double. HotXLS gætter aldrig en dato ud fra et cellenummerformat, for talformatet er præsentation, og cachen er data. ODF tilføjer endnu et tilfælde, der er værd at kende — office:value-type="void" udtrykker en cache, der er til stede, men ikke bærer nogen værdi, og da ODF ikke har nogen fejl-værditype, bevares fejlagtig tekst som tekst frem for at blive forfremmet til en fejl

function DescribeCache(const Info: TXLSFormulaCacheInfo): string;
begin
  case Info.State of
    xlfcsNotFormula:  Result := 'not a formula cell';
    xlfcsMissing:     Result := 'formula stored with no cached value';
    xlfcsInvalidated: Result := 'formula replaced since load';
  else
    case Info.Kind of
      xlfcvError:    Result := 'error code ' + IntToStr(TVarData(Info.Value).VError);
      xlfcvDateTime: Result := DateTimeToStr(VarToDateTime(Info.Value));
      xlfcvBoolean:  Result := BoolToStr(Info.Value, True);
      xlfcvNumber:   Result := FloatToStr(Double(Info.Value));
      xlfcvString:   Result := VarToStr(Info.Value);
    else
      Result := 'present but blank';
    end;
  end;
end;

Deler delte formler deres cachede værdier?

Nej, og at antage andet er måden, en sweep ender med at rapportere det samme tal for en hel kolonne. En OOXML delt formel deler kun formeludtrykket og lageroptimeringen; hver medlemscelle ejer stadig sin egen <v>. HotXLS propagerer derfor aldrig rodmedlemmets cache til en følger, der ankom uden en værdi, og en følger, der loadede som xlfcsMissing, rapporterer stadig xlfcsMissing efter en gemning og genåbning. Hvis du arbejder dig gennem, hvordan gruppen i første omgang gemmes og udvides, behandles mekanikken i attributten si for delte formler og dens udvidelse separat; for cache-læsning reducerer regelen til én linje — spørg hver celle, stol på intet, du ikke bad om

Et HotXLS-billede af en OOXML delt formelgruppe, hvor si-attributten kun deler udtrykket og lagerlayoutet, mens hver medlemscelle ejer sin egen cachede værdi, så en følger, der loadede uden én, vedbliver med at rapportere xlfcsMissing
Gruppen deler udtrykket, ikke tallene, så rod-cachen propageres aldrig, og et medlem, der ankom uden en værdi, vedbliver med at rapportere det hul

Cached-value-læsning, den samlede cross-engine-læser og genberegningsmotoren, du kan vælge ikke at kalde, følger alle med den standard HotXLS Delphi Spreadsheet Component til Delphi og C++Builder, uden afhængighed af Excel eller nogen OLE-automatiseringsserver; produktsiden bærer den fulde API-reference til de arbejdsbogs- og læserindgangspunkter, der vises her