O HotXLS, a biblioteca Excel nativa para Delphi e C++Builder, lê o valor que o Excel já guardou ao lado de uma fórmula através de TryGetCachedFormulaValue e IXLSFormulaCacheReader. Nenhum dos pontos de entrada chama a calculadora, descompila tokens de fórmula, atualiza o estado dirty ou escreve qualquer coisa de volta no modelo, pelo que um livro que só lê fica exatamente como o abriu
O cenário que motiva isto é aborrecido e extremamente comum. Um trabalho noturno abre algumas centenas de livros produzidos por outra pessoa, retira uma coluna de totais de cada um e empurra os números para um armazém de dados. Os totais já estão nos ficheiros — o Excel calculou-os e guardou-os. Ainda assim, no momento em que o trabalho pede o valor de uma célula de fórmula, uma biblioteca que só tem uma resposta para essa questão constrói um grafo de dependências e avalia a folha inteira, e um trabalho que devia ser limitado por E/S transforma-se numa referência de desempenho de cálculo
Porque é 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 a única forma universalmente correta de produzir um é avaliar a fórmula. Esse é o predefinido certo para uma aplicação que edita livros, e o predefinido errado para um pipeline que os extrai. Pior, a avaliação não está isenta de efeitos secundários: escreve resultados de volta nas células, altera flags dirty, e pode resolver de forma diferente da aplicação produtora quando uma função não é suportada ou uma referência externa está partida. Um trabalho que descreveu à sua equipa de operações como só de leitura produz silenciosamente um livro que já não corresponde ao que está em disco, e se algo o guardar mais tarde, o ficheiro em disco também muda
A leitura de valores em cache é a outra metade do contrato. Responde a uma questão mais estreita — o que é que a aplicação produtora guardou aqui? — e recusa-se a responder a qualquer outra coisa. Quando quer genuinamente números frescos, o HotXLS continua a dar-lhe o recálculo incremental conduzido por um grafo de dependências; o ponto é que extração e avaliação deviam ser duas chamadas diferentes, não uma chamada com dois humores
Três factos ortogonais sobre uma célula
Primeiro a conclusão: um valor de fórmula em cache transporta três factos independentes, e colapsá-los num único Variant perde informação de que precisa. O TXLSFormulaCacheInfo mantém-nos separados como State, Kind e Value. O TXLSFormulaCacheState regista a proveniência em cinco casos — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated e xlfcsInvalidated — enquanto o TXLSFormulaCacheValueKind classifica a carga útil como xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean ou xlfcvError. Esta separação é o que permite reportar a presença honestamente: um vazio em cache, uma cadeia vazia em cache, um False em cache, um zero em cache e um erro em cache são todos valores reais, pelo que a presença nunca pode ser inferida de VarIsEmpty ou VarIsNull. O TryGetCachedFormulaValue devolve True apenas para xlfcsLoaded e xlfcsCalculated, e mesmo assim preenche um estado diagnosticável quando devolve False
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;
Porque é que o valor em cache está em falta?
Há exatamente quatro razões para o TryGetCachedFormulaValue devolver False, e o estado diz-lhe qual se aplica. xlfcsNotFormula significa que a célula contém um literal ou nada de todo, e coordenadas fora do intervalo colapsam na mesma resposta. xlfcsMissing significa que a célula é realmente uma fórmula mas o produtor não guardou nenhuma carga útil de valor para ela — um desfecho comum quando um gerador escreve fórmulas e deixa o Excel preencher os resultados na primeira abertura. xlfcsInvalidated significa que o texto da fórmula foi substituído depois do carregamento, pelo que o valor que lá estava descreve uma expressão que já não existe. xlfcsCalculated, em contrapartida, é um caso de sucesso: marca um valor que o seu próprio código ou o avaliador do HotXLS produziu durante esta sessão, por oposição a xlfcsLoaded, que veio do ficheiro
A honestidade sobre uma cache em falta importa mais do que a tapar. O HotXLS recusa-se a inventar um valor, e ao guardar é igualmente rigoroso — só xlfcsLoaded e xlfcsCalculated emitem um valor em cache, enquanto xlfcsMissing e xlfcsInvalidated escrevem apenas a fórmula em vez de congelar um número obsoleto no ficheiro. Isso deixa-lhe três respostas sensatas num pipeline: saltar a linha e registar a lacuna, recalcular deliberadamente esse livro e aceitar o custo, ou avaliar e reconciliar. Se o número avaliado discordar do que a aplicação produtora teria escrito, o traçador de avaliação de fórmulas é a ferramenta para descobrir onde as duas computações divergem, em vez de adivinhar a partir do resultado
Um leitor para os motores clássico, OOXML e ODF
Um pipeline não devia importar-se se o ficheiro que acabou de abrir era BIFF, OOXML ou ODF. O IXLSFormulaCacheReader é o único ponto de entrada só de leitura para os três: tanto o TXLSWorkbook.CreateFormulaCacheReader como o TXLSXWorkbook.CreateFormulaCacheReader devolvem um adaptador leve sobre a pesquisa esparsa de células que cada motor já usa, com coordenadas idênticas de folha, linha e coluna base um. As classes de livro deliberadamente não implementam a interface elas próprias — uma referência de interface ao livro mudaria a sua semântica de posse e deixaria os chamadores escapar ao lease de tempo de vida. Em vez disso, destruir o livro limpa o ponteiro em bruto dentro desse lease, e qualquer leitor ainda mantido pelo seu código lança EXLSFormulaCacheReaderInvalidated na sua próxima consulta em vez de desreferenciar memória libertada. É verificação 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 correu, nenhuma flag dirty mudou, o Book fica inalterado
end;
Onde vivem realmente os bytes em cache
Para ficheiros .xls clássicos a cache é o campo FormulaValue do registo Formula, oito bytes descritos pelo [MS-XLS] §2.5.133. Quando a palavra alta é igual a $FFFF a carga útil não é um double IEEE 754 mas um variant etiquetado, e a disposição é fácil de errar subtilmente: o tipo do variant está em val[0] e a carga útil booleana ou BErr está em val[2], com val[1] indefinido. O HotXLS lia anteriormente a carga útil de val[1], que é o tipo de erro de um que só aparece nos ficheiros específicos que colocam em cache um booleano ou um erro em vez de um número. O leitor e o escritor de fórmulas partilhadas agora concordam nos mesmos desvios, pelo que um TRUE em cache sobrevive intacto a um carregamento e gravação em vez de decair em ruído
A fidelidade de tipos nos formatos de pacote é um problema separado com a sua própria armadilha. No OOXML o valor em cache pende do elemento c como <v>, com o atributo t a nomear o tipo conforme a ECMA-376 Parte 1 §18.3.1.4. O HotXLS lê t="e" diretamente num Variant varError e mapeia-o de volta para o texto de erro padrão ao guardar, pelo que os erros nunca se disfarçam de inteiros ordinários — mas a RTL do Delphi não o ajuda aqui, porque VarAsType(Integer, varError) lança uma exceção de conversão. A construção que funciona define TVarData.VType e TVarData.VError diretamente. As datas seguem a mesma disciplina na direção oposta: t="d" e o tipo de valor de data ODF são declarações de tipo explícitas e tornam-se varDate, enquanto uma cache numérica BIFF não transporta flag de data nenhuma e por isso permanece um Double. O HotXLS nunca adivinha uma data a partir do formato numérico de uma célula, porque o formato numérico é apresentação e a cache é dados. O ODF acrescenta mais um caso que vale a pena conhecer — office:value-type="void" expressa uma cache que está presente mas não transporta valor, e como o ODF não tem tipo de valor de erro, texto com aparência de erro é preservado como texto em vez de ser 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;
As fórmulas partilhadas partilham os seus valores em cache?
Não, e assumir o contrário é como uma varredura acaba por reportar o mesmo número para uma coluna inteira. Uma fórmula partilhada OOXML partilha apenas a expressão da fórmula e a otimização de armazenamento; cada célula membro continua a ter o seu próprio <v>. O HotXLS por isso nunca propaga a cache do membro raiz a um seguidor que chegou sem valor, e um seguidor que carregou como xlfcsMissing continua a reportar xlfcsMissing depois de guardar e reabrir. Se está a trabalhar como o grupo é armazenado e expandido em primeiro lugar, a mecânica do atributo si de fórmula partilhada e a sua expansão está coberta separadamente; para leitura de cache, a regra reduz-se a uma linha — pergunte a cada célula, não confie em nada que não tenha pedido
A leitura de valores em cache, o leitor unificado entre motores e o motor de recálculo que pode escolher não invocar chegam todos no HotXLS Delphi Spreadsheet Component padrão para Delphi e C++Builder, sem dependência do Excel nem de qualquer servidor de automação OLE; a página do produto transporta a referência API completa para os pontos de entrada de livro e leitor aqui mostrados