Artículo técnico

Comparar dos libros de Excel en Delphi con HotXLS

HotXLS compara dos libros mediante TXLSXWorkbookCompare, que empareja las hojas de cálculo por nombre, recorre las celdas pobladas de cada par, e informa de lo que difiere como una lista estructurada de registros de diferencia y, si se solicita, como una línea legible por diferencia. No interviene ninguna instalación de Excel, y la comparación se ejecuta enteramente sobre el modelo de objetos cargado en Delphi o C++Builder

La necesidad suele aparecer la primera vez que alguien pregunta qué cambió. Un libro de finanzas vuelve de revisión, una exportación nocturna se regenera tras un cambio de código, o dos departamentos envían versiones de la misma plantilla. Abrir ambos lado a lado funciona para una hoja y falla para veinte. Comparar archivos byte a byte no responde nada en absoluto, porque dos guardados del mismo libro difieren de maneras que a nadie le importan

¿Qué cuenta como diferencia?

La comparación informa de ocho tipos, y el conjunto es deliberadamente pequeño: una hoja añadida o eliminada, una celda poblada añadida o eliminada, una celda cuyo valor cambió, una celda cuya fórmula cambió, y un rango combinado añadido o eliminado. Todo se expresa respecto al libro izquierdo como línea base, así que un elemento añadido existe solo a la derecha y un elemento eliminado solo a la izquierda

Las hojas se emparejan por nombre en lugar de por posición. Reordenar las hojas de cálculo, por tanto, no produce ninguna diferencia en absoluto, que es casi siempre el comportamiento deseado: que un usuario arrastre una pestaña no es un cambio de datos. Una hoja presente solo en un lado informa de una única entrada a nivel de hoja en lugar de expandir cada celda poblada dentro de ella, lo que mantiene legible el informe de dos libros estructuralmente distintos en lugar de miles de líneas

Valor o fórmula, y cómo se compara cada uno

Cada celda aporta una firma, y la regla es sencilla: una celda que lleva una fórmula se compara por su texto de fórmula con un signo igual inicial, y una celda sin fórmula se compara por su valor convertido a texto. Esa distinción importa más de lo que parece a primera vista. Dos celdas pueden mostrar el mismo número visible mientras una es un literal y la otra una fórmula, y tratarlas como iguales ocultaría exactamente la edición más importante de detectar en un libro revisado

También significa que una fórmula cuyo texto no ha cambiado no informa de ninguna diferencia aunque su resultado en caché difiera, que es el comportamiento correcto para comparar contenido redactado, y el comportamiento incorrecto si se intenta detectar una deriva de recálculo. Para esa segunda cuestión, recalcule ambos libros antes de comparar, de modo que los valores que compare sean los que las fórmulas realmente producen hoy

Ejecutar una comparación

Compare recibe los dos libros cargados y devuelve el número de diferencias encontradas. La lista de diferencias queda entonces disponible por índice, o se puede volcar en cualquier TStrings:

uses
  lxHandleX, lxCompare;

var
  Left, Right: TXLSXWorkbook;
  Cmp: TXLSXWorkbookCompare;
  Lines: TStringList;
begin
  Left := TXLSXWorkbook.Create;
  Right := TXLSXWorkbook.Create;
  Cmp := TXLSXWorkbookCompare.Create;
  Lines := TStringList.Create;
  try
    if (Left.Open('baseline.xlsx') <> 1) or
       (Right.Open('reviewed.xlsx') <> 1) then
      Exit;

    if Cmp.Compare(Left, Right) = 0 then
      Writeln('workbooks are equivalent')
    else
    begin
      Cmp.Report(Lines);              // una línea legible por diferencia
      Lines.SaveToFile('workbook-diff.txt');
      Writeln(Format('%d difference(s) written', [Cmp.Count]));
    end;
  finally
    Lines.Free;
    Cmp.Free;
    Right.Free;
    Left.Free;
  end;
end;

Una línea producida por Report se lee como value changed: Data!A2: 10 -> 99, lo cual es suficiente para un revisor y suficiente para un mensaje de commit. Esa es la superficie orientada a personas. La superficie programática es el propio registro de diferencia, y es la que hay que usar cuando la comparación alimenta una decisión en lugar de un documento

Dirigir la lógica a partir de las diferencias estructuradas

Cada diferencia expone su tipo, el nombre de la hoja, la fila y la columna en base uno para entradas a nivel de celda, una referencia A1 para las entradas a nivel de combinación, y el texto izquierdo y derecho. Las entradas a nivel de hoja y de combinación informan de fila y columna como cero, que es cómo se distinguen sin inspeccionar el tipo:

var
  I: Integer;
  D: TlxCompareDiff;
  FormulaEdits: Integer;
begin
  FormulaEdits := 0;
  for I := 0 to Cmp.Count - 1 do
  begin
    D := Cmp.Diff(I);
    case D.Kind of
      lckFormulaChanged:
        begin
          Inc(FormulaEdits);
          Writeln(Format('%s R%dC%d: %s => %s',
            [D.Sheet, D.Row, D.Col, D.LeftText, D.RightText]));
        end;
      lckSheetAdded, lckSheetRemoved:
        Writeln(Format('structure: %s', [D.Describe]));
      lckMergeAdded, lckMergeRemoved:
        Writeln(Format('layout: %s at %s', [D.Describe, D.Ref]));
    end;
  end;

  // Una política de revisión que solo bloquea ante ediciones de fórmula
  if FormulaEdits > 0 then
    raise Exception.CreateFmt(
      '%d formula change(s) need sign-off', [FormulaEdits]);
end;

Dos propiedades de la salida vale la pena conocer antes de escribir aserciones contra ella. El orden de las entradas a nivel de celda sigue el orden de recorrido interno del almacén de celdas, así que las pruebas deberían escribirse de forma independiente del orden. Y la firma de fórmula lleva su propio signo igual inicial, lo que significa que una cadena de descripción construida por concatenación puede mostrar un == duplicado; compruebe los valores de campo en lugar de analizar la línea descriptiva cuando el resultado dirige la lógica

Dónde da sus frutos la comparación de libros

Tres usos justifican la función por sí solos. Pruebas de regresión de un generador de informes: conservar un libro que se sabe correcto, regenerar, comparar, y hacer fallar la compilación ante cualquier diferencia inesperada. Revisión de cambios: entregar a un revisor el informe legible en lugar de dos archivos. Y verificación de migración: tras convertir un lote de libros heredados, comparar cada resultado con su origen para demostrar que no se perdió nada

Ese tercer caso combina de forma natural con las pasadas de inventario y auditoría descritas en el banco de trabajo de auditoría y conversión de libros, donde contar lo que contiene un libro ocurre antes de la conversión y la comparación ocurre después. Si sus diferencias se agrupan en torno a filas insertadas, las reglas de reescritura de referencias de el ajuste de referencias de fórmula al insertar y eliminar explican por qué fórmulas que parecen inalteradas se notifican como cambiadas

Los límites, expuestos con claridad

La comparación cubre valores, fórmulas, combinaciones y presencia de hojas. No compara formatos numéricos, fuentes, rellenos, reglas de formato condicional, validaciones de datos, gráficos, imágenes o nombres definidos. Una celda cuyo valor es idéntico pero cuyo formato cambió de General a Moneda no informa de ninguna diferencia, lo cual es correcto para una comparación de datos e insuficiente para una revisión de formato

Las celdas con valor de fecha merecen una advertencia específica: se comparan por su conversión a texto, así que un libro almacenado en el sistema de fechas de 1904 y otro en el sistema de 1900 pueden compararse como iguales o distintos de formas que sorprenden si los números de serie subyacentes difieren. Las reglas del sistema de fechas se tratan en los números de serie de fecha y el sistema de 1904. Cuando el formato o la fidelidad a nivel de objeto forman parte de la cuestión, combine la comparación con una pasada de auditoría que cuente esas funciones en cada lado

La comparación, la auditoría y la conversión de libros se ejecutan todas sobre el mismo motor para Delphi y C++Builder; la lista completa de funciones está en la página del componente de hojas de cálculo para Delphi de HotXLS