Artykuł techniczny

Porównywanie dwóch skoroszytów Excela w Delphi za pomocą HotXLS

HotXLS porównuje dwa skoroszyty za pomocą TXLSXWorkbookCompare, które paruje arkusze po nazwie, przechodzi wypełnione komórki każdej pary i zgłasza różnice jako uporządkowaną listę rekordów różnic oraz, na żądanie, jako jeden czytelny wiersz na różnicę. Nie jest wymagana żadna instalacja Excela, a porównanie działa całkowicie na wczytanym modelu obiektowym w Delphi lub C++Builder

Ta potrzeba zwykle pojawia się, gdy ktoś po raz pierwszy pyta, co się zmieniło. Skoroszyt finansowy wraca z przeglądu, nocny eksport jest regenerowany po zmianie kodu, albo dwa działy wysyłają wersje tego samego szablonu. Otwarcie obu obok siebie działa dla jednego arkusza i zawodzi dla dwudziestu. Porównywanie plików bajt po bajcie nie odpowiada na nic, ponieważ dwa zapisy tego samego skoroszytu różnią się w sposoby, którymi nikt się nie przejmuje

Co liczy się jako różnica?

Porównanie zgłasza osiem rodzajów różnic, a zbiór jest celowo mały: dodany lub usunięty arkusz, dodana lub usunięta wypełniona komórka, komórka, której wartość się zmieniła, komórka, której formuła się zmieniła, oraz dodany lub usunięty scalony zakres. Wszystko jest wyrażane względem lewego skoroszytu jako punktu odniesienia, więc element dodany istnieje tylko po prawej stronie, a usunięty tylko po lewej

Arkusze są parowane po nazwie, a nie po pozycji. Zmiana kolejności arkuszy nie daje więc w ogóle żadnych różnic, co niemal zawsze jest pożądanym zachowaniem: przeciągnięcie karty przez użytkownika nie jest zmianą danych. Arkusz obecny tylko po jednej stronie zgłasza jeden wpis na poziomie arkusza zamiast rozwijać każdą wypełnioną komórkę w jego wnętrzu, co utrzymuje raport dwóch strukturalnie różnych skoroszytów czytelny zamiast rozciągniętego na tysiące wierszy

Wartość czy formuła i jak każda z nich jest porównywana

Każda komórka wnosi sygnaturę, a reguła jest prosta: komórka niosąca formułę jest porównywana po tekście formuły z wiodącym znakiem równości, a komórka bez formuły jest porównywana po swojej wartości przekonwertowanej na tekst. To rozróżnienie ma większe znaczenie, niż wydaje się na pierwszy rzut oka. Dwie komórki mogą wyświetlać tę samą liczbę, gdy jedna jest literałem, a druga formułą, a potraktowanie ich jako równych ukryłoby dokładnie tę edycję, którą najbardziej warto wychwycić w recenzowanym skoroszycie

Oznacza to też, że formuła, której tekst się nie zmienił, nie zgłasza różnicy, nawet jeśli jej zbuforowany wynik jest inny, co jest poprawnym zachowaniem przy porównywaniu autorskiej treści, a błędnym, gdy próbuje się wykryć dryf ponownego przeliczenia. Dla tego drugiego pytania przelicz oba skoroszyty przed porównaniem, tak by porównywane wartości były tymi, które formuły faktycznie dają dzisiaj

Wykonywanie porównania

Compare przyjmuje dwa wczytane skoroszyty i zwraca liczbę znalezionych różnic. Lista różnic jest następnie dostępna po indeksie albo można ją zrzucić do dowolnego 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);              // jeden czytelny wiersz na różnicę
      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;

Wiersz wygenerowany przez Report wygląda jak value changed: Data!A2: 10 -> 99, co wystarcza dla recenzenta i wystarcza na wiadomość commita. To powierzchnia zwrócona ku człowiekowi. Powierzchnią programistyczną jest sam rekord różnicy i to jego należy używać, gdy porównanie zasila decyzję, a nie dokument

Sterowanie logiką na podstawie uporządkowanych różnic

Każda różnica udostępnia swój rodzaj, nazwę arkusza, liczony od jednego wiersz i kolumnę dla wpisów na poziomie komórki, odwołanie A1 dla wpisów na poziomie scalenia oraz tekst lewy i prawy. Wpisy na poziomie arkusza i na poziomie scalenia zgłaszają wiersz i kolumnę jako zero, po czym można je odróżnić bez sprawdzania rodzaju:

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;

  // Zasada przeglądu blokująca tylko przy edycjach formuł
  if FormulaEdits > 0 then
    raise Exception.CreateFmt(
      '%d formula change(s) need sign-off', [FormulaEdits]);
end;

Dwie właściwości wyniku warto znać, zanim napiszesz wobec niego asercje. Kolejność wpisów na poziomie komórki podąża za wewnętrzną kolejnością przechodzenia magazynu komórek, więc testy powinny być pisane niezależnie od kolejności. A sygnatura formuły niesie własny wiodący znak równości, co oznacza, że ciąg opisowy zbudowany przez konkatenację może pokazać podwójne ==; gdy wynik steruje logiką, sprawdzaj wartości pól, a nie parsuj wiersza opisowego

Gdzie porównywanie skoroszytów się opłaca

Trzy zastosowania same w sobie uzasadniają tę funkcję. Testowanie regresyjne generatora raportów: zachowaj znany dobry skoroszyt, zregeneruj, porównaj i zawal kompilację przy każdej nieoczekiwanej różnicy. Przegląd zmian: przekaż recenzentowi czytelny raport zamiast dwóch plików. I weryfikacja migracji: po konwersji partii starych skoroszytów porównaj każdy wynik z jego źródłem, aby udowodnić, że nic nie zginęło

Ten trzeci przypadek naturalnie łączy się z przebiegami inwentaryzacji i audytu opisanymi w warsztacie audytu i konwersji skoroszytów, gdzie liczenie zawartości skoroszytu odbywa się przed konwersją, a porównanie po niej. Jeśli różnice skupiają się wokół wstawionych wierszy, reguły przepisywania odwołań w dostosowywaniu odwołań formuł przy wstawianiu i usuwaniu wyjaśniają, dlaczego formuły wyglądające na niezmienione zgłaszają się jako zmienione

Ograniczenia, powiedziane wprost

Porównanie obejmuje wartości, formuły, scalenia i obecność arkuszy. Nie porównuje formatów liczb, fontów, wypełnień, reguł formatowania warunkowego, sprawdzania poprawności danych, wykresów, obrazów ani nazw zdefiniowanych. Komórka o identycznej wartości, ale ze zmienionym formatem z Ogólnego na Walutowy nie zgłasza różnicy, co jest poprawne dla porównania danych, a niewystarczające dla przeglądu formatowania

Komórki z wartościami daty zasługują na jedną konkretną przestrogę: są porównywane po swojej konwersji tekstowej, więc skoroszyt zapisany w systemie dat 1904 i taki w systemie 1900 mogą porównać się jako równe albo różne w sposób, który zaskoczy, jeśli leżące u podstaw liczby seryjne się różnią. Reguły systemu dat są opisane w liczbach seryjnych dat i systemie 1904. Gdy częścią pytania jest wierność formatowania lub wierność na poziomie obiektów, połącz diff z przebiegiem audytu, który liczy te funkcje po każdej stronie

Porównywanie skoroszytów, audyt i konwersja działają na tym samym silniku dla Delphi i C++Builder; pełna lista funkcji znajduje się na stronie komponentu arkusza kalkulacyjnego HotXLS dla Delphi