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 / SUMIF | MATCH(…,0) / XLOOKUP 模式 2 | DSUM 準則 | Find,整格、萬用字元開啟 |
|---|---|---|---|---|
ab | ab, AB | ab, AB | ab, AB, abc, abcb | ab, AB |
a*b | a~b, ab, AB, abcb, a*b, axb | 與 COUNTIF 相同 | 每一列都中,abc 也算 | 與 COUNTIF 相同 |
a~b | 只有 a~b | ab, AB | ab, AB, abc, abcb | ab, AB |
a~*b | 只有 a*b | 只有 a*b | 只有 a*b | 只有 a*b |
=ab | ab, 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 與資料庫函式全走它
萬用字元模式裡的跳脫規則與 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 引擎則會把 = 開頭的值編譯成公式,除非您在前面加一個單引號
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
同一版也動了波浪號。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 產品頁