Poned =VLOOKUP(A1,B:B,1) en una celda de la columna B y Excel la calcula sin queja. Dadle el mismo libro a un motor de recálculo con grafo de dependencias y lo probable es que obtengáis un error de referencia circular, porque la fórmula depende de un rango que contiene la fórmula. HotXLS informaba exactamente de eso hasta la v2.361.98. El arreglo no es un caso especial para rangos de columna completa; es una distinción entre dos clases de arista de dependencia que un motor de hojas de cálculo necesita y que un grafo dirigido llano no tiene
El argumento de matriz de búsqueda de la familia de búsquedas, LOOKUP, MATCH, HLOOKUP, VLOOKUP, XLOOKUP y XMATCH, se marca ahora como referencia de escaneo. Una referencia de escaneo sigue sembrando suciedad, así que editar una celda dentro del rango recalcula la fórmula, pero nunca contribuye a la detección de ciclos ni al orden de evaluación. Los ciclos reales se siguen encontrando; los falsos han desaparecido
Por qué Excel permite que un rango de búsqueda contenga la fórmula?
Porque ese argumento no se consume como un operando aritmético. La familia de búsquedas escanea el rango en busca de valores en caché y devuelve una coincidencia; no exige que el rango se haya evaluado por completo antes. Excel trata un rango de búsqueda que se solapa consigo mismo como una lectura de lo que esas celdas contienen ahora mismo, que es la misma semántica que aplica a cualquier libro no iterativo: las celdas que no se han recalculado en esta pasada aportan su último valor calculado
Las referencias de columna completa hacen de esto el caso común y no uno exótico. B:B es la forma idiomática de escribir «toda la tabla de búsqueda» en una hoja donde se añaden filas, y cualquier fórmula que viva en la columna B queda entonces dentro de su propio rango de búsqueda. Los modelos financieros, las hojas de conciliación y los libros de auditoría hacen esto constantemente, normalmente sin que nadie note que el rango se solapa
Qué hace un grafo de dependencias con la misma fórmula
HotXLS recalcula incrementalmente, lo que exige un grafo de dependencias de verdad: nodos para celdas, aristas para referencias, un orden topológico para la evaluación y una pasada de componentes fuertemente conexos para clasificar ciclos. Esa maquinaria se describe en el artículo de recálculo incremental, y es precisamente por eso que apareció el falso positivo
Extraed las dependencias de =VLOOKUP(A1,B:B,1) en la celda B7 y el segundo argumento produce un rango que contiene a la propia B7. El grafo tiene ahora un self-loop. El grado de entrada de ese nodo nunca llega a cero, así que la pasada topológica nunca puede planificarlo, y la pasada de componentes lo clasifica como ciclo. El motor razona correctamente sobre el grafo que le dieron. El grafo es el modelo equivocado, porque codifica un tipo de arista donde la hoja de cálculo tiene dos
Dos clases de arista, un grafo
El cambio añade una bandera al record de referencia resuelta, TXLSDepRange.LookupScan, que el extractor de dependencias activa cuando recorre el argumento de matriz de búsqueda de una de las seis funciones. Aguas abajo, las aristas procedentes de esas referencias se almacenan aparte de las ordinarias: el nodo del grafo guarda listas ScanDependents y ScanPrecedents junto a sus listas normales de dependientes y precedentes
La separación es lo que hace correcta la semántica. Las aristas de escaneo se recorren por la propagación de suciedad, así que una edición en cualquier punto de B:B sigue marcando B7 como sucia y B7 se recalcula. Las aristas de escaneo nunca se cuentan en el grado de entrada y nunca entran en el constructor de componentes, así que no pueden crear un bloqueo topológico ni clasificarse como ciclo. Ambas implementaciones de grafo de la biblioteca, el grafo clásico por libro y el grafo de espacio de trabajo entre libros que lleva el análisis de componentes, se cambiaron juntas; dejarlas divergir produciría un libro que recalcula de forma distinta según se abriera solo o como parte de un workspace
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Ledger');
Sheet.Cells[1, 1].Value := 'ACC-4471';
Sheet.Cells[1, 2].Value := 1200.00;
// El rango de búsqueda cubre la columna B, y esta fórmula vive en ella
Sheet.Cells[7, 2].Formula := 'VLOOKUP(A1,B:B,1)';
case Book.Recalculate of
lxOk:
// Antes de v2.361.98 esta rama era inalcanzable para esta hoja
SaveReport(Book);
lxErrorRef:
LogWarning('Genuine circular reference - review model inputs');
end;
finally
Book.Free;
end;
end;
A qué renunciáis al excluir las aristas de escaneo del orden
Exactamente una cosa, y merece decirse llanamente en lugar de esconderla. Como las aristas de escaneo no participan en el orden topológico, una fórmula de búsqueda puede evaluarse en la misma pasada antes de que algunas celdas de su rango de búsqueda se hayan recalculado, y entonces leerá sus valores anteriores. El resultado converge en el siguiente recálculo
Eso es aceptable porque es lo que hace Excel. Para un libro sin cálculo iterativo activado, la propia respuesta de Excel para un valor aún no recalculado en la pasada actual es el último valor calculado, así que un motor que reproduce este comportamiento iguala a la implementación de referencia en lugar de aproximarla. Si necesitáis una respuesta genuinamente convergente sobre un modelo autorreferencial, el mecanismo para eso es el cálculo iterativo con un límite de iteración explícito, cubierto en el artículo de cálculo iterativo, y aplica a ciclos reales y no a solapes de escaneo
El riesgo de regresión escondido dentro del arreglo
Añadir LookupScan a TXLSDepRange introdujo un riesgo que no tiene nada que ver con búsquedas y todo que ver con Pascal. TXLSDepRange es un record no gestionado, así que una variable local de ese tipo no se inicializa a cero. Todo sitio de la base de código que construya uno a mano, incluidos los bloques de dependencias de tablas de datos y varios ayudantes de prueba, tuvo por tanto que actualizarse para asignar el campo nuevo explícitamente. Perder uno y el byte que hubiera en la pila decide si esa referencia se trata como arista de escaneo, lo que produce un error de recálculo que aparece y desaparece con cambios de código ajenos
// Un campo Boolean nuevo en un record no gestionado convierte cada
// sitio de construcción manual en un bug latente. Dos modismos seguros:
var
R: TXLSDepRange;
begin
FillChar(R, SizeOf(R), 0); // poner todo a cero y luego rellenar
R.Sheet1 := SheetIndex;
R.Sheet2 := SheetIndex;
R.Row1 := Row; R.Col1 := Col;
R.Row2 := Row; R.Col2 := Col;
// o asignar cada campo, incluido el nuevo, en cada sitio
R.LookupScan := False;
end;
La regla general que esto se ganó: añadir un campo a un record que se construye en la pila en más de un puñado de sitios es un cambio de más riesgo del que parece, y el compilador no os ayudará a encontrar los sitios. Si el record es alcanzable desde una ruta caliente, preferid un ayudante que lo inicialice por completo a confiar en que cada punto de llamada se actualice
Distinguir un ciclo real de un solape de escaneo
Nada en este cambio debilita la detección de ciclos. =B7+1 en B7 sigue siendo un ciclo, una cadena de tres fórmulas que se cierra sobre sí misma sigue siendo un ciclo, y ambos se siguen informando a través del resultado de recálculo, con los miembros del ciclo conservando sus valores anteriores en caché mientras todo lo de fuera del ciclo se mantiene actualizado. Lo único que cambió es que el argumento de matriz de búsqueda ya no fabrica ciclos que Excel no ve
Si estáis auditando un libro y queréis saber qué referencias resolvió realmente el motor y en qué orden, el tracer de evaluación es la herramienta para ello; el artículo del tracer de evaluación de fórmulas cubre cómo leer su salida. HotXLS es un componente de hojas de cálculo nativo para Delphi y C++Builder que lee y escribe XLS, XLSX, ODS y CSV sin Excel instalado, y el motor de recálculo es el mismo en todos los formatos; la cobertura actual de funciones y motor está en la página de producto de HotXLS Delphi spreadsheet component