Technical Article

Read Excel Cached Formula Values in Delphi Without Recalc

HotXLS, the native Delphi and C++Builder Excel library, reads the value Excel already stored beside a formula through TryGetCachedFormulaValue and IXLSFormulaCacheReader. Neither entry point calls the calculator, decompiles formula tokens, updates dirty state, or writes anything back into the model, so a workbook you only read stays exactly as you opened it

The scenario that drives this is dull and extremely common. A nightly job opens a few hundred workbooks produced by someone else, pulls one column of totals out of each, and pushes the numbers into a warehouse. The totals are already sitting in the files — Excel computed them and saved them. Yet the moment the job asks a formula cell for its value, a library that has only one answer for that question builds a dependency graph and evaluates the whole sheet, and a job that should be I/O bound turns into a calculation benchmark

Why does reading a formula cell cost a full recalculation?

Because a value getter on a formula cell is a request to produce a value, and the only universally correct way to produce one is to evaluate the formula. That is the right default for an application that edits workbooks, and the wrong default for a pipeline that extracts them. Worse, evaluation is not free of side effects: it writes results back into cells, it flips dirty flags, and it can resolve differently from the producing application when a function is unsupported or an external reference is broken. A job you described to your operations team as read-only quietly produces a workbook that no longer matches the one on disk, and if anything later saves it, the file on disk changes too

Cached-value reading is the other half of the contract. It answers a narrower question — what did the producing application store here? — and refuses to answer anything else. When you genuinely want fresh numbers, HotXLS still gives you incremental recalculation driven by a dependency graph; the point is that extraction and evaluation should be two different calls, not one call with two moods

Three orthogonal facts about one cell

The conclusion first: a cached formula value carries three independent facts, and collapsing them into a single Variant loses information you need. TXLSFormulaCacheInfo keeps them apart as State, Kind and Value. TXLSFormulaCacheState records provenance across five cases — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated and xlfcsInvalidated — while TXLSFormulaCacheValueKind classifies the payload as xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean or xlfcvError. This separation is what lets presence be reported honestly: a cached blank, a cached empty string, a cached False, a cached zero and a cached error are all real values, so presence can never be inferred from VarIsEmpty or VarIsNull. TryGetCachedFormulaValue returns True only for xlfcsLoaded and xlfcsCalculated, and still fills in a diagnosable state when it returns False

The HotXLS record TXLSFormulaCacheInfo keeps three orthogonal facts about one formula cell apart: the provenance State across five cases, the payload Kind across six, and the Variant Value, so a cached blank or False is never mistaken for an absent cache
Provenance, payload type and payload value stay separate, which is the only way a cached blank, zero, empty string or error can be reported as the real value it is
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row and Col are all 1-based here
    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;

Why is the cached value missing?

There are exactly four reasons TryGetCachedFormulaValue hands back False, and the state tells you which one applies. xlfcsNotFormula means the cell holds a literal or nothing at all, and the coordinates being out of range collapses into the same answer. xlfcsMissing means the cell really is a formula but the producer stored no value payload for it — a common outcome when a generator writes formulas and lets Excel fill in results on first open. xlfcsInvalidated means the formula text was replaced after load, so the value that used to be there describes an expression that no longer exists. xlfcsCalculated, by contrast, is a success case: it marks a value your own code or the HotXLS evaluator produced during this session, as opposed to xlfcsLoaded, which came from the file

Honesty about a missing cache matters more than papering over it. HotXLS refuses to invent a value, and on save it is equally strict — only xlfcsLoaded and xlfcsCalculated emit a cached value, while xlfcsMissing and xlfcsInvalidated write the formula alone rather than freezing a stale number into the file. That leaves you three sane responses in a pipeline: skip the row and record the gap, recalculate that one workbook deliberately and accept the cost, or evaluate and reconcile. If the evaluated number disagrees with what the producing application would have written, the formula evaluation tracer is the tool for finding out where the two computations diverge, rather than guessing from the result

One reader across the classic, OOXML and ODF engines

A pipeline should not care whether the file it just opened was BIFF, OOXML or ODF. IXLSFormulaCacheReader is the single read-only entry point for all three: both TXLSWorkbook.CreateFormulaCacheReader and TXLSXWorkbook.CreateFormulaCacheReader return a lightweight adapter over the sparse cell lookup each engine already uses, with identical 1-based sheet, row and column coordinates. The workbook classes deliberately do not implement the interface themselves — an interface reference to the workbook would change its ownership semantics and let callers slip past the lifetime lease. Instead, destroying the workbook clears the raw pointer inside that lease, and any reader still held by your code raises EXLSFormulaCacheReaderInvalidated on its next query instead of dereferencing freed memory. It is fail-fast lifetime checking, not a concurrency guarantee

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);
  // No calculator ran, no dirty flag moved, Book is unchanged
end;

Where the cached bytes actually live

For classic .xls files the cache is the FormulaValue field of the Formula record, eight bytes described by [MS-XLS] §2.5.133. When the high word equals $FFFF the payload is not an IEEE 754 double but a tagged variant, and the layout is easy to get subtly wrong: the variant type sits in val[0] and the boolean or BErr payload sits in val[2], with val[1] undefined. HotXLS previously read the payload from val[1], which is the kind of off-by-one that surfaces only on the specific files that cache a boolean or an error rather than a number. The reader and the shared-formula writer now agree on the same offsets, so a cached TRUE survives a load and save intact instead of decaying into noise

The eight-byte FormulaValue field of a classic XLS Formula record as HotXLS reads it: an IEEE 754 double unless the high word equals FFFF, in which case the variant type sits in val zero and the Boolean or error payload in val two
When the high word is FFFF the field is a tagged variant, and the payload sits in val[2] with val[1] undefined, which is exactly the byte the reader used to take

Type fidelity in the package formats is a separate problem with its own trap. In OOXML the cached value hangs off the c element as <v>, with the t attribute naming the type per ECMA-376 Part 1 §18.3.1.4. HotXLS reads t="e" straight into a varError Variant and maps it back to the standard error text on save, so errors never masquerade as ordinary integers — but the Delphi RTL will not help you here, because VarAsType(Integer, varError) raises a conversion exception. The working construction sets TVarData.VType and TVarData.VError directly. Dates follow the same discipline in the opposite direction: t="d" and the ODF date value type are explicit type declarations and become varDate, while a BIFF numeric cache carries no date flag at all and therefore stays a Double. HotXLS never guesses a date from a cell number format, because the number format is presentation and the cache is data. ODF adds one more case worth knowing — office:value-type="void" expresses a cache that is present but carries no value, and since ODF has no error value type, error-looking text is preserved as text rather than promoted to an error

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;

Do shared formulas share their cached values?

No, and assuming otherwise is how a sweep ends up reporting the same number for an entire column. An OOXML shared formula shares the formula expression and the storage optimisation only; every member cell still owns its own <v>. HotXLS therefore never propagates the root member cache to a follower that arrived without a value, and a follower that loaded as xlfcsMissing still reports xlfcsMissing after a save and reopen. If you are working through how the group is stored and expanded in the first place, the mechanics of the shared formula si attribute and its expansion are covered separately; for cache reading, the rule reduces to one line — ask every cell, trust nothing you did not ask for

A HotXLS view of an OOXML shared formula group in which the si attribute shares only the expression and the storage layout, while every member cell owns its own cached value, so a follower that loaded without one keeps reporting xlfcsMissing
The group shares the expression, not the numbers, so the root cache is never propagated and a member that arrived without a value keeps reporting that gap

Cached-value reading, the unified cross-engine reader and the recalculation engine you can choose not to invoke all ship in the standard HotXLS Delphi Spreadsheet Component for Delphi and C++Builder, with no dependency on Excel or on any OLE automation server; the product page carries the full API reference for the workbook and reader entry points shown here