试图以一致的方式与世界上所有的书写系统打交道的一个小烦恼是,实际上你认为你知道的关于字符的所有内容实际上都是正确的。这使得做“不区分大小写的比较”之类的事情变得很棘手。确实,进行任何形式的区域感知比较都很棘手,而且不区分大小写也很棘手。
虽然有一些限制,但还是有可能完成的。所需的算法可以使用正常的编程实践(和一些静态数据的预计算)“有效地”实现,但它不能像不正确的算法那样有效地实现。通常可以用正确性来换取速度,但结果并不令人愉快。不正确但快速的语言环境实施可能会吸引那些语言环境实施正确的人,但对于那些语言环境产生意外结果的部分观众来说显然是不满意的。
字典顺序不适用于人类
大多数语言环境(“C”语言环境除外)已经以预期的方式处理字母大小写,即仅在考虑所有其他差异后才使用大小写差异。也就是说,如果单词列表按区域设置的排序顺序排序,则列表中仅大小写不同的单词将是连续的。大写单词在小写单词之前还是之后取决于语言环境,但中间不会有其他单词。
该结果不能通过任何单次从左到右逐个字符的比较(“字典顺序”)来实现。而且大多数语言环境都有其他排序规则的怪癖,这些怪癖也不会屈服于幼稚的字典顺序。
如果您有适当的语言环境定义,标准 C++ 排序规则应该能够处理所有这些问题。但它不能简化为仅对 whar_t 对使用比较函数的字典比较,因此 C++ 标准库不提供该接口。
以下只是语言环境感知排序为何复杂的几个示例;在Unicode Technical Standard 10 中可以找到更长的解释以及更多示例。
重音去哪儿了?
大多数浪漫语言(在处理借词时也包括英语)将元音上的重音视为次要特征;也就是说,首先对单词进行排序,就好像不存在重音一样,然后进行第二次遍历,其中非重音字母排在重音字母之前。处理大小写需要第三遍,在前两遍中忽略。
但这不适用于北欧语言。瑞典语、挪威语和丹麦语的字母表有三个额外的元音,在字母表中跟在 z 之后。在瑞典语中,这些元音写成 å、ä 和 ö;在挪威语和丹麦语中,这些字母写成 å、æ 和 ø,而在丹麦语中,å 有时是写成 aa,使奥胡斯成为按字母顺序排列的丹麦城市列表中的最后一个条目。
在德语中,字母 ä、ö 和 ü 通常按字母顺序排列,就像带有浪漫口音一样,但在德语电话簿中(有时其他字母列表),它们按字母顺序排列,就好像它们被写成 ae、oe 和 ue,这是写相同音素的旧风格. (有许多常见的姓氏,例如“Müller”和“Mueller”,发音相同且经常混淆,因此将它们相互对照是有意义的。当我年轻时,加拿大电话簿中的苏格兰名字也使用了类似的惯例;拼写 M'、Mc 和 Mac 都聚集在一起,因为它们在语音上都是相同的。)
一个符号,两个字母。或者两个字母,一个符号
德语也有符号ß,它被整理成好像写成ss,尽管它在语音上并不完全相同。稍后我们会再次遇到这个有趣的符号。
事实上,许多语言都将二合字母甚至三合字母视为单个字母。 44个字母的匈牙利字母包括Cs、Dz、Dzs、Gy、Ly、Ny、Sz、Ty 和 Zs,以及各种重读元音。然而,关于这种现象的文章中最常引用的语言——西班牙语——在 1994 年停止将二合字母 ch 和 ll 视为字母,大概是因为它更容易强制西班牙裔作家要符合计算机系统而不是改变计算机系统来处理西班牙语有向图。 (维基百科称这是来自“联合国教科文组织和其他国际组织”的压力;每个人都花了很长时间才接受新的字母排序规则,在南美国家的字母列表中,你仍然偶尔会在“哥伦比亚”之后找到“智利”。)
总结:比较字符串需要多遍,有时还需要比较字符组
不区分大小写
由于语言环境在比较中正确处理大小写,因此实际上没有必要进行不区分大小写的排序。进行不区分大小写的等价类检查(“相等”测试)可能很有用,尽管这提出了其他不精确的等价类可能有用的问题。 Unicode 规范化、重音删除,甚至转写为拉丁语在某些情况下都是合理的,而在其他情况下则非常烦人。但事实证明,大小写转换也没有你想象的那么简单。
由于二合字母和三合字母的存在,其中一些具有 Unicode 代码点,Unicode 标准实际上识别三种情况,而不是两种:小写、大写和标题大小写。最后一个是你用来将单词的第一个字母大写的方法,例如,克罗地亚二合字母 dž(U+01C6;单个字符)需要它,它的大写是 DŽ (U+01C4),其标题大小写为 Dž (U+01C5)。 “不区分大小写”比较的理论是,我们可以转换(至少在概念上)任何字符串,使得由“忽略大小写”定义的等价类的所有成员都转换为相同的字节序列。传统上这是通过“大写”字符串来完成的,但事实证明这并不总是可能的,甚至是正确的; Unicode 标准更喜欢使用术语“大小写折叠”,我也是。
C++ 语言环境不能胜任这项工作
所以,回到 C++,可悲的事实是 C++ 语言环境没有足够的信息来进行准确的大小写折叠,因为 C++ 语言环境的工作假设是字符串的大小写折叠仅由顺序和单独组成使用将代码点映射到另一个代码点的函数将字符串中的每个代码点大写。正如我们将看到的那样,这根本行不通,因此它的效率问题是无关紧要的。另一方面,ICU library 有一个接口,它可以像 Unicode 数据库允许的那样正确地进行大小写折叠,并且它的实现是由一些非常好的编码人员精心设计的,因此它可能在约束范围内尽可能高效。所以我绝对推荐使用它。
如果您想全面了解折叠箱的难度,您应该阅读Unicode standard (PDF for chapter 5) 的第 5.18 和 5.19 节。以下只是几个例子。
大小写转换不是从单个字符到单个字符的映射
最简单的例子是德语的ß (U+00DF),它没有大写形式,因为它从不出现在单词的开头,传统的德语正字法也没有使用全部-大写。标准的大写变换是 SS(或在某些情况下是 SZ),但该变换是不可逆的;并非所有 ss 的实例都写为 ß。例如,比较 grüßen 和 küssen(分别表示问候和亲吻)。在 v5.1 中,ẞ 是一个“大写 ß”,作为 U+1E9E 添加到 Unicode 中,但它不常用,除非在全大写街道标志中,法律规定使用它。大写的正常期望大小写 ß 将是两个字母 SS。
并非所有的表意文字(可见字符)都是单字符代码
即使大小写转换将单个字符映射到单个字符,它也可能无法将其表示为wchar→wchar 映射。例如,ǰ 可以很容易地大写为 J̌,但前者是单个组合字形(U+01F0),而第二个是大写 J 和一个组合符号(U+030C)。
像 ǰ 这样的字形还有一个问题:
简单的逐个字符的大小写折叠可以反规范化
假设我们像上面一样大写ǰ。我们如何大写 ǰ̠ (如果它不能在您的系统上正确呈现,它是同一个字符,下面有一个条,另一个 IPA 约定)?该组合是 U+01F0,U+0320(j 与 caron,组合下面的减号),因此我们继续将 U+01F0 替换为 U+004A,U+030C,然后将 U+0320 保持原样: J̠̌。这很好,但它不会与下面带有正号和减号的规范化大写 J 进行比较,因为在正常形式中,减号变音符号首先出现:U+004A,U+0320,U+030C (J̠̌ ,看起来应该是一样的)。所以有时(很少,说实话,但有时)有必要重新规范化。
抛开 unicode 的怪异,有时大小写转换是上下文相关的
希腊语有很多例子说明标记是如何根据它们是词首、词尾还是词内来打乱的——你可以在 Unicode 标准的第 7 章中阅读更多相关信息——但是简单常见的情况是Σ,它有两个小写版本:σ和ς。具有一定数学背景的非希腊人可能对 σ 很熟悉,但可能不知道它不能用在词尾,必须使用 ς。
简而言之
大小写折叠的最佳可用正确方法是应用 Unicode 大小写折叠算法,该算法需要为每个源字符串创建一个临时字符串。然后,您可以在两个转换后的字符串之间进行简单的按字节比较,以验证原始字符串是否在同一个等价类中。对转换后的字符串进行排序规则排序虽然可能,但效率低于对原始字符串排序的排序规则,并且出于排序目的,未转换的比较可能与转换后的比较一样好或更好。
理论上,如果您只对大小写相等感兴趣,您可以进行线性转换,记住转换不一定是上下文无关的,也不是简单的字符到字符映射功能。不幸的是,C++ 语言环境没有为您提供执行此操作所需的数据。 Unicode CLDR 更接近,但它是一个复杂的数据结构。
-
所有这些东西都非常复杂,并且充满了边缘情况。 (例如,请参阅 Unicode 标准中关于重音立陶宛语 i 的注释。)最好使用维护良好的现有解决方案,其中最好的例子是 ICU。
李>