Artículo técnico

Leer valores en caché de fórmulas de Excel sin recalcular

HotXLS, la biblioteca Excel nativa para Delphi y C++Builder, lee el valor que Excel ya almacenó junto a una fórmula a través de TryGetCachedFormulaValue e IXLSFormulaCacheReader. Ninguno de los dos puntos de entrada llama a la calculadora, descompila tokens de fórmula, actualiza estados dirty ni escribe nada de vuelta en el modelo, así que un libro que solo lees se queda exactamente como lo abriste

El escenario que motiva esto es sordo y extremadamente común. Un trabajo nocturno abre unos cuantos cientos de libros producidos por otra persona, saca una columna de totales de cada uno y empuja los números a un almacén de datos. Los totales ya están en los archivos — Excel los calculó y los guardó. Sin embargo, en el momento en que el trabajo pide a una celda de fórmula su valor, una biblioteca que solo tiene una respuesta para esa pregunta construye un grafo de dependencias y evalúa toda la hoja, y un trabajo que debería estar limitado por E/S se convierte en un benchmark de cálculo

¿Por qué leer una celda de fórmula cuesta un recálculo completo?

Porque un getter de valor sobre una celda de fórmula es una petición de producir un valor, y la única forma universalmente correcta de producirlo es evaluar la fórmula. Ese es el valor por defecto correcto para una aplicación que edita libros, y el equivocado para una tubería que los extrae. Peor aún, la evaluación no está libre de efectos secundarios: escribe resultados en las celdas, cambia banderas dirty y puede resolverse de forma distinta a la aplicación productora cuando una función no tiene soporte o una referencia externa está rota. Un trabajo que le describiste a tu equipo de operaciones como de solo lectura produce en silencio un libro que ya no coincide con el del disco, y si algo lo guarda más tarde, el archivo del disco también cambia

La lectura de valores en caché es la otra mitad del contrato. Responde a una pregunta más estrecha — ¿qué almacenó aquí la aplicación productora? — y se niega a responder a ninguna otra. Cuando de verdad quieres números frescos, HotXLS sigue ofreciéndote recálculo incremental dirigido por un grafo de dependencias; el punto es que extracción y evaluación deberían ser dos llamadas distintas, no una única llamada que se comporta de dos maneras

Tres hechos ortogonales sobre una celda

Primero la conclusión: un valor de fórmula en caché lleva tres hechos independientes, y colapsarlos en un único Variant pierde información que necesitas. TXLSFormulaCacheInfo los mantiene separados como State, Kind y Value. TXLSFormulaCacheState registra la procedencia en cinco casos — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated y xlfcsInvalidated — mientras que TXLSFormulaCacheValueKind clasifica la carga como xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean o xlfcvError. Esta separación es la que permite reportar la presencia con honestidad: un blanco en caché, una cadena vacía en caché, un False en caché, un cero en caché y un error en caché son todos valores reales, así que la presencia nunca puede inferirse de VarIsEmpty o VarIsNull. TryGetCachedFormulaValue devuelve True solo para xlfcsLoaded y xlfcsCalculated, y aun así rellena un estado diagnosticable cuando devuelve False

El registro TXLSFormulaCacheInfo de HotXLS mantiene separados tres hechos ortogonales sobre una celda de fórmula: el State de procedencia en cinco casos, el Kind de carga en seis y el Value Variant, de modo que un blanco o False en caché nunca se confunde con una caché ausente
Procedencia, tipo de carga y valor de carga se quedan separados, que es la única forma de reportar un blanco, cero, cadena vacía o error en caché como el valor real que es
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row y Col son de base uno aquí
    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 qué falta el valor en caché?

Hay exactamente cuatro razones por las que TryGetCachedFormulaValue devuelve False, y el estado te dice cuál aplica. xlfcsNotFormula significa que la celda contiene un literal o nada, y que las coordenadas estén fuera de rango colapsa en la misma respuesta. xlfcsMissing significa que la celda realmente es una fórmula pero el productor no almacenó carga de valor para ella — un resultado común cuando un generador escribe fórmulas y deja que Excel rellene los resultados en la primera apertura. xlfcsInvalidated significa que el texto de la fórmula se reemplazó después de la carga, así que el valor que estaba ahí describe una expresión que ya no existe. xlfcsCalculated, en cambio, es un caso de éxito: marca un valor que tu propio código o el evaluador de HotXLS produjeron en esta sesión, en oposición a xlfcsLoaded, que vino del archivo

La honestidad sobre una caché ausente importa más que taparla. HotXLS se niega a inventar un valor, y al guardar es igual de estricto — solo xlfcsLoaded y xlfcsCalculated emiten un valor en caché, mientras que xlfcsMissing y xlfcsInvalidated escriben la fórmula sola en lugar de congelar un número rancio en el archivo. Eso te deja tres respuestas sanas en una tubería: saltarte la fila y registrar el hueco, recalcular deliberadamente ese libro y aceptar el coste, o evaluar y reconciliar. Si el número evaluado discrepa de lo que la aplicación productora habría escrito, el tracer de evaluación de fórmulas es la herramienta para descubrir dónde divergen los dos cálculos, en lugar de adivinar a partir del resultado

Un lector para los motores clásico, OOXML y ODF

Una tubería no debería importarle si el archivo que acaba de abrir era BIFF, OOXML u ODF. IXLSFormulaCacheReader es el único punto de entrada de solo lectura para los tres: tanto TXLSWorkbook.CreateFormulaCacheReader como TXLSXWorkbook.CreateFormulaCacheReader devuelven un adaptador ligero sobre la búsqueda dispersa de celdas que cada motor ya usa, con coordenadas de hoja, fila y columna idénticas de base 1. Las clases de libro deliberadamente no implementan la interfaz ellas mismas — una referencia de interfaz al libro cambiaría sus semánticas de propiedad y dejaría a los llamadores colarse por encima del lease de vida. En su lugar, destruir el libro limpia el puntero crudo dentro de ese lease, y cualquier lector que tu código aún sostenga lanza EXLSFormulaCacheReaderInvalidated en su siguiente consulta en lugar de desreferenciar memoria liberada. Es comprobación fail-fast de vida, no una garantía de concurrencia

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 corrió ninguna calculadora, ninguna bandera dirty se movió, Book queda igual
end;

Dónde viven realmente los bytes en caché

Para los archivos .xls clásicos la caché es el campo FormulaValue del registro Formula, ocho bytes descritos por [MS-XLS] §2.5.133. Cuando la palabra alta es igual a $FFFF la carga no es un double IEEE 754 sino una variante etiquetada, y la disposición es fácil de errar sutilmente: el tipo de variante está en val[0] y la carga boolean o BErr está en val[2], con val[1] indefinido. HotXLS antes leía la carga desde val[1], que es el tipo de desplazamiento de uno que solo aflora en los archivos específicos que guardan en caché un boolean o un error en lugar de un número. El lector y el escritor de fórmulas compartidas ahora concuerdan en los mismos desplazamientos, así que un TRUE en caché sobrevive intacto a una carga y guardado en lugar de decaer en ruido

El campo FormulaValue de ocho bytes de un registro Formula XLS clásico tal como lo lee HotXLS: un double IEEE 754 salvo que la palabra alta sea igual a FFFF, en cuyo caso el tipo de variante está en val cero y la carga Boolean o error en val dos
Cuando la palabra alta es FFFF el campo es una variante etiquetada, y la carga está en val[2] con val[1] indefinido, que es exactamente el byte que el lector tomaba

La fidelidad de tipos en los formatos de paquete es un problema aparte con su propia trampa. En OOXML el valor en caché cuelga del elemento c como <v>, con el atributo t nombrando el tipo según ECMA-376 Parte 1 §18.3.1.4. HotXLS lee t="e" directamente a un Variant varError y lo mapea de vuelta al texto de error estándar al guardar, así que los errores nunca se disfrazan de enteros ordinarios — pero el RTL de Delphi no te ayudará aquí, porque VarAsType(Integer, varError) lanza una excepción de conversión. La construcción que funciona establece TVarData.VType y TVarData.VError directamente. Las fechas siguen la misma disciplina en dirección contraria: t="d" y el tipo de valor de fecha ODF son declaraciones de tipo explícitas y se convierten en varDate, mientras que una caché numérica BIFF no lleva ninguna bandera de fecha y por tanto se queda en Double. HotXLS nunca adivina una fecha a partir del formato numérico de una celda, porque el formato numérico es presentación y la caché es datos. ODF añade un caso más que conviene conocer — office:value-type="void" expresa una caché presente pero sin valor, y como ODF no tiene tipo de valor de error, el texto con apariencia de error se preserva como texto en lugar de promoverse a 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;

¿Comparten sus valores en caché las fórmulas compartidas?

No, y asumir lo contrario es como un barrido acaba reportando el mismo número para una columna entera. Una fórmula compartida OOXML comparte la expresión y la optimización de almacenamiento solamente; cada celda miembro sigue poseyendo su propio <v>. HotXLS por tanto nunca propaga la caché del miembro raíz a un seguidor que llegó sin valor, y un seguidor que cargó como xlfcsMissing sigue reportando xlfcsMissing tras un guardado y reapertura. Si estás trabajando cómo se almacena y expande el grupo en primer lugar, la mecánica del atributo si de la fórmula compartida y su expansión se cubre por separado; para la lectura de caché, la regla se reduce a una línea — pregunta a cada celda, no te fíes de nada que no hayas pedido

Una vista de HotXLS de un grupo de fórmulas compartidas OOXML en el que el atributo si comparte solo la expresión y la disposición de almacenamiento, mientras cada celda miembro posee su propio valor en caché, así que un seguidor que cargó sin uno sigue reportando xlfcsMissing
El grupo comparte la expresión, no los números, así que la caché raíz nunca se propaga y un miembro que llegó sin valor sigue reportando ese hueco

La lectura de valores en caché, el lector unificado multi-motor y el motor de recálculo que puedes elegir no invocar se envían todos en el HotXLS Delphi Spreadsheet Component estándar para Delphi y C++Builder, sin dependencia de Excel ni de ningún servidor de automatización OLE; la página del producto lleva la referencia API completa de los puntos de entrada de libro y lector mostrados aquí