如何使用任意区域设置比较"basic_string"

How to compare a "basic_string" using an arbitary locale

本文关键字:basic string 比较 设置 何使用 任意 区域      更新时间:2023-10-16

我重新发布了今天早些时候提交的一个问题,但现在我引用了一个具体的例子来回应我收到的反馈。最初的问题可以在这里找到(注意这不是家庭作业):

我只是想确定C++是否不可能对basic_string对象执行(有效的)大小写INsensitive比较,该对象还考虑了任何任意的locale对象。例如,似乎不可能编写一个有效的函数,如以下函数:

bool AreStringsEqualIgnoreCase(const string &str1, const string &str2, const locale &loc);

根据我目前的理解(但有人能证实这一点吗),这个函数为给定的locale调用ctype::toupper()collate::compare()(一如既往地使用use_facet()提取)。但是,由于collate::compare()特别需要4个指针参数,因此您需要为需要比较的每个字符传递这4个参数(在第一次调用ctype::toupper()之后),或者,先将两个字符串转换为uppercase,然后对collate::compare()进行一次调用。

第一种方法显然效率低下(每个测试的字符要传递4个指针),第二种方法要求将两个字符串全部转换为大写(需要分配内存,并且不需要将两个字符复制/转换为大写)。我对此是否正确,即不可能有效地做到这一点(因为没有办法绕过collate::compare())。

试图以一致的方式与世界上所有的写作系统打交道的一个小烦恼是,实际上你认为自己对字符的了解都是不正确的。这使得像"不区分大小写的比较"这样的事情变得棘手。事实上,进行任何形式的区域设置感知比较都很棘手,而且不区分大小写也很棘手。

然而,有了一些限制,这是有可能实现的。所需的算法可以使用正常的编程实践(以及一些静态数据的预计算)"高效"地实现,但它不能像不正确的算法那样高效地实现。通常可以用正确性换取速度,但结果并不令人愉快。不正确但快速的区域设置实现可能会吸引那些区域设置实现正确的人,但对于那些区域设置产生意外结果的受众来说,这显然是不令人满意的。

词典排序对人类不起作用

具有大小写的语言的大多数语言环境(除了"C"语言环境)已经按照预期的方式处理了字母大小写,即只有在考虑了所有其他差异之后才使用大小写差异。也就是说,如果单词列表按照区域设置的排序顺序进行排序,那么列表中只有大小写不同的单词将是连续的。大写单词是在小写单词之前还是之后取决于语言环境,但中间不会有其他单词。

这种结果不可能通过任何从左到右逐个字符的比较("字典排序")来实现。大多数地方都有其他的排序怪癖,这些怪癖也不会屈服于天真的词典排序。

如果您有适当的区域设置定义,标准C++排序规则应该能够处理所有这些问题。但是,仅仅在成对的whar_t上使用比较函数并不能将其简化为字典比较,因此C++标准库没有提供该接口。

以下只是几个示例,说明了区分区域设置的排序规则为何复杂;Unicode技术标准10中提供了更长的解释和更多的示例。

口音在哪里

大多数浪漫主义语言(以及英语,在处理借词时)都认为元音之上的重音是的次要特征;也就是说,首先对单词进行排序,就好像不存在重音一样,然后进行第二次排序,其中非重音字母位于重音字母之前。第三遍是处理这个案子所必需的,在前两遍中被忽略了。

但这对北欧语言不起作用。瑞典语、挪威语和丹麦语的字母表有三个额外的元音,在字母表中跟随在z之后。在瑞典语中,这些元音被写为åäæø时被写为

aa在德语中,字母äöü常按字母顺序排列,就像带有浪漫口音一样,但在德语电话簿(有时还有其他字母表)中,它们按字母顺序排序,就好像它们是写的aeeuee,这是书写相同音位的古老风格。(有很多常见的姓氏对,如"Müller"answers"Mueller"发音相同,经常混淆,所以将它们相互命名是有意义的。我年轻时,加拿大电话簿中的苏格兰名字也有类似的惯例;M'McMac的拼写都聚在一起,因为它们在发音上都是相同的。)

一个符号,两个字母。或者两个字母,一个符号

德语也有符号ß被整理为ss,尽管它在语音上并不完全相同。稍后我们将再次见到这个有趣的符号。

事实上,许多语言都认为有向图甚至三角图是单个字母。44个字母的匈牙利字母表包括CsDzzsGy

LyzZsh和

ll摘要:比较字符串需要多次通过,有时需要比较字符组

使其不区分大小写

相比之下,由于区域设置正确地处理大小写,因此实际上应该没有必要进行不区分大小写的排序。进行不区分大小写的等价类检查("相等"测试)可能很有用,尽管这会引发其他哪些不精确的等价类可能有用的问题。Unicode规范化、重音删除,甚至转写为拉丁文,在某些情况下都是合理的,而在另一些情况下则非常令人讨厌。但事实证明,案例转换也并不像你想象的那么简单。

由于存在di和trigraph,其中一些具有Unicode代码点,Unicode标准实际上识别三种情况,而不是两种情况:小写、大写和标题。最后一个是单词第一个字母的大写字母,例如克罗地亚有向图dž(U+01C6;单个字符),其大写字母为DŽ(U+01C4)并且其标题大小写是(U+01C5)。"不区分大小写"比较的理论是,我们可以转换(至少在概念上)任何字符串,使"忽略大小写"定义的等价类的所有成员都转换为相同的字节序列。传统上,这是通过"上套管"来完成的,但事实证明,这并不总是可能的,甚至是正确的;Unicode标准更喜欢使用"大小写折叠"一词,I.也是如此

C++语言环境不能胜任这项工作

因此,回到C++,可悲的事实是,C++语言环境没有足够的信息来进行精确的大小写折叠,因为C++语言环境的工作假设是,大小写折叠字符串只包括使用一个函数将一个代码点映射到另一个码点,按顺序和单独地将字符串中的每个代码点大写。正如我们将看到的那样,这根本不起作用,因此其效率问题也无关紧要。另一方面,ICU库有一个接口,可以在Unicode数据库允许的范围内正确地进行大小写折叠,并且它的实现是由一些非常好的程序员精心设计的,因此它可能在限制范围内尽可能高效。所以我绝对建议使用它。

如果你想很好地了解大小写折叠的难度,你应该阅读Unicode标准的第5.18和5.19节(第5章为PDF)。以下只是几个例子。

事例转换不是从单个字符到单个字符的映射

最简单的例子是德语ß(U+00DF),它没有大写形式,因为它从不出现在单词的开头,而且传统的德语正字法也不使用所有的大写字母。标准的大写变换是SS(或者在某些情况下SZs的实例都被写成<kbd&szlig>

。例如,比较grüßen和küssen(分别表示问候和亲吻)。在v5.1中;,一个"大写ß"被添加到Unicode中作为U+1E9E,但它并不常用,除非在所有大写的路标中使用,在那里它的使用是法律规定的。大写&szleg;的正常期望值是两个字母SS

并非所有表意文字(可见字符)都是单字符代码

即使当大小写变换将单个字符映射到单个字符时,也可能无法将其表示为wchar→wchar映射。例如,ǰ可以容易地大写为,但前者是一个单独的组合字形(U+01F0),而第二个是带有组合角符的大写J(U+030C)。

ǰ

按字符大小写折叠的天真字符可以反规范化

假设我们的大写ǰ如上所述。我们如何将ǰ;大写̠(如果它在您的系统上不能正确渲染,它是下面有一个条的同一个字符,另一个IPA约定)?该组合是U+01F0,U+0320(j与caron,组合下面的减号),因此我们继续用U+004A,U+030C替换U+01FO,然后使U+0320保持原样:ǰ̠。这很好,但它不能与下面有caron和减号的标准化大写J相比,因为在正常形式中,减号变音符号排在第一位:U+004A,U+0320,U+030C(J̠;̌;,看起来应该相同)。因此,有时(老实说,很少,但有时)有必要重新规范化。

抛开unicode wierdness不谈,有时大小写转换是上下文敏感的

希腊语有很多例子说明标记是如何根据单词首字母、单词尾字母还是单词内部字母而被打乱的——你可以在Unicode标准的第7章中阅读更多关于这一点的内容——但一个简单而常见的例子是Σ,它有两个小写版本:#x03c3。具有一定数学背景的非希腊人可能熟悉σ,但可能没有意识到它不能用在单词的末尾,必须使用#x03c2

简而言之

  1. 大小写折叠的最佳正确方法是应用Unicode大小写折叠算法,该算法需要为每个源字符串创建一个临时字符串。然后,您可以在两个转换后的字符串之间进行简单的字节比较,以验证原始字符串是否在同一个等价类中。对转换后的字符串进行排序可能比对原始字符串进行排序效率低,而且出于排序目的,未转换的比较可能与转换后的比较一样好或更好。

  2. 理论上,如果你只对大小写折叠等式感兴趣,你可以线性地进行转换,记住转换不一定是上下文无关的,也不是一个简单的字符到字符映射函数。不幸的是,C++语言环境没有为您提供执行此操作所需的数据。Unicode CLDR更接近,但它是一个复杂的数据结构。

  3. 所有这些东西都非常复杂,而且充斥着边缘案例。(例如,请参阅Unicode标准中关于重音立陶宛i的注释。)您最好使用维护良好的现有解决方案,其中最好的例子是ICU。