Technical Article

Diff Two Excel Workbooks in Delphi with HotXLS

HotXLS compares two workbooks through TXLSXWorkbookCompare, which pairs worksheets by name, walks the populated cells of each pair, and reports what differs as a structured list of difference records and, on request, as one readable line per difference. No Excel installation is involved, and the comparison runs entirely on the loaded object model in Delphi or C++Builder

The need usually appears the first time someone asks what changed. A finance workbook comes back from review, a nightly export is regenerated after a code change, or two departments send versions of the same template. Opening both side by side works for one sheet and fails for twenty. Comparing files byte by byte answers nothing at all, because two saves of the same workbook differ in ways nobody cares about

What counts as a difference?

The comparison reports eight kinds, and the set is deliberately small: a sheet added or removed, a populated cell added or removed, a cell whose value changed, a cell whose formula changed, and a merged range added or removed. Everything is expressed against the left workbook as the baseline, so an added item exists only on the right and a removed item only on the left

Sheets are paired by name rather than by position. Reordering worksheets therefore produces no differences at all, which is almost always the behaviour you want: a user dragging a tab is not a data change. A sheet present on only one side reports one sheet-level entry rather than expanding every populated cell inside it, which keeps a report of two structurally different workbooks readable instead of thousands of lines long

Value or formula, and how each is compared

Each cell contributes a signature, and the rule is simple: a cell carrying a formula compares by its formula text with a leading equals sign, and a cell without one compares by its value converted to text. That distinction matters more than it first appears. Two cells can hold the same displayed number while one is a literal and the other a formula, and treating them as equal would hide exactly the edit most worth catching in a reviewed workbook

It also means a formula whose text is unchanged reports no difference even if its cached result differs, which is the correct behaviour for comparing authored content, and the wrong behaviour if you are trying to detect recalculation drift. For that second question, recalculate both workbooks before comparing, so the values you compare are the ones the formulas actually produce today

Running a comparison

Compare takes the two loaded workbooks and returns the number of differences found. The difference list is then available by index, or can be dumped into any 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);              // one readable line per difference
      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;

A line produced by Report reads like value changed: Data!A2: 10 -> 99, which is enough for a reviewer and enough for a commit message. That is the human-facing surface. The programmatic surface is the difference record itself, and it is the one to use when the comparison feeds a decision rather than a document

Driving logic from the structured differences

Each difference exposes its kind, the sheet name, one-based row and column for cell-level entries, an A1 reference for merge-level entries, and the left and right text. Sheet-level and merge-level entries report row and column as zero, which is how you tell them apart without inspecting the 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;

  // A review policy that only blocks on formula edits
  if FormulaEdits > 0 then
    raise Exception.CreateFmt(
      '%d formula change(s) need sign-off', [FormulaEdits]);
end;

Two properties of the output are worth knowing before you write assertions against it. The order of cell-level entries follows the internal traversal order of the cell store, so tests should be written order independently. And the formula signature carries its own leading equals sign, which means a description string built by concatenation can show a doubled ==; check the field values rather than parsing the descriptive line when the result drives logic

Where workbook diffing pays off

Three uses justify the feature on their own. Regression testing a report generator: keep a known-good workbook, regenerate, compare, and fail the build on any unexpected difference. Change review: hand a reviewer the readable report instead of two files. And migration verification: after converting a batch of legacy workbooks, compare each result against its source to prove nothing was lost

That third case pairs naturally with the inventory and audit passes described in the workbook audit and conversion workbench, where counting what a workbook contains happens before conversion and comparison happens after. If your differences cluster around inserted rows, the reference rewriting rules in formula reference adjustment on insert and delete explain why formulas that look unchanged report as changed

The limits, stated plainly

The comparison covers values, formulas, merges and sheet presence. It does not compare number formats, fonts, fills, conditional formatting rules, data validations, charts, images or defined names. A cell whose value is identical but whose format changed from General to Currency reports no difference, which is correct for a data comparison and insufficient for a formatting review

Date-valued cells deserve one specific caution: they compare as their text conversion, so a workbook stored on the 1904 date system and one on the 1900 system can compare equal or unequal in ways that surprise you if the underlying serial numbers differ. The date system rules are covered in date serial numbers and the 1904 system. When formatting or object-level fidelity is part of the question, combine the diff with an audit pass that counts those features on each side

Workbook comparison, auditing and conversion all run on the same engine for Delphi and C++Builder; the complete feature list is on the HotXLS Delphi spreadsheet component page