技術記事

HotXLSでDelphiから2つのExcelワークブックを比較する

HotXLSは、TXLSXWorkbookCompareを通じて2つのワークブックを比較します。ワークシートを名前でペアにし、それぞれのペアのデータが入ったセルをたどり、何が違うのかを、差分レコードの構造化されたリストとして、また要求すれば差分ごとに1行の読みやすい形式でも報告します。Excelのインストールは一切不要であり、比較はDelphiまたはC++Builder上の読み込み済みオブジェクトモデルだけで完結します

この必要性は、たいてい誰かが「何が変わったのか」と尋ねた瞬間に生じます。財務のワークブックがレビューから戻ってきたとき、コード変更の後に夜間エクスポートが再生成されたとき、あるいは2つの部門が同じテンプレートの異なるバージョンを送ってきたときなどです。両方を並べて開く方法は、シート1枚なら通用しますが、20枚になると破綻します。ファイルをバイト単位で比較する方法は、何の答えにもなりません。同じワークブックを2回保存しただけでも、誰も気にしない理由で違いが生じるからです

何が差分として扱われるのか?

この比較は8種類の差分を報告しますが、この種類の数は意図的に絞り込まれています。シートの追加または削除、データの入ったセルの追加または削除、値が変わったセル、数式が変わったセル、結合範囲の追加または削除です。すべては左側のワークブックを基準として表現されるため、追加された項目は右側にのみ存在し、削除された項目は左側にのみ存在します

シートは位置ではなく名前でペアにされます。そのため、ワークシートの並べ替えはまったく差分を生みません。これはほとんどの場合、望ましい挙動です。ユーザーがタブをドラッグすることはデータの変更ではないからです。片方にしか存在しないシートは、その中のデータが入ったすべてのセルを展開するのではなく、シートレベルのエントリを1つだけ報告します。これにより、構造的に大きく異なる2つのワークブックのレポートも、何千行にもなることなく読みやすい状態に保たれます

値か数式か、それぞれどう比較されるのか

各セルは1つの署名を提供し、その規則はシンプルです。数式を持つセルは、先頭に等号を付けた数式のテキストで比較され、数式を持たないセルは、テキストに変換された値で比較されます。この区別は、見た目以上に重要です。2つのセルが同じ表示上の数値を持っていても、片方はリテラルでもう片方は数式である場合があり、それらを等しいものとして扱ってしまうと、レビュー対象のワークブックで最も見つける価値のある編集が、まさに隠れてしまいます

これはまた、キャッシュされた計算結果が異なっていても、テキストが変わっていない数式は差分なしと報告されることを意味します。これは、著者が記述した内容を比較するという観点では正しい挙動ですが、再計算によるずれを検出しようとしている場合には誤った挙動です。後者の目的であれば、比較する前に両方のワークブックを再計算してください。そうすれば、比較する値は、数式が今実際に生み出している値になります

比較を実行する

Compareは読み込み済みの2つのワークブックを受け取り、見つかった差分の数を返します。差分のリストはインデックスでアクセスできるほか、任意の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);              // 差分ごとに読みやすい1行
      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;

Reportが生成する行は、value changed: Data!A2: 10 -> 99のような形式であり、レビュアーにとっても、コミットメッセージにとっても十分な情報量です。これが人間向けの側面です。プログラム向けの側面は差分レコードそのものであり、比較の結果が文書ではなく判断に使われる場合には、こちらを使うべきです

構造化された差分からロジックを駆動する

各差分は、種類、シート名、セルレベルのエントリでは1始まりの行と列、結合セルレベルのエントリではA1形式の参照、そして左右それぞれのテキストを公開します。シートレベルと結合セルレベルのエントリは、行と列をゼロとして報告します。これが、Kindを調べなくてもそれらを区別できる方法です:

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;

  // 数式の編集だけをブロックするレビューポリシー
  if FormulaEdits > 0 then
    raise Exception.CreateFmt(
      '%d formula change(s) need sign-off', [FormulaEdits]);
end;

出力について、アサーションを書く前に知っておく価値のある性質が2つあります。セルレベルのエントリの順序は、セルストアの内部走査順に従うため、テストは順序に依存しない形で書くべきです。また、数式の署名は自身の先頭に等号を持つため、連結によって組み立てられた説明文字列には二重の==が現れることがあります。結果がロジックを駆動する場合は、説明用の行を解析するのではなく、フィールドの値を確認してください

ワークブックの差分比較が役立つ場面

この機能を単独でも正当化できる用途が3つあります。レポート生成器の回帰テストでは、既知の正しいワークブックを保持しておき、再生成し、比較して、予期しない差分があればビルドを失敗させます。変更レビューでは、レビュアーに2つのファイルではなく読みやすいレポートを渡します。そして移行の検証では、レガシーなワークブックのバッチを変換した後、各結果を元のファイルと比較して、何も失われなかったことを証明します

3つ目のケースは、ワークブックの監査と変換ワークベンチで説明した棚卸しと監査のパスと自然に組み合わさります。そこでは、ワークブックの内容を数え上げる処理が変換前に行われ、比較が変換後に行われます。差分が挿入された行の周辺に集中している場合は、挿入・削除時の数式参照調整で説明した参照の書き換え規則が、見た目には変わっていない数式がなぜ変更として報告されるのかを説明しています

制限事項をはっきり述べる

この比較がカバーするのは、値、数式、結合セル、シートの有無です。数値の書式、フォント、塗りつぶし、条件付き書式の規則、入力規則、チャート、画像、名前付き範囲は比較しません。値は同一だが書式が「標準」から「通貨」に変わったセルは、差分なしと報告されます。これはデータ比較としては正しい挙動ですが、書式のレビューとしては不十分です

日付値を持つセルには、1つ特有の注意点があります。テキストへの変換結果で比較されるため、1904年基準の日付システムで保存されたワークブックと1900年基準のワークブックは、内部のシリアル値が異なる場合、意外な形で等しいと判定されたり異なると判定されたりします。日付システムの規則については日付シリアル値と1904年システムで説明しています。書式やオブジェクトレベルの忠実度が問いの一部である場合は、差分比較と、両側でそれらの機能を数え上げる監査パスを組み合わせてください

ワークブックの比較、監査、変換は、いずれもDelphiおよび C++Builder向けの同じエンジン上で動作します。完全な機能一覧はHotXLS Delphiスプレッドシートコンポーネントページに掲載されています