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); // 每項差異一行可讀文字
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,這對審閱者已經足夠,對提交訊息也已經足夠。這是面向人的表面。而程式化的表面是差異紀錄本身,當比對結果要驅動某個決策而非產生一份文件時,該用的就是它
依結構化差異驅動邏輯
每項差異都會揭露其類型、工作表名稱、儲存格層級項目的一基列與欄,合併範圍層級項目的 A1 參照,以及左右兩側的文字。工作表層級與合併範圍層級項目會把列與欄回報為零,這就是不必檢視類型欄位、也能分辨它們的方法:
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;
在你依這份輸出撰寫斷言之前,有兩項特性值得了解。儲存格層級項目的順序,跟隨的是儲存格儲存區的內部走訪順序,因此測試應該以不依賴順序的方式撰寫。而公式簽章本身帶有自己的等號前綴,代表以字串串接方式組出的描述文字可能出現重複的 ==;當比對結果要驅動邏輯時,請檢查欄位值本身,而不是剖析那行描述文字
活頁簿差異比對能在哪些地方發揮價值
有三種用途單靠自己就足以證明這項功能的價值。回歸測試報表產生器:保留一份已知正確的活頁簿,重新產生、比對,並在任何未預期的差異上讓建置失敗。變更審閱:把可讀報告直接交給審閱者,而不是給兩份檔案。以及遷移驗證:轉換一批舊版活頁簿後,把每份結果與其來源比對,證明沒有任何東西遺失
第三種情境自然與 活頁簿稽核與轉換工作台 一文所述的盤點與稽核步驟搭配:先在轉換前計算活頁簿內含什麼,比對則在轉換後進行。若你的差異集中在插入的列附近,插入與刪除時的公式參照調整 一文說明了為什麼看起來沒變的公式會被回報為已變更
坦白說明限制之處
這項比對涵蓋值、公式、合併範圍與工作表是否存在。它不比對數字格式、字型、填色、條件式格式規則、資料驗證、圖表、影像或已定義名稱。一個值相同、但格式從一般改成貨幣的儲存格,不會回報任何差異,這對資料比對而言是正確行為,對格式審閱而言則不夠用
日期值的儲存格值得特別留意:它們是以文字轉換後的形式比對,因此一份採用 1904 日期系統的活頁簿與一份採用 1900 系統的活頁簿,若底層序列值不同,比對結果可能相等也可能不相等,令人意外。日期系統的規則說明於 日期序列值與 1904 系統 一文。當格式或物件層級的保真度也是問題的一部分時,請把差異比對搭配一次會計算兩側各項功能數量的稽核流程一起使用
活頁簿比對、稽核與轉換都在同一套適用 Delphi 與 C++Builder 的引擎上執行;完整功能清單列於 HotXLS Delphi 試算表元件頁面