Technisch artikel

Excel formulecachewaarden lezen in Delphi zonder recalc

HotXLS, de native Excel-bibliotheek voor Delphi en C++Builder, leest de waarde die Excel al naast een formule heeft opgeslagen via TryGetCachedFormulaValue en IXLSFormulaCacheReader. Geen van beide instappunten roept de calculator aan, decompileert formuletokens, werkt dirty-status bij of schrijft iets terug in het model, dus een werkmap die u alleen leest blijft precies zoals u haar opende

Het scenario dat dit vraagt is saai en uitermate gewoon. Een nachtelijke taak opent enkele honderden werkmappen die iemand anders heeft gemaakt, trekt uit elke map één kolom totalen en duwt de cijfers een datawarehouse in. De totalen staan al in de bestanden — Excel heeft ze berekend en opgeslagen. Toch bouwt, zodra de taak een formulecel om haar waarde vraagt, een bibliotheek die daarop slechts één antwoord heeft een afhankelijkheidsgrafiek en evalueert het hele werkblad, en een taak die I/O-gebonden zou moeten zijn verandert in een rekenbenchmark

Waarom kost het lezen van een formulecel een volledige herberekening?

Want een waarde-getter op een formulecel is een verzoek om een waarde te produceren, en de enig algemeen correcte manier om er een te produceren is de formule evalueren. Dat is de juiste standaard voor een applicatie die werkmappen bewerkt, en de verkeerde voor een pipeline die ze extraheert. Erger nog, evaluatie is niet vrij van neveneffecten: ze schrijft resultaten terug in cellen, zet dirty-vlaggen om, en kan anders uitpakken dan bij de producerende applicatie wanneer een functie niet wordt ondersteund of een externe verwijzing kapot is. Een taak die u aan uw operationsteam als read-only hebt beschreven produceert geruisloos een werkmap die niet meer matcht met die op schijf, en als er later iets in opslaat verandert het bestand op schijf ook

Het lezen van cachewaarden is de andere helft van het contract. Ze beantwoordt een smallere vraag — wat heeft de producerende applicatie hier opgeslagen? — en weigert iets anders te antwoorden. Als u echt verse cijfers wilt, geeft HotXLS u nog steeds incrementele herberekening aangedreven door een afhankelijkheidsgrafiek; het punt is dat extractie en evaluatie twee verschillende aanroepen moeten zijn, niet één aanroep met twee gezichten

Drie orthogonale feiten over één cel

Eerst de conclusie: een gecachete formulewaarde draagt drie onafhankelijke feiten, en ze samenvouwen tot één Variant verliest informatie die u nodig hebt. TXLSFormulaCacheInfo houdt ze uit elkaar als State, Kind en Value. TXLSFormulaCacheState registreert de herkomst in vijf gevallen — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated en xlfcsInvalidated — terwijl TXLSFormulaCacheValueKind de payload classificeert als xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean of xlfcvError. Die scheiding maakt het mogelijk om aanwezigheid eerlijk te melden: een gecachete lege cel, een gecachete lege string, een gecachete False, een gecachete nul en een gecachete fout zijn allemaal echte waarden, dus aanwezigheid kan nooit worden afgeleid uit VarIsEmpty of VarIsNull. TryGetCachedFormulaValue geeft True alleen voor xlfcsLoaded en xlfcsCalculated, en vult bij False nog steeds een diagnoseerbare status in

Het HotXLS-record TXLSFormulaCacheInfo houdt drie orthogonale feiten over één formulecel uit elkaar: de herkomst State in vijf gevallen, de payload-Kind in zes en de Variant Value, zodat een gecachete lege cel of False nooit wordt aangezien voor een afwezige cache
Herkomst, payloadtype en payloadwaarde blijven gescheiden, en dat is de enige manier waarop een gecachete lege cel, nul, lege string of fout kan worden gemeld als de echte waarde die ze is
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row en Col zijn hier allemaal 1-based
    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;

Waarom ontbreekt de cachewaarde?

Er zijn precies vier redenen waarom TryGetCachedFormulaValue False teruggeeft, en de status vertelt u welke van toepassing is. xlfcsNotFormula betekent dat de cel een literal bevat of helemaal niets, en coördinaten buiten bereik vallen samen met hetzelfde antwoord. xlfcsMissing betekent dat de cel echt een formule is maar de producer geen waarde-payload voor haar heeft opgeslagen — een gangbare uitkomst wanneer een generator formules schrijft en Excel de resultaten bij de eerste open invult. xlfcsInvalidated betekent dat de formulertekst na het laden is vervangen, dus de waarde die er stond beschrijft een expressie die niet meer bestaat. xlfcsCalculated is daarentegen een succesgeval: die markeert een waarde die uw eigen code of de HotXLS-evaluator tijdens deze sessie produceerde, in tegenstelling tot xlfcsLoaded, die uit het bestand kwam

Eerlijkheid over een ontbrekende cache weegt zwaarder dan haar te maskeren. HotXLS weigert een waarde te verzinnen, en bij het opslaan is ze even streng — alleen xlfcsLoaded en xlfcsCalculated emitteren een cachewaarde, terwijl xlfcsMissing en xlfcsInvalidated alleen de formule weg schrijven in plaats van een verouderd getal in het bestand te bevriezen. Dat laat u drie verstandige reacties in een pipeline: de rij overslaan en het gat registreren, die ene werkmap bewust herberekenen en de kosten accepteren, of evalueren en vergelijken. Als het geëvalueerde getal niet klopt met wat de producerende applicatie geschreven zou hebben, is de formule-evaluatietracer het gereedschap om te vinden waar de twee berekeningen uiteenlopen, in plaats van te gissen uit het resultaat

Eén reader voor de klassieke, OOXML- en ODF-engines

Een pipeline zou niet mogen uitmaken of het bestand dat ze zojuist opende BIFF, OOXML of ODF was. IXLSFormulaCacheReader is het enige read-only instappunt voor alle drie: zowel TXLSWorkbook.CreateFormulaCacheReader als TXLSXWorkbook.CreateFormulaCacheReader geeft een lichtgewicht adapter terug over de sparse cel-opzoeking die elke engine al gebruikt, met identieke 1-based werkblad-, rij- en kolomcoördinaten. De werkmapklassen implementeren de interface bewust niet zelf — een interfacereferentie naar de werkmap zou haar eigendomssemantiek veranderen en aanroepers de lifetime lease laten omzeilen. In plaats daarvan wist het vernietigen van de werkmap de raw pointer binnen die lease, en elke reader die uw code nog vasthoudt geeft bij de volgende query EXLSFormulaCacheReaderInvalidated in plaats van vrijgegeven geheugen te derefereren. Het is fail-fast lifetimecontrole, geen garanties over concurrency

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);
  // Geen calculator gedraaid, geen dirty-vlag verplaatst, Book is ongewijzigd
end;

Waar de gecachte bytes feitelijk wonen

Bij klassieke .xls-bestanden is de cache het veld FormulaValue van het Formula-record, acht bytes beschreven in [MS-XLS] §2.5.133. Wanneer het hoge woord gelijk is aan $FFFF is de payload geen IEEE 754 double maar een tagged variant, en de layout is gemakkelijk subtiel verkeerd te lezen: het varianttype zit in val[0] en de boolean- of BErr-payload zit in val[2], met val[1] ongedefinieerd. HotXLS las de payload voorheen uit val[1], precies het soort off-by-one dat alleen opduikt op de specifieke bestanden die een boolean of een fout cachen in plaats van een getal. De reader en de shared-formula-writer zijn het nu over dezelfde offsets eens, dus een gecachete TRUE overleeft een load en save ongeschonden in plaats van te vervallen tot ruis

Het acht bytes grote veld FormulaValue van een klassiek XLS Formula-record zoals HotXLS het leest: een IEEE 754 double tenzij het hoge woord gelijk is aan FFFF, in welk geval het varianttype in val nul zit en de boolean- of foutpayload in val twee
Wanneer het hoge woord FFFF is, is het veld een tagged variant en zit de payload in val[2] met val[1] ongedefinieerd — precies de byte die de reader voorheen pakte

Typegetrouwheid in de pakketformaten is een apart probleem met een eigen valkuil. In OOXML hangt de cachewaarde aan het element c als <v>, met het attribuut t dat het type benoemt volgens ECMA-376 Part 1 §18.3.1.4. HotXLS leest t="e" direct in een varError-Variant en mapt haar bij het opslaan terug naar de standaardfouttekst, dus fouten geven zich nooit uit voor gewone integers — maar de Delphi RTL helpt u hier niet, want VarAsType(Integer, varError) gooit een conversie-exception. De werkende constructie zet TVarData.VType en TVarData.VError direct. Data volgen dezelfde discipline in omgekeerde richting: t="d" en het ODF-datumwaardetype zijn expliciete typedeclaraties en worden varDate, terwijl een BIFF numerieke cache helemaal geen datavlag draagt en dus een Double blijft. HotXLS gist nooit een datum uit een celgetalnotatie, want de getalnotatie is presentatie en de cache is data. ODF voegt nog één geval toe dat de moeite van kennen waard is — office:value-type="void" drukt een cache uit die aanwezig is maar geen waarde draagt, en omdat ODF geen foutwaardetype kent, wordt foutachtige tekst bewaard als tekst in plaats van gepromoveerd tot een fout

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;

Delen gedeelde formules hun cachewaarden?

Nee, en anders aannemen is precies hoe een sweep eropuit komt met hetzelfde getal voor een hele kolom. Een OOXML shared formula deelt alleen de formula-expressie en de opslagoptimalisatie; elke lidcel bezit nog steeds haar eigen <v>. HotXLS propageert daarom de cachewaarde van het root-lid nooit naar een volger die zonder waarde aankwam, en een volger die als xlfcsMissing laadt meldt na een save en heropen nog steeds xlfcsMissing. Als u eerst wilde uitzoeken hoe de groep wordt opgeslagen en uitgebreid, wordt de werking van het si-attribuut van de shared formula en zijn expansie apart behandeld; voor het lezen van caches komt de regel neer op één regel — vraag elke cel, vertrouw niets dat u niet vroeg

Een HotXLS-weergave van een OOXML shared formula-groep waarin het si-attribuut alleen de expressie en de opslaglayout deelt, terwijl elke lidcel haar eigen cachewaarde bezit, zodat een volger die zonder waarde laadt blijvend xlfcsMissing meldt
De groep deelt de expressie, niet de getallen, dus de root-cache wordt nooit gepropageerd en een lid dat zonder waarde aankwam meldt dat gat blijvend

Het lezen van cachewaarden, de verenigde cross-engine reader en de herberekeningsengine die u ervoor kunt kiezen niet aan te roepen, zitten allemaal in het standaard HotXLS Delphi Spreadsheet Component voor Delphi en C++Builder, zonder afhankelijkheid van Excel of van een OLE-automatiseringsserver; de productpagina bevat de volledige API-referentie voor de werkmap- en reader-instappunten die hier worden getoond