Artigo Técnico

Ler valores em cache de fórmulas Excel em Delphi sem recalc

O HotXLS, a biblioteca Excel nativa para Delphi e C++Builder, lê o valor que o Excel já armazenou ao lado de uma fórmula por meio de TryGetCachedFormulaValue e IXLSFormulaCacheReader. Nenhum dos pontos de entrada chama a calculadora, descompila tokens de fórmula, atualiza estado dirty ou escreve qualquer coisa de volta no modelo, então uma pasta de trabalho que você só lê permanece exatamente como a abriu

O cenário que motiva isso é sem graça e extremamente comum. Um job noturno abre algumas centenas de pastas de trabalho produzidas por outra pessoa, puxa uma coluna de totais de cada uma e empurra os números para um warehouse. Os totais já estão nos arquivos — o Excel os computou e salvou. Ainda assim, no momento em que o job pede o valor de uma célula de fórmula, uma biblioteca que só tem uma resposta para essa pergunta constrói um grafo de dependências e avalia a planilha inteira, e um job que deveria ser limitado por I/O vira um benchmark de cálculo

Por que ler uma célula de fórmula custa um recálculo completo?

Porque um getter de valor numa célula de fórmula é um pedido para produzir um valor, e o único modo universalmente correto de produzir um é avaliar a fórmula. Esse é o padrão certo para um aplicativo que edita pastas de trabalho, e o padrão errado para um pipeline que as extrai. Pior, a avaliação não é livre de efeitos colaterais: escreve resultados de volta nas células, vira flags dirty e pode resolver diferente do aplicativo produtor quando uma função não é suportada ou uma referência externa está quebrada. Um job que você descreveu à sua equipe de operações como somente leitura produz silenciosamente uma pasta de trabalho que não corresponde mais à que está no disco, e se qualquer coisa salvá-la depois, o arquivo em disco muda também

A leitura de valor em cache é a outra metade do contrato. Ela responde uma pergunta mais estreita — o que o aplicativo produtor armazenou aqui? — e se recusa a responder qualquer outra coisa. Quando você genuinamente quer números frescos, o HotXLS ainda te dá recálculo incremental dirigido por um grafo de dependências; o ponto é que extração e avaliação deveriam ser duas chamadas diferentes, não uma chamada com dois humores

Três fatos ortogonais sobre uma célula

Primeiro a conclusão: um valor de fórmula em cache carrega três fatos independentes, e colapsá-los num único Variant perde informação de que você precisa. O TXLSFormulaCacheInfo os mantém separados como State, Kind e Value. O TXLSFormulaCacheState registra procedência em cinco casos — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated e xlfcsInvalidated — enquanto o TXLSFormulaCacheValueKind classifica o payload como xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean ou xlfcvError. Essa separação é o que permite que presença seja reportada com honestidade: um blank em cache, uma string vazia em cache, um False em cache, um zero em cache e um erro em cache são todos valores reais, então presença jamais pode ser inferida de VarIsEmpty ou VarIsNull. O TryGetCachedFormulaValue retorna True só para xlfcsLoaded e xlfcsCalculated, e ainda preenche um estado diagnosticável quando retorna False

O record TXLSFormulaCacheInfo do HotXLS mantém três fatos ortogonais sobre uma célula de fórmula separados: a procedência State em cinco casos, o Kind do payload em seis, e o Value Variant, então um blank ou False em cache nunca é confundido com um cache ausente
Procedência, tipo do payload e valor do payload permanecem separados, que é o único jeito de um blank, zero, string vazia ou erro em cache ser reportado como o valor real que é
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row e Col são todos base um aqui
    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;

Por que o valor em cache está ausente?

Existem exatamente quatro razões para o TryGetCachedFormulaValue devolver False, e o estado te diz qual se aplica. xlfcsNotFormula significa que a célula guarda um literal ou nada, e coordenadas fora do range colapsam na mesma resposta. xlfcsMissing significa que a célula realmente é uma fórmula mas o produtor não armazenou payload de valor para ela — desfecho comum quando um gerador escreve fórmulas e deixa o Excel preencher resultados na primeira abertura. xlfcsInvalidated significa que o texto da fórmula foi substituído depois do load, então o valor que costumava estar lá descreve uma expressão que não existe mais. xlfcsCalculated, por contraste, é um caso de sucesso: marca um valor que seu próprio código ou o avaliador do HotXLS produziu nesta sessão, em oposição a xlfcsLoaded, que veio do arquivo

Honestidade sobre um cache ausente importa mais que cobri-lo com tapa-buraco. O HotXLS se recusa a inventar um valor, e no save é igualmente rigoroso — só xlfcsLoaded e xlfcsCalculated emitem um valor em cache, enquanto xlfcsMissing e xlfcsInvalidated escrevem só a fórmula em vez de congelar um número velho no arquivo. Isso te deixa três respostas sãs num pipeline: pular a linha e registrar a lacuna, recalcular deliberadamente aquela pasta de trabalho e aceitar o custo, ou avaliar e reconciliar. Se o número avaliado discorda do que o aplicativo produtor teria escrito, o tracer de avaliação de fórmulas é a ferramenta para descobrir onde as duas computações divergem, em vez de chutar pelo resultado

Um leitor pelos motores clássico, OOXML e ODF

Um pipeline não deveria se importar se o arquivo que acabou de abrir era BIFF, OOXML ou ODF. O IXLSFormulaCacheReader é o único ponto de entrada somente leitura para os três: tanto TXLSWorkbook.CreateFormulaCacheReader quanto TXLSXWorkbook.CreateFormulaCacheReader retornam um adaptador leve sobre a busca esparsa de células que cada motor já usa, com coordenadas de planilha, linha e coluna base um idênticas. As classes de pasta de trabalho deliberadamente não implementam a interface elas mesmas — uma referência de interface à pasta de trabalho mudaria sua semântica de posse e deixaria chamadores escapar do lease de tempo de vida. Em vez disso, destruir a pasta de trabalho limpa o ponteiro bruto dentro desse lease, e qualquer leitor ainda segurado pelo seu código levanta EXLSFormulaCacheReaderInvalidated na próxima consulta em vez de desreferenciar memória liberada. É checagem de tempo de vida fail-fast, não uma garantia de concorrência

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);
  // Nenhuma calculadora rodou, nenhuma flag dirty se moveu, Book não muda
end;

Onde os bytes em cache realmente vivem

Para arquivos .xls clássicos o cache é o campo FormulaValue do record Formula, oito bytes descritos pelo [MS-XLS] §2.5.133. Quando a palavra alta é igual a $FFFF o payload não é um double IEEE 754 mas um variant tagado, e o layout é fácil de errar sutilmente: o tipo do variant fica em val[0] e o payload booleano ou BErr fica em val[2], com val[1] indefinido. O HotXLS antes lia o payload de val[1], que é o tipo de off-by-one que só aparece nos arquivos específicos que colocam em cache um booleano ou um erro em vez de um número. O leitor e o writer de fórmulas compartilhadas agora concordam nos mesmos offsets, então um TRUE em cache sobrevive a um load e save intacto em vez de decair em ruído

O campo FormulaValue de oito bytes de um record Formula XLS clássico como o HotXLS o lê: um double IEEE 754 a menos que a palavra alta seja FFFF, caso em que o tipo do variant fica em val zero e o payload Boolean ou erro em val dois
Quando a palavra alta é FFFF o campo é um variant tagado, e o payload fica em val[2] com val[1] indefinido, que é exatamente o byte que o leitor costumava pegar

A fidelidade de tipo nos formatos de pacote é um problema separado com sua própria armadilha. No OOXML o valor em cache pende do elemento c como <v>, com o atributo t nomeando o tipo por ECMA-376 Part 1 §18.3.1.4. O HotXLS lê t="e" direto num Variant varError e o mapeia de volta para o texto de erro padrão no save, então erros nunca se mascaram de inteiros comuns — mas a RTL do Delphi não te ajuda aqui, porque VarAsType(Integer, varError) levanta uma exceção de conversão. A construção que funciona define TVarData.VType e TVarData.VError diretamente. Datas seguem a mesma disciplina na direção oposta: t="d" e o tipo de valor date do ODF são declarações explícitas de tipo e se tornam varDate, enquanto um cache numérico BIFF não carrega flag de data alguma e portanto permanece um Double. O HotXLS nunca chuta uma data pelo formato numérico de célula, porque o formato numérico é apresentação e o cache é dado. O ODF adiciona um caso a mais que vale conhecer — office:value-type="void" expressa um cache que está presente mas não carrega valor, e como o ODF não tem tipo de valor de erro, texto com cara de erro é preservado como texto em vez de promovido a erro

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;

Fórmulas compartilhadas compartilham seus valores em cache?

Não, e assumir o contrário é como uma varredura acaba reportando o mesmo número para uma coluna inteira. Uma fórmula compartilhada do OOXML compartilha apenas a expressão da fórmula e a otimização de armazenamento; cada célula membro ainda é dona do próprio <v>. O HotXLS por isso nunca propaga o cache do membro raiz para um seguidor que chegou sem valor, e um seguidor que carregou como xlfcsMissing continua reportando xlfcsMissing depois de um save e reopen. Se você está trabalhando como o grupo é armazenado e expandido em primeiro lugar, a mecânica do atributo si de fórmula compartilhada e sua expansão é coberta separadamente; para leitura de cache, a regra se reduz a uma linha — pergunte a cada célula, não confie em nada que você não pediu

Uma visão HotXLS de um grupo de fórmula compartilhada OOXML em que o atributo si compartilha apenas a expressão e o layout de armazenamento, enquanto cada célula membro é dona do próprio valor em cache, então um seguidor que carregou sem um continua reportando xlfcsMissing
O grupo compartilha a expressão, não os números, então o cache da raiz nunca é propagado e um membro que chegou sem valor continua reportando essa lacuna

A leitura de valor em cache, o leitor unificado entre motores e o motor de recálculo que você pode escolher não invocar todos vêm no HotXLS Delphi Spreadsheet Component padrão para Delphi e C++Builder, sem dependência de Excel ou de qualquer servidor de automação OLE; a página do produto carrega a referência completa da API para os pontos de entrada de pasta de trabalho e leitor mostrados aqui