技术文章

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 单元格不再被数进去。HotXLS 自 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 永远处于通配符模式,模式串里哪怕没有 * 或 ?,波浪号也是转义。Excel 16 在一个装着 a~b 和 ab 的两单元格区域上证实了它: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 不同。在装着 abc、ab、xab、AB、a~b、a*b 的 Name 列上实测 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 电子表格组件页