Artigo Técnico

Comparar Duas Pastas de Trabalho Excel em Delphi com o HotXLS

O HotXLS compara duas pastas de trabalho através de TXLSXWorkbookCompare, que emparelha folhas de cálculo por nome, percorre as células preenchidas de cada par, e reporta o que difere como uma lista estruturada de registos de diferença e, a pedido, como uma linha legível por diferença. Não está envolvida qualquer instalação do Excel, e a comparação corre inteiramente sobre o modelo de objetos carregado em Delphi ou C++Builder

A necessidade costuma aparecer da primeira vez que alguém pergunta o que mudou. Uma pasta de trabalho financeira volta de uma revisão, uma exportação noturna é regenerada depois de uma alteração de código, ou dois departamentos enviam versões do mesmo modelo. Abrir os dois lado a lado funciona para uma folha e falha para vinte. Comparar ficheiros byte a byte não responde a nada, porque duas gravações da mesma pasta de trabalho diferem de formas com que ninguém se preocupa

O que conta como diferença?

A comparação reporta oito tipos, e o conjunto é deliberadamente pequeno: uma folha acrescentada ou removida, uma célula preenchida acrescentada ou removida, uma célula cujo valor mudou, uma célula cuja fórmula mudou, e uma área unida acrescentada ou removida. Tudo é expresso em relação à pasta de trabalho da esquerda como referência, pelo que um item acrescentado só existe à direita e um item removido só à esquerda

As folhas são emparelhadas por nome, não por posição. Reordenar folhas de cálculo não produz, portanto, nenhuma diferença, o que é quase sempre o comportamento pretendido: um utilizador a arrastar um separador não é uma alteração de dados. Uma folha presente apenas de um dos lados reporta uma única entrada ao nível da folha em vez de expandir cada célula preenchida no seu interior, o que mantém o relatório de duas pastas de trabalho estruturalmente diferentes legível em vez de ter milhares de linhas

Valor ou fórmula, e como cada um é comparado

Cada célula contribui com uma assinatura, e a regra é simples: uma célula com uma fórmula compara-se pelo texto da fórmula com um sinal de igual inicial, e uma célula sem fórmula compara-se pelo seu valor convertido em texto. Essa distinção importa mais do que parece à primeira vista. Duas células podem apresentar o mesmo número visível enquanto uma é um literal e a outra uma fórmula, e tratá-las como iguais esconderia exatamente a edição mais importante de detetar numa pasta de trabalho revista

Também significa que uma fórmula cujo texto não mudou não reporta diferença, mesmo que o seu resultado em cache seja diferente, o que é o comportamento correto para comparar conteúdo criado, e o comportamento errado se o objetivo for detetar deriva de recálculo. Para essa segunda questão, recalcule ambas as pastas de trabalho antes de comparar, para que os valores comparados sejam os que as fórmulas realmente produzem hoje

Executar uma comparação

Compare recebe as duas pastas de trabalho carregadas e devolve o número de diferenças encontradas. A lista de diferenças fica então disponível por índice, ou pode ser volcada para qualquer 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);              // uma linha legível por diferença
      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;

Uma linha produzida por Report lê-se como value changed: Data!A2: 10 -> 99, o que chega para um revisor e chega para uma mensagem de commit. Essa é a face voltada para humanos. A face programática é o próprio registo de diferença, e é essa que se deve usar quando a comparação alimenta uma decisão em vez de um documento

Orientar lógica a partir das diferenças estruturadas

Cada diferença expõe o seu tipo, o nome da folha, a linha e a coluna baseadas em um para as entradas ao nível de célula, uma referência A1 para as entradas ao nível de área unida, e o texto da esquerda e da direita. As entradas ao nível de folha e ao nível de área unida reportam linha e coluna como zero, o que é a forma de as distinguir sem inspecionar o 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;

  // Uma política de revisão que só bloqueia em edições de fórmula
  if FormulaEdits > 0 then
    raise Exception.CreateFmt(
      '%d formula change(s) need sign-off', [FormulaEdits]);
end;

Vale a pena conhecer duas propriedades do resultado antes de escrever asserções contra ele. A ordem das entradas ao nível de célula segue a ordem de travessia interna do repositório de células, pelo que os testes devem ser escritos independentemente da ordem. E a assinatura de fórmula transporta o seu próprio sinal de igual inicial, o que significa que uma string de descrição construída por concatenação pode mostrar um == duplicado; verifique os valores dos campos em vez de analisar a linha descritiva quando o resultado orienta lógica

Onde a comparação de pastas de trabalho compensa

Três utilizações justificam a funcionalidade por si só. Testes de regressão de um gerador de relatórios: mantenha uma pasta de trabalho conhecida como correta, regenere, compare, e falhe a compilação em qualquer diferença inesperada. Revisão de alterações: entregue a um revisor o relatório legível em vez de dois ficheiros. E verificação de migração: depois de converter um lote de pastas de trabalho legadas, compare cada resultado com a sua origem para provar que nada se perdeu

Esse terceiro caso combina-se naturalmente com as passagens de inventário e auditoria descritas em a bancada de auditoria e conversão de pastas de trabalho, onde contar o que uma pasta de trabalho contém acontece antes da conversão e a comparação acontece depois. Se as suas diferenças se concentrarem em torno de linhas inseridas, as regras de reescrita de referências em ajuste de referências de fórmula ao inserir e apagar explicam por que razão fórmulas que parecem inalteradas reportam como alteradas

Os limites, ditos claramente

A comparação cobre valores, fórmulas, áreas unidas e presença de folhas. Não compara formatos de número, tipos de letra, preenchimentos, regras de formatação condicional, validações de dados, gráficos, imagens ou nomes definidos. Uma célula cujo valor é idêntico mas cujo formato mudou de Geral para Moeda não reporta diferença, o que é correto para uma comparação de dados e insuficiente para uma revisão de formatação

As células com valores de data merecem um aviso específico: comparam-se pela sua conversão em texto, pelo que uma pasta de trabalho guardada no sistema de datas de 1904 e outra no sistema de 1900 podem comparar-se como iguais ou diferentes de formas surpreendentes se os números de série subjacentes forem diferentes. As regras do sistema de datas estão descritas em números de série de data e o sistema de 1904. Quando a fidelidade de formatação ou ao nível de objeto faz parte da questão, combine a comparação com uma passagem de auditoria que conte essas funcionalidades em cada lado

A comparação, a auditoria e a conversão de pastas de trabalho correm todas no mesmo motor para Delphi e C++Builder; a lista completa de funcionalidades está na página do componente de folha de cálculo para Delphi HotXLS