技術文章

HotXLS Excel 萬用字元:COUNTIF、MATCH、DSUM 與 Find

HotXLS Delphi Component 會把同一個樣式字串讀出四種意思,因為 Excel 16 就是這樣。在 COUNTIF 與 SUMIF 裡,a~b 這串文字是字面值,除非準則裡還有 * 或 ?;在 MATCH 與 XLOOKUP 的萬用字元模式裡,波浪號永遠是跳脫字元,所以 a~b 找到的是 ab;在 DSUM 與其他資料庫函式裡,純文字代表「以…開頭」;而整格 Find 得回溯到最後一個 *。這些實測出來的規則,HotXLS 分別自 v2.384.52、v2.384.60 與 v2.384.64 起遵循

這個領域的 bug 回報從不提萬用字元。它們說的是:伺服器產生的報表比 Excel 重算同一份檔案少算了幾列,或者某個帶波浪號的零件編號,這條公式找得到、下一條卻視而不見。病因是比對器假定樣式在什麼地方都只有一種意思。Excel 偏不這麼運作,快取結果必須與 Excel 一致的引擎也就不能這麼運作。v2.384.52 之前,HotXLS 把每個準則都餵給 DOS 式的檔案遮罩,日常樣式對,邊角情況錯,而且錯得悄無聲息

為什麼一個樣式字串在 Excel 裡有四種意思?

一個樣式字串有四種意思,是因為 Excel 從四個功能各繼承了一套比對規則,而從未統一。準則函式(COUNTIF、SUMIF、AVERAGEIF 與 *IFS 家族)按個別準則決定萬用字元到底啟不啟用。查找函式(match type 0 的 MATCH、match_mode 2 的 XLOOKUP)永遠啟用。資料庫函式(DSUM、DCOUNTA 這票)遵循進階篩選的規則,裸字代表前綴。Find 對話框另有整格與部分比對兩種模式。下表以一欄放著 a~b、ab、AB、abc、abcb、a*b 與 axb 的資料,列出每個樣式各有哪些儲存格命中,所有函式都用預設的不分大小寫模式

樣式COUNTIF / SUMIFMATCH(…,0) / XLOOKUP 模式 2DSUM 準則Find,整格、萬用字元開啟
abab, ABab, ABab, AB, abc, abcbab, AB
a*ba~b, ab, AB, abcb, a*b, axb與 COUNTIF 相同每一列都中,abc 也算與 COUNTIF 相同
a~b只有 a~bab, ABab, AB, abc, abcbab, AB
a~*b只有 a*b只有 a*b只有 a*b只有 a*b
=abab, AB不適用ab, AB不適用

a~b 那一列正是 COUNTIF 與 MATCH 翻臉的地方,而零件編號與手工輸入的代碼裡,波浪號出現的頻率超出所有人預期。a*b 那一列是另一個陷阱:abc 對 DSUM 命中、對 COUNTIF 卻沒有,因為資料庫函式默默補了一個 *。ab、a*b 與 =ab 的 DSUM 結果直接取自 Excel 16 實跑;a~b 的 DSUM 結果由同一條前綴規則推出——補上的 * 讓準則變成萬用字元樣式,其中 ~b 是被跳脫的 b

COUNTIF 什麼時候切進萬用字元模式?

只有當準則文字含有 * 或 ?(無論是否被跳脫),COUNTIF 才切進萬用字元模式。兩個字元都沒有時,Excel 把準則當整串文字與每格比對、不分大小寫,波浪號就只是波浪號,所以 COUNTIF(A1:A7,"a~b") 算的是字面上放著 a~b 的那格。加一顆星,意思整個翻轉:"a~b*" 裡的波浪號改跳脫 b,樣式讀作「ab 後面接任何東西」,a~b 那格就不再入計。兩個引擎自 v2.384.52 起都套這條規則,由 lxCalc 裡一個共用的準則比對器承擔,COUNTIF、SUMIF、AVERAGEIF、COUNTIFS、SUMIFS、AVERAGEIFS 與資料庫函式全走它

HotXLS 萬用字元閘門示意圖:COUNTIF 與 SUMIF 只在準則含星號或問號時啟用萬用字元,a~b 於是算到字面那格、回傳 1;MATCH type 0 與 XLOOKUP 模式 2 永遠處於萬用字元模式,a~b 因此找到位置 2 的 ab
閘門就是全部差異所在:COUNTIF 要先看到星號或問號才把波浪號當跳脫,MATCH 從不問,同一個樣式字串於是算到一格、找到另一格

萬用字元模式裡的跳脫規則與 Excel 其他地方相同:~ 讓下一個字元無論是什麼都變字面值,~b 就是 b、~~ 是一個波浪號,樣式最末端的波浪號直接丟棄,"a*~" 行同 "a*"。方括號永遠不特殊:"[x]" 算的是放著 [x] 三個字元的儲存格,"[a-z]" 在普通資料上一個也算不到。TXLSXWorkbook.Calculate 對使用中工作表求公式字串的值、回傳 Variant,是拿自己的資料驗證這些規則最快的路

uses
  System.Variants, lxHandleX;

const
  Names: array [1..7] of string = ('a~b', 'ab', 'AB', 'abc', 'abcb', 'a*b', 'axb');
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  i: Integer;

  procedure Show(const Formula: string);
  begin
    Writeln(Formula, ' = ', VarToStr(Book.Calculate(Formula)));
  end;

begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Data');
    for i := 1 to High(Names) do
    begin
      Sheet.Cells[i, 1].Value := Names[i];
      Sheet.Cells[i, 2].Value := 1 shl (i - 1);  // 1, 2, 4 ... 讓 SUMIF 總和指認出是哪些列
    end;
    Sheet.Cells[8, 1].Value := 5;                // 一個數字;A9 保持空白

    Show('=COUNTIF(A1:A7,"a~b")');      // 1    無 * 也無 ?:純文字,算到 a~b 那格
    Show('=COUNTIF(A1:A7,"a~b*")');     // 4    萬用字元模式:ab, AB, abc, abcb
    Show('=COUNTIF(A1:A7,"a*b")');      // 6    整串萬用字元比對,abc 不算
    Show('=SUMIF(A1:A7,"a*b",B1:B7)');  // 119  除了 abc(8)以外的每一列
    Show('=COUNTIF(A1:A7,"a~*b")');     // 1    字面的 a*b
    Show('=COUNTIF(A1:A9,"<>ab")');     // 7    數字 5 與空白的 A9 都算
    Show('=COUNTIF(A1:A9,"<>")');       // 8    非空白儲存格
  finally
    Book.Free;
  end;
end.

「<>text」算什麼?

"<>text" 準則算每一個不是這串文字的儲存格,Excel 16 裡連數字、布林、錯誤值與空白儲存格都在內。裸的 "<>" 則是完全不同的問題:它代表「不是空白儲存格」,跳過空儲存格,但每個值都算,包括 ="" 這類公式回傳的空字串。HotXLS 舊程式碼對文字儲存格沒錯、對數字卻錯:Variant 的不等於逼 Delphi 把 'ab' 轉成數字,轉換丟出例外,例外處理器把它吞成「不匹配」,數字儲存格於是無聲地掉出計數。這件事的空白儲存格側,包括普通比較裡空運算元等於什麼,見HotXLS 怎麼處理比較鏈、空白儲存格與 SUMIF

搜尋 a~b 時 MATCH 為什麼找到 ab?

搜尋 a~b 時 MATCH 找到 ab,因為 match type 0 的 MATCH 與 match_mode 2 的 XLOOKUP 永遠在萬用字元模式,樣式裡就算沒有 * 或 ?,波浪號照樣是跳脫。放著 a~b 與 ab 兩格的範圍上,Excel 16 證實了這一點:MATCH("a~b",D1:D2,0) 回傳 2;範圍裡只有 a~b 時,同一個呼叫回傳 #N/A。要查找字面文字 a~b,得寫 "a~~b"。與此同時,同樣兩格上的 COUNTIF(D1:D2,"a~b") 回傳 1,算到的是另一格。同一字串、同一範圍,命中的是相反的儲存格

這正是 HotXLS 把兩個決策分開、不收進單一「比對樣式」入口的原因。比對器本身共用:v2.384.52 起,MATCH、XLOOKUP 與準則函式跑同一個回溯比對器,同樣的跳脫處理、同樣的尾端波浪號規則。差別在它前面那道閘。準則路徑先問「這串文字有沒有 * 或 ??」;查找路徑從不問。把兩者合併,救了一邊就毀了另一邊,兩個方向都在兩個引擎裡對著 Excel 16 的值檢查。萬用字元查找還有自己的前置條件:XLOOKUP 拒絕萬用字元比對搭配二分搜尋模式,這條規則見HotXLS 的 XLOOKUP 與 XMATCH 搜尋模式指南

DSUM 與資料庫函式怎麼讀純文字準則?

DSUM 與其他資料庫函式,把不帶前置 =、< 或 > 的文字準則讀作「以…開頭」,萬用字元照樣有效。這是進階篩選的規則,而且刻意與 COUNTIF 不同。Name 欄放著 abc、ab、xab、AB、a~b 與 a*b 時,Excel 16 實測:準則 ab 命中 abc、ab 與 AB;=ab 只命中 ab 與 AB;<>ab 是整列不相等;a*b 與 a? 同樣是前綴樣式;>ab 則是普通比較。v2.384.64 之前,HotXLS 對 ab 精確比對,同一份測試資料上 DSUM 回傳 10,Excel 卻回傳 11

這個修正得繞過條件剖析器——它把 ab 與 =ab 摺成同一個相等條件。HotXLS 於是在信任剖析後的條件之前,先檢查原始準則文字:首字元不是 =、< 或 > 的文字準則,補上一個 * 送進萬用字元比對器;其餘一律維持整列比較。在程式裡建準則範圍有個實務細節:XLSX 引擎把字串 '=ab' 指派給 TXLSXCell.Value 存的是文字;傳統 TXLSWorkbook 引擎則會把 = 開頭的值編譯成公式,除非您在前面加一個單引號

HotXLS 的 DSUM 準則規則示意圖:裸文字準則補上星號、按前綴比對,ab 因此命中 ab、AB、abc 與 abcb;等於 ab 比較整列,角括號 ab 排除兩者,波浪號加星號存活為字面的 a*b,並標出實測的 DSUM 總和 30、6、121 與 32
Excel 為資料庫函式繼承了進階篩選的規則:裸文字代表以…開頭,前置等號或不等號則比較整列;HotXLS 在信任剖析後的條件之前,先檢查原始準則文字
const
  Names: array [1..7] of string = ('a~b', 'ab', 'AB', 'abc', 'abcb', 'a*b', 'axb');
  Criteria: array [0..4] of string = ('ab', '=ab', '<>ab', 'a*b', 'a~*');
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  i: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Db');
    Sheet.Cells[1, 1].Value := 'Name';
    Sheet.Cells[1, 2].Value := 'Val';
    for i := 1 to High(Names) do
    begin
      Sheet.Cells[i + 1, 1].Value := Names[i];
      Sheet.Cells[i + 1, 2].Value := 1 shl (i - 1);
    end;
    Sheet.Cells[1, 4].Value := 'Name';            // 準則標頭放在 D1
    for i := 0 to High(Criteria) do
    begin
      Sheet.Cells[2, 4].Value := Criteria[i];     // 在 XLSX 引擎裡保持為文字
      Writeln(Criteria[i], ' -> ',
        VarToStr(Book.Calculate('=DSUM(A1:B8,"Val",D1:D2)')));
    end;
    // ab   -> 30   ab, AB, abc, abcb(前綴命中)
    // =ab  -> 6    ab, AB(整列比較)
    // <>ab -> 121  除了 ab 與 AB 的全部
    // a*b  -> 127  a*b* 命中全部七個,abc 也算
    // a~*  -> 32   只有字面的 a*b
  finally
    Book.Free;
  end;
end;

有一個相關差異活過了前綴修正,在較舊的建置上仍然要緊。>ab 這類文字比較以前用碼位順序,Excel 卻把標點排在字母之前,"a~b">"ab" 在 Excel 是 FALSE、在 HotXLS 卻曾是 TRUE。v2.384.67 起,> 與 < 準則連同普通文字比較與排序,都改用目前使用者地區設定下的 Excel 字組排序定序,兩邊重新一致

整格 Find 為什麼漏掉 abcb?

整格 Find 漏掉 abcb,是因為比對器在樣式用盡的第一個時點就停手,沒有回溯到最後一個 *。Replace 背後的部分比對器,樣式耗盡就立刻回傳;整格 Find 重用它,又額外要求比對得覆蓋整格:a*b 對上 abcb 在 ab 之後就停了,4 個字元只吃掉 2 個,於是被拒。v2.384.60 起,整格比對器是獨立實作,把「樣式完了、文字還有」也算一次不匹配,從最後一顆星重試——a*b 從此比得上 abcb、a?b*b 比得上 axbyb,正如勾選「儲存格內容完全相符」的 Excel 16 Find

HotXLS 的整格萬用字元 Find 回溯示意圖:樣式 a*b 在儲存格 abcb 裡吃掉 a 與 b,舊比對器在樣式用盡時停手並拒絕該格;現行比對器把「樣式用盡、文字尚餘」算一次不匹配,從最後一顆星重試,直到覆蓋整格
整格比對在樣式用盡時還沒完;把剩餘文字算一次不匹配,比對器就會退回最後一顆星,a*b 便是這樣觸及 abcb,與 Excel 16 Find 一致

同一版也動了波浪號。Excel 16 Find 不分整格或部分模式,都把 ~ 當成其後任一字元的跳脫:a~b 找到 ab,a~~b 找到 a~b,結尾的波浪號被忽略,q~ 行同 q。HotXLS 舊比對器只認 ~*、~? 與 ~~ 是跳脫,a~b 於是找到的是文字 a~b。單一個 ~ 的 Find 樣式在 Excel 本身就不穩定,會像空樣式一樣命中任何儲存格,HotXLS 不模仿這一點

XLSX 引擎的搜尋是 TXLSXWorksheet.FindText 加一組 TXLSXFindOptions:lxfUseWildcards 開啟 *、? 與 ~,lxfWholeCell 要求整格命中,lxfMatchCase 讓比對分大小寫。不帶 lxfUseWildcards 時,每個字元連星號在內都是字面值。Find 只看文字值:數字儲存格跳過,公式儲存格也跳過,除非設了 lxfSearchFormulas,那時搜尋的是公式文字。StartRow 與 StartCol 給的錨點含端點本身,Find All 迴圈因此要在每次命中之後跨過一欄

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Row, Col, NextRow, NextCol, Changed: Integer;
  Opts: TXLSXFindOptions;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Parts');
    Sheet.Cells[1, 1].Value := WideString('abc');
    Sheet.Cells[2, 1].Value := WideString('abcb');
    Sheet.Cells[3, 1].Value := WideString('a~b');
    Sheet.Cells[4, 1].Value := WideString('ab');

    Opts := [lxfUseWildcards, lxfWholeCell];
    if Sheet.FindText('a*b', Row, Col, Opts, 1, 1) then
      Writeln('a*b  whole cell -> row ', Row);   // 2:abc 被拒,回溯到 abcb
    if Sheet.FindText('a~b', Row, Col, Opts, 1, 1) then
      Writeln('a~b  whole cell -> row ', Row);   // 4:~b 是被跳脫的 b
    if Sheet.FindText('a~~b', Row, Col, Opts, 1, 1) then
      Writeln('a~~b whole cell -> row ', Row);   // 3:~~ 是一個字面波浪號

    // 部分比對,Find All:錨點儲存格含在內,每次命中後要跨過去
    NextRow := 1;
    NextCol := 1;
    while Sheet.FindText('a*b', Row, Col, [lxfUseWildcards], NextRow, NextCol) do
    begin
      Writeln('a*b  contained in row ', Row);     // 第 1、2、3、4 列
      NextRow := Row;
      NextCol := Col + 1;
    end;

    // 整格萬用字元取代只改寫字面的 a~b
    Changed := Sheet.ReplaceText('a~~b', 'a-b', Opts);
    Writeln(Changed, ' cell(s) replaced');         // 1
  finally
    Book.Free;
  end;
end;

部分比對迴圈四列全中,包括 abc,因為部分模式下 a*b 只要在格內某處出現就算。FindTextIn 與 ReplaceTextIn 收同樣的選項,外加 FirstRow、FirstCol、LastRow、LastCol 視窗,等同「在選取範圍內搜尋」的程式化版本。傳統引擎用一個帶三個布林的多載暴露同一套規則,即 TXLSWorksheet.FindText(SearchText, Row, Col, MatchCase, UseWildcards, WholeCell),另有對應的 ReplaceText 多載,列與欄的結果從 1 起算:

var
  Classic: IXLSWorkbook;
  Sheet: TXLSWorksheet;
  Row, Col: Integer;
begin
  Classic := TXLSWorkbook.Create;
  Sheet := Classic.Sheets.Add;
  Sheet.Range['A1', 'A1'].Value := 'abcb';
  // MatchCase = False, UseWildcards = True, WholeCell = True
  if Sheet.FindText('a*b', Row, Col, False, True, True) then
    Writeln('found at ', Row, ',', Col);           // 1,1
  if not Sheet.FindText('a*c', Row, Col, False, True, True) then
    Writeln('a*c does not cover abcb');
end;

舊的 DOS 遮罩比對器錯在哪?

舊比對器錯在特殊字元,因為 DOS 檔案遮罩與 Excel 萬用字元是兩種語言。v2.384.52 之前,準則函式與資料庫函式把每個樣式都交給 MatchesMask——lxMasks 單元裡的檔案遮罩比對器。常見情況下它的語法與 Excel 重疊,問題因此一直藏著;但真實資料開始有趣的地方,它就分道揚鑣:

  • [x] 被讀成字元集合,COUNTIF(A1:A10,"[x]") 於是算到放著 x 的儲存格而非帶括號的文字,"[a-z]" 則命中任何單字母儲存格
  • 沒有波浪號跳脫,"a~*b" 比不到字面的星號
  • 畸形的遮罩,例如沒閉合的括號,會丟出例外,呼叫端把它吞成「不匹配」,準則裡一個打錯的字就變成悄悄錯誤的總和
  • 查找這一端,MATCH 與 XLOOKUP 只認 ~*、~? 與 ~~ 是跳脫,MATCH("a~b",…,0) 於是找到字面的 a~b 而非 ab

活頁簿若只在純英數資料上用過 * 與 ?,結果本來就對,也不會變。若裡面有方括號、波浪號、"<>text" 之下的混合型別欄位,或寫成裸字的 DSUM 準則,用 v2.384.64 以後的版本重算,總和可能改變——而新的總和才是 Excel 顯示的那個。Excel 怎麼存放準則與怎麼比較準則的這道分野,在已存篩選上同樣出現,見HotXLS 的 BIFF8 AutoFilter DOPER 準則一文

速查:HotXLS 裡的 Excel 萬用字元規則

  • COUNTIF、SUMIF、AVERAGEIF 與 *IFS 家族,只在準則含 * 或 ? 時用萬用字元;否則整串不分大小寫比對,~ 是字面值(v2.384.52 起)
  • match type 0 的 MATCH 與 match_mode 2 的 XLOOKUP 永遠用萬用字元,a~b 因此找到 ab,字面值得寫 a~~b(v2.384.52 起)
  • 萬用字元模式裡 ~ 跳脫其後任一字元、結尾的 ~ 直接丟棄;[ 與 ] 是普通字元
  • "<>text" 算數字、布林、錯誤值與空白儲存格;裸的 "<>" 算非空白儲存格,="" 的結果也在內
  • DSUM 與其他資料庫函式把純文字當「以…開頭」;=text 與 <>text 比較整列(v2.384.64 起)
  • 帶 lxfUseWildcards 與 lxfWholeCell 的整格 Find 會回溯,a*b 比得上 abcb;Find 與 Replace 把 ~ 當任一字元的跳脫(v2.384.60 起)
  • > 與 < 準則的文字順序遵循 Excel 字組排序定序,標點在字母之前(v2.384.67 起)

公式引擎裡的 Excel 相容性,多半就是這類邊角情況,靠對著 Excel 實測,不是照文件猜。HotXLS 在 Delphi 與 C++Builder 裡原生求 COUNTIF、MATCH、XLOOKUP、DSUM 與整個函式庫的值,傳統引擎與 XLSX 引擎皆是,不必安裝 Excel。細節、版本與試用下載見 HotXLS Delphi spreadsheet component 產品頁