Pongan =VLOOKUP(A1,B:B,1) en una celda de la columna B y Excel la calcula sin queja. Pasen el mismo libro a un motor de recálculo por grafo de dependencias y lo más probable es que reciban un error de referencia circular, porque la fórmula depende de un rango que contiene la fórmula. HotXLS reportaba exactamente 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 corriente no tiene
El argumento lookup-array de la familia de búsquedas, LOOKUP, MATCH, HLOOKUP, VLOOKUP, XLOOKUP y XMATCH, ahora se marca 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 desaparecieron
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 cacheados y devuelve una coincidencia; no exige que el rango se haya evaluado por completo primero. Excel trata un rango de búsqueda que se solapa a sí mismo como una lectura de lo que esas celdas contienen ahora, que es la misma semántica que aplica a cualquier libro no iterativo: las celdas que no se recalcularon 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 lo hacen constantemente, por lo general sin que nadie note el solapamiento
Qué hace un grafo de dependencias con la misma fórmula
HotXLS recalcula incrementalmente, lo que exige un grafo de dependencias real: 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 la razón de que apareciera el falso positivo
Extraigan 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 programarlo, 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 lookup-array de una de las seis funciones. Aguas abajo, las aristas que nacen de esas referencias se almacenan aparte de las aristas ordinarias: el nodo del grafo conserva listas ScanDependents y ScanPrecedents junto a sus listas normales de dependientes y precedentes
La separación es lo que vuelve correcta la semántica. Las aristas de escaneo las recorre la propagación de suciedad, así que una edición en cualquier punto de B:B sigue marcando B7 como sucia y B7 recalcula. Las aristas de escaneo nunca cuentan en el grado de entrada y nunca entran al 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 workspace entre libros que lleva el análisis de componentes, se cambiaron juntas; dejarlas divergir produciría un libro que recalcula de manera distinta según se abra 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;
Qué ceden al excluir las aristas de escaneo del ordenamiento
Exactamente una cosa, y vale enunciarla con claridad 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 ante un valor aún no recalculado en la pasada actual es el último valor calculado, así que un motor que reproduce este comportamiento está igualando a la implementación de referencia y no aproximándola. Si necesitan una respuesta genuinamente convergida sobre un modelo autorreferente, 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 solapamientos 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 lugar 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 que actualizarse para asignar el campo nuevo explícitamente. Olvidar uno hace que el byte que casualmente estuviera en la pila decida 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 error latente. Dos modismos seguros:
var
R: TXLSDepRange;
begin
FillChar(R, SizeOf(R), 0); // poner todo a cero y luego llenar
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 ganó: añadir un campo a un record que se construye en la pila en más de un puñado de lugares es un cambio de más riesgo de lo que parece, y el compilador no los ayudará a encontrar los sitios. Si el record es alcanzable desde una ruta caliente, prefieran un ayudante que lo inicialice por completo antes que confiar en que cada sitio de llamada se actualice
Distinguir un ciclo real de un solapamiento de escaneo
Nada de 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 reportando por el resultado del recálculo con los miembros del ciclo conservando sus valores cacheados anteriores mientras todo lo que está fuera del ciclo se mantiene actualizado. Lo único que cambió es que el argumento lookup-array ya no fabrica ciclos que Excel no ve
Si están auditando un libro y quieren saber qué referencias resolvió realmente el motor y en qué orden, el trazador de evaluación es la herramienta; el artículo del trazador 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 del producto HotXLS Delphi spreadsheet component