При условии Unicode и нечувствительности к регистру, шаблон ".." должен соответствовать "FfIsS"?

Звучит как шутка, но я могу это доказать.

Предположения:

  • Точка соответствует любому отдельному символу.
  • Совпадения с учетом регистра символов s если и только если это соответствует s.toUpperCase(),

Все следующее довольно логично и верно в Java:

  • "ffi".matches(".") ЛАТИНСКАЯ МАЛЕНЬКАЯ ЛИГАТУРА FFI (U+FB03) - символ, поэтому он должен соответствовать
  • "ß".matches(".") LATIN SMALL LETTER 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".

Я действительно надеюсь, что что-то не так, в противном случае регулярные выражения станут довольно непригодными.

Вопросы:

  • Что не так с моим "доказательством"?
  • Что означает "без учета регистра", если мое второе предположение не выполняется?

2 ответа

Решение

На случай складывания

Ответ - нет, точка не будет совпадать ss без учета регистра, хотя причины немного эзотерические.

Однако некоторые из тех, кто знает о таких вещах, часто поднимали вашу головоломку, потому что они тоже считают, что это ведет к противоречиям.

В Unicode есть две формы отображения случаев. Существует простое отображение случая, в котором одна кодовая точка когда-либо отображается только на одну другую кодовую точку. Так что если length(s) == 1 тогда вам гарантировано, что length(fc(s)) == 1 также, где fc является картой в формате Unicode Но это также относится к uc, tc, а также lc отображения случая.

Проблема в том, что при анализе определенных видов реального текста вы не получите столь же хороших результатов, как тогда, когда вы даете такие гарантии точной длины 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, вы можете получить такие вещи, как tschüß а также TSCHÜSS чтобы соответствовать, даже если они имеют неодинаковую длину. Perl и Ruby используют полное отображение регистра при выполнении сравнения без учета регистра. Это приводит к странным парадоксам в отрицательных классах персонажей, если вы не будете осторожны.

Но вот в чем проблема: сопоставление без учета регистра не выполняет транзитивную операцию. Другими словами, если . Матчи ß и при сопоставлении без учета регистра, ß Матчи SS, это не значит, что через транзитивность . Матчи SS без учета регистра. Просто так не получается, хотя умнее людей, чем я, глубоко задумался над этим вопросом.

Однако оба эти кодовых пункта:

  • U + 00DF ß ЛАТИНСКАЯ МАЛЕНЬКАЯ ПИСЬМА SHARP S
  • U + 1E9E L ПИСЬМО ЛАТИНСКОГО КАПИТАЛА SHARP S

безусловно, регистронезависимо совпадают не только друг с другом, но и SS, Ss, sS, а также ss при полном картографировании. Они просто не делают это при простом картографировании.

Unicode дает некоторые гарантии по этому поводу. Во-первых, если length(s) == n, тот length(fn(s)) <= 3*n где fn является любой из четырех карт случая: lc, fc, uc, а также tc,

На нормализацию

Если вы думаете, что это плохо, то на самом деле становится намного хуже, когда вы рассматриваете формы нормализации. Здесь гарантия 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";
}

Если вы запустите это, вы обнаружите, что это действительно работает:

Найдено совпадение буквального ‹MUESS› в положении 4 в ‹Ich müß Perl studieren.› As ‹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, который Брайан Фой любезно поддержал.


Для дальнейшего чтения

Смотрите также:

Как отметил в своем комментарии Мааартинус, Java обеспечивает (по крайней мере, в теории) поддержку Юникода для нечувствительного к регистру соответствия reg-exp. Формулировка в документации по API Java заключается в том, что сопоставление выполняется "в соответствии со стандартом Unicode". Проблема, однако, в том, что стандарт Unicode определяет разные уровни поддержки для преобразования регистра и сопоставления без учета регистра, а в документации API не указано, какой уровень поддерживается языком Java.

Хотя это и не задокументировано, по крайней мере в Oracle VM VM, реализация reg-exp ограничена так называемым простым нечувствительным к регистру соответствием. Ограничивающие факторы, относящиеся к данным вашего примера, заключаются в том, что алгоритм сопоставления работает только так, как ожидается, если сворачивание (преобразование) в регистр приводит к одинаковому количеству символов, а наборы (например, ".") Ограничены, чтобы соответствовать точно одному символу во входных данных. строка. Первое ограничение даже приводит к тому, что "ß" не соответствует "SS", как вы, возможно, и ожидали.

Чтобы получить поддержку для полного нечувствительного к регистру совпадения между строковыми литералами, вы можете использовать реализацию reg-exp в библиотеке ICU4J, так что по крайней мере "ß" и "SS" совпадают. AFAIK, однако, не существует реализаций reg-exp для Java с полной поддержкой групп, наборов и подстановочных знаков.

Другие вопросы по тегам