假设 Unicode 和不区分大小写,模式 “..” 是否应与 “FfIsS” 匹配?关于规范化区域设置匹配

这听起来像是一个笑话,但我可以证明这一点。

假设:

  • 点匹配任何单个字符。
  • 不区分大小写的模式当且仅当匹配 时匹配。ss.toUpperCase()

以下所有内容都非常合乎逻辑,并且适用于Java:

  • "ffi".matches(".")拉丁语小连字 FFI (U+FB03) 是一个字符,因此它必须匹配
  • "ß".matches(".")拉丁文小写字母 SHARP S (U+00DF) 是一个字符,因此必须匹配
  • "ffi".toUpperCase().equals("FFI")根据 Unicode 标准(没有大写连字 FFI)
  • "ß".toUpperCase().equals("SS")根据Unicode标准(有一个大写的尖锐S,但它没有被使用)
  • "FfI".toUpperCase().equals("FFI")明显地
  • "sS".toUpperCase.equals("SS")明显地

因此,假设正则表达式中的第一个点代表正则表达式,第二个点代表 ,正则表达式必须与“FFISS”匹配,并且由于大小写不敏感,也与“FfIsS”匹配。ß

我真的希望有问题,否则正则表达式会变得非常不可用。

问题:

  • 我的“证明”有什么问题?
  • 如果我的第二个假设不成立,“不区分大小写”到底是什么意思?

答案 1

关于案例折叠

答案是否定的,点不会不区分大小写,尽管原因有点深奥。ss

然而,你的困惑经常被一些最了解这些事情的人提出,因为他们也觉得这会导致矛盾。

Unicode 中有两种形式的案例映射。有一个简单的案例映射,其中一个代码点只映射到另一个代码点。因此,如果 ,那么可以保证,Unicode 折叠映射在哪里。但它也适用于 、 和 事例映射。length(s) == 1length(fc(s)) == 1fcuctclc

问题在于,在分析某些类型的现实世界文本时,您不会获得那么好的结果,然后您就会做出这些确切的1:1长度保证。

事实上,其中有相当多的。这些数字表示在四个事例映射下有多少个单独的 BMP 代码点映射到指定的长度:

length lc == 2          1
length lc == 3          0

length fc == 2         88
length fc == 3         16

length uc == 2         86
length uc == 3         16

length tc == 2         32
length tc == 3         16

在完整大小写中,而不是Java的正则表达式使用的简单大小写中,您确实可以获得类似和匹配的东西,即使它们的长度不等。Perl 和 Ruby 在进行不区分大小写的比较时使用完整的大小写映射。如果你不小心,这会导致否定字符类中的奇怪悖论。tschüßTSCHÜSS

但问题是:不区分大小写的匹配不执行传递操作。换句话说,如果匹配且在大小写不敏感匹配下匹配 匹配 ,这并不意味着通过传递性不区分大小写匹配。它只是不以这种方式工作,尽管比我聪明的人已经深入思考了这个问题。.ßßSS.SS

但是,这两个代码点:

  • U+00DF ‭ ß LATIN SMALL LETTER SHARP S
  • U+1E9E ‭ ẞ LATIN CAPITAL LETTER SHARP S

当然,不区分大小写不仅彼此匹配,而且 、 、 和 完整大小写映射。他们只是在简单的案例映射下不这样做。SSSssSss

Unicode确实对此做出了一些保证。一个是 if ,那么 where 是四个案例映射中的任何一个:、 、 和 。length(s) == nlength(fn(s)) <= 3*nfnlcfcuctc

关于规范化

如果你认为这是不好的,那么当你考虑规范化形式时,它实际上会变得更糟。这里的保证是5×而不是3×。所以,正如你所看到的,它变得越来越昂贵。length(NFx(s)) <= 5 * length(s)

下面是等效的表,显示四种规范化形式中每种形式的代码点扩展到多个代码点的数量:

length NFC  == 2        70
length NFC  == 3         2
length NFC  == 4         0
length NFC  == 5         0

length NFKC == 2       686
length NFKC == 3       404
length NFKC == 4        53
length NFKC == 5        15

length NFD  == 2       762
length NFD  == 3       220
length NFD  == 4        36
length NFD  == 5         0

length NFKD == 2      1345
length NFKD == 3       642
length NFKD == 4       109
length NFKD == 5        16

这不是很了不起吗?有一段时间,Unicode想要尝试在其模式匹配中构建规范等价性。他们知道由于上述原因,这很昂贵,但是他们花了一段时间才弄清楚,由于在一个字形单位内组合字符的必要规范重新排序,这基本上是不可能的。

出于这个原因,以及许多其他原因,如果您想“不区分大小写”或“规范化不敏感地”比较事物,目前的建议是自己通过两边的转换来运行它,然后比较结果。

例如,给定一个合适的代码点逐个代码点等价运算符==

fc(a) == fc(b)

对于模式匹配运算符也是如此(当然,它以传统方式工作,不像Java的破碎方法不恰当地锚定事物):=~match

fc(a) =~ fc(b)

这样做的问题是,您无法再在模式的特定部分打开或关闭不区分大小写,例如

/aaa(?i:xxx)bbb/

并且只对部分进行不区分大小写。xxx

完整的大小写很难,但它可以(在大多数情况下)完成,正如Perl和Ruby所证明的那样。但是,在你应该理解的地方,它也是相当不直观的(阅读:令人惊讶)。你必须用括号中的字符类做一些特殊的事情,特别是对它们的否定,否则会导致胡说八道。

区域设置匹配

最后,要使事情真正复杂化,在现实世界中,你必须做的不仅仅是事例映射和归一化。在某些国家/地区,情况更为复杂。例如,在德语电话簿中,带有元音变音符的元音与相同的基本元音后跟字母 e 的计数完全相同。因此,在那里,类似的东西有望在不区分大小写的情况下进行匹配。müßMUESS

要正确完成所有这些操作,您不仅需要绑定完整的大小写映射和规范化表、DUCET 本身、默认 Unicode 排序规则元素表,甚至 CLDR 数据(请参阅参考书目):

#!/usr/bin/perl
use utf8;
use open qw(:utf8 :std);
use Unicode::Collate::Locale;

my $Collator = Unicode::Collate::Locale->new(
    locale        => "de__phonebook",
    level         => 1,
    normalization => undef,
);

my $full = "Ich müß Perl studieren.";
my $sub  = "MUESS";
if (my ($pos,$len) = $Collator->index($full, $sub)) {
    my $match = substr($full, $pos, $len);
    print "Found match of literal ‹$sub› at position $pos in ‹$full› as ‹$match›\n";
}

如果你运行它,你会发现它确实有效:

在 ‹Ich müß Perl studieren 中位置 4 处找到文字 ‹MUESS› 的匹配项。› 作为 ‹müß›


参考书目

这些例子中的大多数都是从第4版的Programming Perl中获取的,并得到了其作者的许可。:)我在那里写了很多关于Unicode问题的文章,这些东西不是特定于Perl的,而是针对Unicode的。

unichars(1) 程序允许我收集如下统计信息:

$ unichars 'length fc == 2' | wc -l
      88

$ unichars 'length NFKD == 4' | wc -l
     109

$ unichars '/ss/i'
U+00DF ‭ ß  LATIN SMALL LETTER SHARP S
U+1E9E ‭ ẞ  LATIN CAPITAL LETTER SHARP S

Unicode::Tussle CPAN模块套件的一部分,Brian Foy一直很好心地为我维护。


进一步阅读

另请参阅:


答案 2

正如maaartinus在他的评论中指出的那样,Java为不区分大小写的reg-exp匹配提供了(至少在理论上)Unicode支持。Java API文档中的措辞是,匹配是“以与Unicode标准一致的方式”完成的。然而,问题在于,Unicode 标准定义了对大小写转换和不区分大小写匹配的不同级别的支持,而 API 文档没有指定 Java 语言支持哪个级别。

虽然没有记录,但至少在Oracle的Java VM中,reg-exp实现仅限于所谓的简单不区分大小写的匹配。与示例数据相关的限制因素是,仅当大小写折叠(转换)导致相同数量的字符并且集合(例如“.”)被限制为与输入字符串中的一个字符完全匹配时,匹配算法才会按预期工作。第一个限制甚至导致“ß”与“SS”不匹配,正如您可能也预料到的那样。

若要支持字符串文本之间完全不区分大小写的匹配,可以使用 ICU4J 库中的 reg-exp 实现,以便至少“ß”和“SS”匹配。AFAIK,但是Java没有完全支持组,集合和通配符的reg-exp实现。