기술 문서

HotXLS로 Delphi에서 두 엑셀 워크북 diff 하기

HotXLS는 TXLSXWorkbookCompare를 통해 두 워크북을 비교합니다. 이름으로 워크시트를 짝짓고, 각 쌍에서 값이 채워진 셀을 순회한 다음, 무엇이 다른지 구조화된 차이 레코드 목록으로, 요청이 있으면 차이마다 읽기 쉬운 한 줄로도 보고합니다. Excel 설치는 전혀 필요 없으며, 비교는 전적으로 Delphi나 C++Builder에서 로드된 객체 모델 위에서 실행됩니다

이 필요성은 대개 누군가 무엇이 바뀌었는지 물어볼 때 처음 드러납니다. 재무 워크북이 검토를 마치고 돌아오거나, 코드 변경 이후 야간 내보내기가 다시 생성되거나, 두 부서가 같은 템플릿의 서로 다른 버전을 보내옵니다. 두 파일을 나란히 펼쳐 보는 방법은 시트 하나에는 통하지만 스무 개에는 통하지 않습니다. 파일을 바이트 단위로 비교하는 방법은 아무것도 알려주지 않는데, 같은 워크북을 두 번 저장한 결과는 아무도 신경 쓰지 않는 방식으로 서로 달라지기 때문입니다

무엇을 차이로 칠까?

이 비교는 여덟 가지 종류를 보고하며, 그 집합은 의도적으로 작습니다. 시트 추가 또는 제거, 값이 채워진 셀 추가 또는 제거, 값이 바뀐 셀, 수식이 바뀐 셀, 병합 범위 추가 또는 제거입니다. 모든 것은 왼쪽 워크북을 기준선으로 표현되므로, 추가된 항목은 오른쪽에만, 제거된 항목은 왼쪽에만 존재합니다

시트는 위치가 아니라 이름으로 짝지어집니다. 그래서 워크시트 순서를 바꿔도 아무 차이도 생기지 않으며, 이는 거의 항상 원하는 동작입니다. 탭을 드래그하는 사용자는 데이터 변경이 아니기 때문입니다. 한쪽에만 있는 시트는 그 안의 채워진 셀 전부를 펼쳐 보이는 대신 시트 수준 항목 하나로 보고되며, 이는 구조적으로 완전히 다른 두 워크북의 리포트를 수천 줄짜리가 아니라 읽을 만하게 유지해 줍니다

값이냐 수식이냐, 그리고 각각을 비교하는 방식

셀마다 서명 하나를 제공하며, 규칙은 단순합니다. 수식을 담은 셀은 앞에 등호가 붙은 수식 텍스트로 비교하고, 수식이 없는 셀은 텍스트로 변환된 값으로 비교합니다. 이 구분은 처음 보이는 것보다 더 중요합니다. 두 셀이 같은 표시 숫자를 가지고 있는데 하나는 리터럴이고 다른 하나는 수식일 수 있으며, 이를 동일하다고 취급하면 검토 대상 워크북에서 가장 놓쳐서는 안 될 편집이 정확히 숨겨져 버립니다

이는 텍스트가 바뀌지 않은 수식은 캐시된 결과가 다르더라도 차이가 없다고 보고된다는 뜻이며, 작성된 콘텐츠를 비교하는 데는 올바른 동작이지만 재계산 드리프트를 탐지하려는 목적에는 잘못된 동작입니다. 후자를 확인하려면 비교 전에 두 워크북을 모두 재계산해서, 비교하는 값이 오늘 그 수식들이 실제로 만들어낸 값이 되도록 하십시오

비교 실행하기

Compare는 로드된 두 워크북을 받아 발견된 차이의 개수를 반환합니다. 이후 차이 목록은 인덱스로 접근할 수 있고, 임의의 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;

Report가 만들어내는 한 줄은 value changed: Data!A2: 10 -> 99처럼 읽히는데, 이는 검토자에게도 충분하고 커밋 메시지에도 충분합니다. 이것은 사람이 보는 표면이며, 프로그래밍적인 표면은 차이 레코드 자체로, 비교 결과가 문서가 아니라 의사 결정에 쓰일 때 사용할 대상입니다

구조화된 차이로 로직 구동하기

각 차이는 종류, 시트 이름, 셀 수준 항목이라면 1부터 시작하는 행과 열, 병합 수준 항목이라면 A1 참조, 그리고 좌우 텍스트를 노출합니다. 시트 수준과 병합 수준 항목은 행과 열을 0으로 보고하는데, 이는 종류를 일일이 확인하지 않고도 구분할 수 있는 방법입니다:

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;

결과에 대해 단언문을 작성하기 전에 알아둘 만한 속성이 두 가지 있습니다. 셀 수준 항목의 순서는 셀 저장소의 내부 순회 순서를 따르므로, 테스트는 순서에 의존하지 않게 작성해야 합니다. 그리고 수식 서명은 자기 앞에 등호를 그대로 달고 있기 때문에, 문자열 연결로 만든 설명 문자열은 ==가 두 번 겹쳐 보일 수 있습니다. 결과가 로직을 구동한다면 설명용 줄을 파싱하는 대신 필드 값을 확인하십시오

워크북 diff가 빛을 발하는 곳

세 가지 용도만으로도 이 기능은 그 자체로 정당화됩니다. 리포트 생성기의 회귀 테스트: 정답 워크북을 보관하고, 다시 생성하고, 비교하고, 예상치 못한 차이가 있으면 빌드를 실패시킵니다. 변경 검토: 파일 두 개 대신 읽기 쉬운 리포트를 검토자에게 건넵니다. 그리고 마이그레이션 검증: 레거시 워크북 묶음을 변환한 뒤 각 결과를 원본과 비교해 아무것도 유실되지 않았음을 증명합니다

세 번째 용도는 워크북 감사 및 변환 워크벤치에서 설명하는 인벤토리와 감사 패스와 자연스럽게 짝을 이루며, 워크북이 담고 있는 것을 세는 작업은 변환 전에, 비교는 변환 후에 이루어집니다. 차이가 삽입된 행 주변에 몰려 있다면, 삽입과 삭제 시의 수식 참조 조정에서 변하지 않아 보이는 수식이 왜 변경으로 보고되는지 설명합니다

솔직하게 말하는 한계

이 비교는 값, 수식, 병합, 시트 존재 여부를 다룹니다. 숫자 서식, 글꼴, 채우기, 조건부 서식 규칙, 데이터 유효성 검사, 차트, 이미지, 정의된 이름은 비교하지 않습니다. 값은 동일하지만 서식이 일반에서 통화로 바뀐 셀은 차이가 없다고 보고되는데, 이는 데이터 비교로는 옳지만 서식 검토로는 부족합니다

날짜 값을 가진 셀은 한 가지 구체적인 주의가 필요합니다. 이들은 텍스트 변환 결과로 비교되므로, 1904 날짜 체계로 저장된 워크북과 1900 체계로 저장된 워크북은 내부 일련번호가 다를 경우 놀라운 방식으로 같거나 다르게 비교될 수 있습니다. 날짜 체계 규칙은 날짜 일련번호와 1904 체계에서 다룹니다. 서식이나 객체 수준 충실도가 질문의 일부라면, diff에 각 쪽의 해당 기능 개수를 세는 감사 패스를 함께 사용하십시오

워크북 비교, 감사, 변환은 모두 Delphi와 C++Builder용 같은 엔진 위에서 실행됩니다. 전체 기능 목록은 HotXLS Delphi 스프레드시트 컴포넌트 페이지에 있습니다