Artigo Técnico

Recálculo Incremental de Fórmulas no HotXLS em Delphi

O HotXLS, a biblioteca nativa Delphi e C++Builder para Excel, realiza o recálculo incremental de fórmulas através de TXLSXWorkbook.Recalculate. A primeira chamada constrói um grafo de dependências de fórmulas e avalia cada célula com fórmula; todas as chamadas posteriores reavaliam apenas as células afetadas por alterações de valores desde o último ciclo, em ordem topológica, numa única varredura cujo custo é proporcional ao número de células alteradas (dirty) e não ao tamanho do livro

Esta única decisão de design dita a diferença entre um modelo financeiro que responde a uma premissa editada em milissegundos e um que bloqueia durante segundos. Se gera relatórios nos quais um pequeno grupo de células de entrada alimenta milhares de fórmulas a jusante, este artigo detalha o papel do grafo, quais as funções que abdicam da incrementalidade e como as referências circulares são reportadas em vez de causarem um ciclo infinito

Porquê que alterar uma célula recalcula cem mil fórmulas?

Um motor de fórmulas simples não guarda memória de quem depende de quem, pelo que a sua única ação segura após qualquer edição consiste em avaliar tudo novamente. Pior ainda, a clássica estratégia recursiva — quando a fórmula A faz referência à fórmula B, avalia B de imediato — reavalia as células referenciadas de forma incondicional, ignorando qualquer valor em cache. Uma cadeia de n fórmulas em que cada uma referencia a anterior custa O(n²) avaliações por ciclo completo, e uma referência circular faz a recursão falhar inevitavelmente. Todos os programadores de folhas de cálculo que ligaram um modelo em cascata a um avaliador recursivo já presenciaram ambos os cenários de falha

O próprio Excel resolveu isto há décadas com a sua cadeia de cálculo: uma ordenação de células com fórmula mantida para que uma edição marque um pequeno conjunto de células como alteradas (dirty), fazendo com que o motor percorra apenas a extremidade afetada da cadeia. O HotXLS aplica a mesma ideia sob a forma de um grafo de dependências explícito, construído uma única vez a partir das árvores de fórmulas compiladas e reutilizado em todos os ciclos de recálculo. O objetivo não é ser engenhoso; é garantir que o custo do recálculo dependa do volume da sua edição e não do tamanho do seu livro

Como o grafo de dependências transforma uma edição num único ciclo

O grafo de dependências do HotXLS atribui a cada célula com fórmula um nó, com arestas a ligar o precedente ao dependente. Quando o seu código escreve o valor de uma célula, o livro regista-a como alterada (dirty); ao executar Recalculate, o estado de alteração propaga-se ao longo das arestas para cada fórmula a jusante, e o subgrafo alterado é avaliado exatamente uma vez em ordem topológica utilizando o algoritmo de Kahn. Visto que uma fórmula nunca é acedida antes dos seus precedentes, cada nó exige uma única avaliação — o que torna o ciclo O(dirty)

A ordem topológica também resolve o problema da recursão na raiz. Durante um ciclo de recálculo, o motor muda para um modo dedicado no qual qualquer referência a outra célula com fórmula lê diretamente o valor em cache dessa célula em vez de a reavaliar — a ordenação garante que o valor em cache já se encontra atualizado. O mesmo mecanismo assegura que um ciclo de referências não consiga desencadear uma recursão infinita: nada dentro do ciclo volta a aceder ao avaliador para uma célula vizinha

var
  Book: TXLSXWorkbook;
  Inputs, Model: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Inputs := Book.Sheets.Add('Inputs');
    Model  := Book.Sheets.Add('Model');

    Inputs.Cells[2, 2].Value := 0.05;                 // premissa de crescimento
    Model.Cells[2, 2].Formula := 'Inputs!B2*1000';    // as fórmulas XLSX não levam o prefixo '='
    Model.Cells[3, 2].Formula := 'B2*(1+Inputs!B2)';
    // ... milhares de linhas adicionais em cascata com base na mesma premissa ...

    Book.Recalculate;                 // primeira chamada: constrói o grafo, avaliação completa

    Inputs.Cells[2, 2].Value := 0.07; // uma edição marca uma célula como alterada (dirty)
    Book.Recalculate;                 // segunda chamada: apenas a cadeia a jusante é executada
  finally
    Book.Free;
  end;
end;

Cada resultado é colocado no Value em cache da célula, pelo que após o retorno de Recalculate, lê os resultados da mesma forma que lê qualquer outra célula. Num ciclo de geração de relatórios, o padrão corresponde precisamente ao código acima: carrega ou constrói o modelo uma vez e, depois, alterna entre escrever em algumas células de entrada e chamar Recalculate, pagando apenas pelas fórmulas que dependem realmente do que foi alterado

Quais as funções do Excel que forçam o recálculo em cada ciclo?

O HotXLS trata as funções NOW, TODAY, RAND, OFFSET e INDIRECT como voláteis: qualquer fórmula que contenha uma delas é reavaliada em cada ciclo de Recalculate, independentemente de se ter verificado alguma alteração a montante. As primeiras três são voláteis pelo mesmo motivo que o são no Excel — o seu resultado depende do momento da avaliação e não de outras células. As funções OFFSET e INDIRECT são voláteis por uma razão mais subtil: as células que leem são calculadas em tempo de execução, pelo que o grafo não consegue saber estaticamente que arestas desenhar para as mesmas

A mesma regra regra conservadora aplica-se a referências que o construtor do grafo não consiga limitar a um único retângulo. Uma fórmula que recorra a um intervalo nomeado de áreas múltiplas (multi-area), ou que referencie um livro externo, é igualmente degradada para volátil e reavaliada em cada ciclo. Esta política é intencional: uma avaliação extra custa um pouco de tempo, mas uma aresta de dependência em falta significa um valor silenciosamente desatualizado num relatório enviado, o que constitui uma falha muito pior. Se o seu modelo depende de nomes com âmbito de livro, o artigo complementar sobre nomes definidos e fórmulas entre folhas aborda como os nomes de área única são resolvidos — esses participam no grafo normalmente

A orientação prática decorre diretamente daqui: mantenha os caminhos críticos de um modelo grande em referências simples de células e intervalos, onde o grafo consiga realizar o seu trabalho, e isole as funções OFFSET e INDIRECT para os poucos locais que necessitem genuinamente de endereçamento dinâmico. Um modelo com mil fórmulas voláteis executa essas mil em cada ciclo, independentemente de quão pequena tenha sido a edição — simulando exatamente o comportamento que os utilizadores do Excel conhecem em livros que "recalculam a cada tecla premida"

Como é que o HotXLS reporta referências circulares?

O método TXLSXWorkbook.Recalculate devolve lxOk num ciclo limpo e lxErrorRef ao detetar um ciclo de referências. Os componentes do ciclo são identificados durante a ordenação topológica — correspondem aos nós que o algoritmo de Kahn nunca consegue libertar — e são ignorados em vez de entrarem em ciclo infinito: os seus valores em cache mantêm-se inalterados, ao passo que todas as fórmulas fora do ciclo continuam a ser avaliadas normalmente e por ordem. O seu código recebe um código de erro definido em vez de um bloqueio da aplicação

case Book.Recalculate of
  lxOk:
    SaveReport(Book);
  lxErrorRef:
    // existe um ciclo de referências; os componentes do ciclo mantiveram os seus valores
    // anteriores em cache e tudo fora do ciclo está atualizado
    LogWarning('Circular reference detected - review model inputs');
end;

Descobrir que células formam o ciclo constitui uma tarefa de depuração, e o rastreador de avaliação de fórmulas é a ferramenta ideal para isso: rastreie a fórmula suspeita e a cadeia de referências que se dobra sobre si mesma tornar-se-á visível passo a passo. Os ciclos em modelos reais resultam quase sempre de um erro de criação — como uma linha de resumo incluída acidentalmente no seu próprio intervalo SUM —, pelo que um código de erro evidente no momento do recálculo é exatamente o desejado

Fórmulas de matriz, rastreamento de alterações e quando o grafo é reconstruído

As fórmulas de matriz CSE recebem um único nó para todo o retângulo fixado, e não um nó por célula. A fórmula raiz é avaliada uma vez por ciclo; a matriz resultante é escrita diretamente em cada célula componente, e uma fórmula que referencie qualquer célula dentro do intervalo fixado — e não apenas a âncora do canto superior esquerdo — herda uma aresta de dependência desse nó raiz. Os resultados escalares são propagados pelo retângulo da forma que a semântica legada de matrizes do Excel prescreve

O rastreamento de alterações (dirty tracking) intereta os métodos comuns de definição de propriedades (setters), pelo que nada muda no seu código. Escrever na propriedade Value de uma célula notifica o livro e marca os dependentes como alterados; atribuir uma nova Formula constitui uma alteração estrutural, pelo que marca o grafo completo como desatualizado, e o próximo Recalculate reconstrói-o antes de avaliar. Adicionar, eliminar ou mover folhas também invalida o grafo, já que a identidade do nó indica o índice da folha. Quando nenhum grafo se encontra ativo — num livro no qual nunca chama Recalculate —, estes ganchos (hooks) custam apenas uma verificação de nulo (nil check) por atribuição, garantindo que o desempenho de operações comuns de leitura/escrita não seja afetado

Um limite importante a referir de forma transparente: o grafo monitoriza dependências entre células, pelo que uma função definida pelo utilizador registada através de OnUserFunction é reavaliada quando as células que alimentam os seus argumentos mudam, tal como qualquer outra fórmula. Se estiver a estender o motor dessa forma, o artigo sobre funções personalizadas no motor de fórmulas do HotXLS detalha o contrato de callback e como os valores dos argumentos são fornecidos

O recálculo incremental faz parte do motor XLSX padrão no HotXLS Delphi Excel Component, juntamente com a calculadora de fórmulas, nomes definidos e o fluxo de importação/exportação que este acelera. Se a sua aplicação Delphi ou C++Builder mantém modelos dinâmicos — tabelas de preços, livros de consolidação ou fluxos de relatórios em cascata —, o método Recalculate dita a diferença entre reprocessar um livro completo e reprocessar uma única edição