Teknisk artikel

Läs cachade Excelformelvärden i Delphi utan omberäkning

HotXLS, det nativa Delphi- och C++Builder-Excelbiblioteket, läser värdet som Excel redan lagrat bredvid en formel via TryGetCachedFormulaValue och IXLSFormulaCacheReader. Ingen av ingångspunkterna anropar kalkylatorn, dekompilerar formeltokens, uppdaterar smutstillstånd eller skriver något tillbaka in i modellen, så en arbetsbok du bara läser förblir exakt som du öppnade den

Scenariot som driver detta är tråkigt och extremt vanligt. Ett nattjobb öppnar några hundra arbetsböcker producerade av någon annan, drar ut en kolumn av totalsummor ur var och en, och trycker in talen i ett datalager. Totalsummorna ligger redan i filerna — Excel beräknade dem och sparade dem. Men i samma ögonblick som jobbet frågar en formelcell efter dess värde bygger ett bibliotek som bara har ett svar på den frågan en beroendegraf och evaluerar hela arket, och ett jobb som borde vara I/O-bundet förvandlas till ett beräkningsriktmärke

Varför kostar det en fullständig omberäkning att läsa en formelcell?

Eftersom en värdgetter på en formelcell är en begäran om att producera ett värde, och det enda universellt korrekta sättet att producera ett är att evaluera formeln. Det är rätt default för en applikation som redigerar arbetsböcker, och fel default för en pipeline som extraherar dem. Värre är att evaluering inte är fri från sidoeffekter: den skriver tillbaka resultat in i celler, den flippar smutsflaggor, och den kan lösa annorlunda än den producerande applikationen när en funktion inte stöds eller en extern referens är bruten. Ett jobb du beskrev för ditt driftteam som skrivskyddat producerar tyst en arbetsbok som inte längre matchar den på disk, och om något senare sparar den ändras filen på disk också

Cachelagrad värdeläsning är kontraktets andra hälft. Det svarar på en snävare fråga — vad lagrade den producerande applikationen här? — och vägrar svara på något annat. När du genuint vill ha färska tal ger HotXLS dig fortfarande inkrementell omberäkning driven av en beroendegraf; poängen är att extrahering och evaluering ska vara två olika anrop, inte ett anrop med två humör

Tre ortogonala fakta om en cell

Slutsatsen först: ett cachelagrat formelvärde bär tre oberoende fakta, och att slå ihop dem till en enda Variant tappar information du behöver. TXLSFormulaCacheInfo håller dem isär som State, Kind och Value. TXLSFormulaCacheState registrerar proveniens över fem fall — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated och xlfcsInvalidated — medan TXLSFormulaCacheValueKind klassificerar nyttolasten som xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean eller xlfcvError. Denna separation är vad som låter närvaro rapporteras ärligt: ett cachelagrat blankt, en cachelagrad tom sträng, ett cachelagrat False, en cachelagrad nolla och ett cachelagrat fel är alla riktiga värden, så närvaro kan aldrig härledas från VarIsEmpty eller VarIsNull. TryGetCachedFormulaValue returnerar True bara för xlfcsLoaded och xlfcsCalculated, och fyller fortfarande i ett diagnostiserbart tillstånd när den returnerar False

HotXLS-posten TXLSFormulaCacheInfo håller tre ortogonala fakta om en formelcell isär: proveniens-State över fem fall, nyttolast-Kind över sex, och Variant-Value, så att ett cachelagrat blankt eller False aldrig misstas för en frånvarande cache
Proveniens, nyttolasttyp och nyttolastvärde förblir separata, vilket är det enda sättet ett cachelagrat blankt, noll, tom sträng eller fel kan rapporteras som det riktiga värde det är
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row och Col är alla 1-baserade här
    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;

Varför saknas det cachelagrade värdet?

Det finns exakt fyra skäl till att TryGetCachedFormulaValue lämnar tillbaka False, och tillståndet talar om vilket som gäller. xlfcsNotFormula betyder att cellen håller en litteral eller inget alls, och att koordinaterna är utanför intervallet kollapsar till samma svar. xlfcsMissing betyder att cellen verkligen är en formel men producenten lagrade ingen värdenyttolast för den — ett vanligt utfall när en generator skriver formler och låter Excel fylla i resultat vid första öppning. xlfcsInvalidated betyder att formeltexten ersattes efter inläsning, så värdet som brukade finnas där beskriver ett uttryck som inte längre existerar. xlfcsCalculated, däremot, är ett framgångsfall: det markerar ett värde som din egen kod eller HotXLS-evalueraren producerade under denna session, till skillnad från xlfcsLoaded, som kom från filen

Ärlighet om en saknad cache spelar större roll än att mörka den. HotXLS vägrar att hitta på ett värde, och vid sparning är den lika strikt — bara xlfcsLoaded och xlfcsCalculated skriver ut ett cachelagrat värde, medan xlfcsMissing och xlfcsInvalidated skriver formeln ensam i stället för att frysa in ett inaktuellt tal i filen. Det lämnar dig tre sunda svar i en pipeline: hoppa över raden och registrera luckan, beräkna om just den arbetsboken avsiktligt och acceptera kostnaden, eller evaluera och stämma av. Om det evaluerade talet inte stämmer överens med vad den producerande applikationen skulle ha skrivit är formelevalueringstraceraren verktyget för att ta reda på var de två beräkningarna divergerar, i stället för att gissa från resultatet

En läsare över de klassiska, OOXML- och ODF-motorerna

En pipeline ska inte bry sig om filen den just öppnade var BIFF, OOXML eller ODF. IXLSFormulaCacheReader är den enda skrivskyddade ingångspunkten för alla tre: både TXLSWorkbook.CreateFormulaCacheReader och TXLSXWorkbook.CreateFormulaCacheReader returnerar en lättviktig adapter över den glesa celluppslagning varje motor redan använder, med identiska 1-baserade ark-, rad- och kolumnkoordinater. Arbetsboksklasserna implementerar avsiktligt inte interfacet själva — en interfacereferens till arbetsboken skulle ändra dess ägarskapssemantik och låta anropare slinka förbi livstidsleaset. I stället rensar förstörelsen av arbetsboken den råa pekaren inuti det leaset, och varje läsare som din kod fortfarande håller kastar EXLSFormulaCacheReaderInvalidated vid sin nästa fråga i stället för att dereferera frigjort minne. Det är fail-fast-livstidskontroll, inte en konkurrensgaranti

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 kalkylator kördes, ingen smutsflagga flyttades, Book är oförändrad
end;

Där de cachelagrade byten faktiskt bor

För klassiska .xls-filer är cachen fältet FormulaValue i Formula-posten, åtta byte beskrivna av [MS-XLS] §2.5.133. När höga ordet är lika med $FFFF är nyttolasten inte en IEEE 754 double utan en taggad variant, och layouten är lätt att få subtilt fel: varianttypen sitter i val[0] och boolean- eller BErr-nyttolasten sitter i val[2], med val[1] odefinierad. HotXLS läste tidigare nyttolasten från val[1], vilket är den sorts off-by-one som bara dyker upp på de specifika filer som cachelagrar en boolean eller ett fel i stället för ett tal. Läsaren och den delade formelskrivaren är nu överens om samma offsets, så ett cachelagrat TRUE överlever en inläsning och sparning intakt i stället för att falla sönder till brus

De åtta byten i fältet FormulaValue i en klassisk XLS Formula-post som HotXLS läser den: en IEEE 754 double om inte höga ordet är lika med FFFF, i vilket fall varianttypen sitter i val noll och Boolean- eller felnyttolasten i val två
När höga ordet är FFFF är fältet en taggad variant, och nyttolasten sitter i val[2] med val[1] odefinierad, vilket är exakt den byte läsaren brukade ta

Typfidelity i paketformaten är ett separat problem med sin egen fälla. I OOXML hänger det cachelagrade värdet på c-elementet som <v>, med attributet t som namnger typen enligt ECMA-376 Part 1 §18.3.1.4. HotXLS läser t="e" rakt in i en varError Variant och mappar tillbaka den till standardfeltexten vid sparning, så fel maskerar sig aldrig som vanliga heltal — men Delphi RTL hjälper dig inte här, eftersom VarAsType(Integer, varError) kastar ett konverteringsundantag. Den fungerande konstruktionen sätter TVarData.VType och TVarData.VError direkt. Datum följer samma disciplin i motsatt riktning: t="d" och ODF:s datumvärdetyp är explicita typdeklarationer och blir varDate, medan en BIFF-numerisk cache inte bär någon datumflagga alls och därför förblir en Double. HotXLS gissar aldrig ett datum från ett cellnummerformat, eftersom talformatet är presentation och cachen är data. ODF lägger till ytterligare ett fall värt att känna till — office:value-type="void" uttrycker en cache som är närvarande men bär inget värde, och eftersom ODF saknar felvärdetyp bevaras felliknande text som text i stället för att befordras till ett fel

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;

Delar delade formler sina cachelagrade värden?

Nej, och att anta motsatsen är hur en svepning slutar rapportera samma tal för en hel kolumn. En OOXML delad formel delar bara formeluttrycket och lagringsoptimeringen; varje medlemscell äger fortfarande sin egen <v>. HotXLS propagerar därför aldrig rotmedlemmens cache till en följare som anlände utan ett värde, och en följare som laddades som xlfcsMissing rapporterar fortfarande xlfcsMissing efter en sparning och återöppning. Om du redan arbetar igenom hur gruppen lagras och expanderas från början täcks mekaniken för si-attributet för delade formler och dess expansion separat; för cacheläsning reduceras regeln till en rad — fråga varje cell, lita på inget du inte frågat efter

En HotXLS-vy av en OOXML-grupp med delad formel där si-attributet bara delar uttrycket och lagringslayouten, medan varje medlemscell äger sitt eget cachelagrade värde, så en följare som laddades utan ett sådant fortsätter rapportera xlfcsMissing
Gruppen delar uttrycket, inte talen, så rotcachen propageras aldrig och en medlem som anlände utan ett värde fortsätter rapportera den luckan

Cacheläsning, den enhetliga läsaren över motorerna och omberäkningsmotorn du kan välja att inte anropa levereras alla i standardkomponenten HotXLS Delphi Spreadsheet Component för Delphi och C++Builder, utan beroende på Excel eller någon OLE-automationsserver; produktsidan bär den fullständiga API-referensen för de arbetsboks- och läsaringångspunkter som visas här