Как работает {m} {n} ( "ровно n раз" )?
Итак, так или иначе (играя вокруг), я нашел себя с регулярным выражением, например \d{1}{2}
.
Логически для меня это должно означать:
(цифра ровно один раз) ровно в два раза, т.е. цифра ровно в два раза.
Но это, по сути, означает просто "цифру ровно один раз" (таким образом, игнорируя {2}
).
String regex = "^\\d{1}{2}$"; // ^$ to make those not familiar with 'matches' happy
System.out.println("1".matches(regex)); // true
System.out.println("12".matches(regex)); // false
Аналогичные результаты можно увидеть, используя {n}{m,n}
или аналогичный.
Почему это происходит? Является ли это явно указано в документации regex/Java где-то или это просто решение, которое разработчики Java сделали "на лету" или это может быть ошибка?
Или это на самом деле не игнорируется, и это на самом деле означает что-то еще?
Не так уж важно, но это не поведенческое поведение регулярных выражений, Rubular делает то, что я ожидаю.
Примечание. Заголовок предназначается, в основном, для поиска пользователей, которые хотят знать, как это работает (не почему).
Ответы
Ответ 1
Когда я ввожу ваше регулярное выражение в RegexBuddy с помощью синтаксиса regex Java, он отображает следующее сообщение
Квантерам должен предшествовать токен, который может быть повторен "{2}"
Изменение регулярного выражения для явного использования группировки ^(\d{1}){2}
решает эту ошибку и работает так, как вы ожидаете.
Я предполагаю, что механизм regex java просто игнорирует ошибку/выражение и работает с тем, что было скомпилировано до сих пор.
Edit
Ссылка на IEEE-Standard в @piet.t ответить похоже, поддерживает это предположение.
Изменить 2 (kudos to @fncomp)
Для полноты можно использовать (?:)
, чтобы избежать захвата группы. Тогда полное регулярное выражение становится ^(?:\d{1}){2}
Ответ 2
IEEE-Standard 1003.1 говорит:
Поведение нескольких соседних символов дублирования ('*' и интервалов) приводит к результатам undefined.
Поэтому каждая реализация может делать то, что ей нравится, просто не полагайтесь на что-то конкретное...
Ответ 3
Научный подход:
нажмите на шаблоны, чтобы увидеть пример на regexplanet.com, и нажмите зеленую кнопку Java.
- Вы уже показали
\d{1}{2}
соответствие "1"
и не соответствует "12"
, поэтому мы знаем, что это isn 't интерпретируется как (?:\d{1}){2}
.
- Тем не менее, 1 является скучным номером, и
{1}
может быть оптимизирован, попробуем попробовать что-то более интересное:
\d{2}{3}
. Это по-прежнему соответствует только двум символам (не шесть), {3}
игнорируется.
- Ok. Там есть простой способ увидеть, что делает двигатель регулярных выражений. Захватывает ли он?
Давайте попробуем
(\d{1})({2})
. Как ни странно, это работает. Вторая группа, $2
, фиксирует пустую строку.
- Итак, зачем нам нужна первая группа? Как насчет
({1})
? Все еще работает.
- И просто
{1}
? Проблем нет.
Похоже, что Java здесь немного странно.
-
Отлично! Итак, {1}
. Мы знаем, что Java расширяет *
и +
до {0,0x7FFFFFFF}
и {1,0x7FFFFFFF}
, так работает *
или +
? Нет:
Висячий метасимвол "+" рядом с индексом 0
+
^
Проверка должна выполняться до *
и +
.
Я не нашел ничего в спецификации, которая объясняет это, похоже, что квантификатор должен появиться по крайней мере после символа, скобок или круглых скобок.
Большинство этих шаблонов считаются недействительными с помощью других ароматизаторов регулярных выражений, и по уважительной причине они не имеют смысла.
Ответ 4
Сначала я был удивлен, что это не бросает PatternSyntaxException
.
Я не могу основать свой ответ на какие-либо факты, так что это всего лишь образованное предположение:
"\\d{1}" // matches a single digit
"\\d{1}{2}" // matches a single digit followed by two empty strings
Ответ 5
Я никогда не видел синтаксиса {m}{n}
в любом месте. Кажется, что механизм регулярных выражений на этой странице Rubular применяет квантор {2}
к наименьшему возможному токену до этого - который равен \\d{1}
. Чтобы подражать этому в Java (или, возможно, в большинстве других двигателей с регулярными выражениями), вам нужно сгруппировать \\d{1}
так:
^(\\d{1}){2}$
Посмотрите в действии здесь.
Ответ 6
Скомпилированная структура регулярного выражения
Ответ Kobi содержит информацию о поведении Java regex (реализация Sun/Oracle) для случая "^\\d{1}{2}$"
или "{1}"
.
Ниже приведена внутренняя скомпилированная структура "^\\d{1}{2}$"
:
^\d{1}{2}$
Begin. \A or default ^
Curly. Greedy quantifier {1,1}
Ctype. POSIX (US-ASCII): DIGIT
Node. Accept match
Curly. Greedy quantifier {2,2}
Slice. (length=0)
Node. Accept match
Dollar(multiline=false). \Z or default $
java.util.regex.Pattern$LastNode
Node. Accept match
Глядя на исходный код
Из моего исследования ошибка связана, вероятно, с тем, что {
не проверяется надлежащим образом в частном методе sequence()
.
Метод sequence()
вызывает atom()
для анализа атома, затем присоединяет квантор к атому, вызывая closure()
, и объединяет все атомы с замыканием в одну последовательность.
Например, с учетом этого регулярного выражения:
^\d{4}a(bc|gh)+d*$
Затем вызов верхнего уровня sequence()
получит скомпилированные узлы для ^
, \d{4}
, a
, (bc|gh)+
, d*
, $
и соединить их вместе.
С учетом этой идеи давайте посмотрим на исходный код sequence()
, скопированный из OpenJDK 8-b132 (Oracle использует одна и та же база кода):
@SuppressWarnings("fallthrough")
/**
* Parsing of sequences between alternations.
*/
private Node sequence(Node end) {
Node head = null;
Node tail = null;
Node node = null;
LOOP:
for (;;) {
int ch = peek();
switch (ch) {
case '(':
// Because group handles its own closure,
// we need to treat it differently
node = group0();
// Check for comment or flag group
if (node == null)
continue;
if (head == null)
head = node;
else
tail.next = node;
// Double return: Tail was returned in root
tail = root;
continue;
case '[':
node = clazz(true);
break;
case '\\':
ch = nextEscaped();
if (ch == 'p' || ch == 'P') {
boolean oneLetter = true;
boolean comp = (ch == 'P');
ch = next(); // Consume { if present
if (ch != '{') {
unread();
} else {
oneLetter = false;
}
node = family(oneLetter, comp);
} else {
unread();
node = atom();
}
break;
case '^':
next();
if (has(MULTILINE)) {
if (has(UNIX_LINES))
node = new UnixCaret();
else
node = new Caret();
} else {
node = new Begin();
}
break;
case '$':
next();
if (has(UNIX_LINES))
node = new UnixDollar(has(MULTILINE));
else
node = new Dollar(has(MULTILINE));
break;
case '.':
next();
if (has(DOTALL)) {
node = new All();
} else {
if (has(UNIX_LINES))
node = new UnixDot();
else {
node = new Dot();
}
}
break;
case '|':
case ')':
break LOOP;
case ']': // Now interpreting dangling ] and } as literals
case '}':
node = atom();
break;
case '?':
case '*':
case '+':
next();
throw error("Dangling meta character '" + ((char)ch) + "'");
case 0:
if (cursor >= patternLength) {
break LOOP;
}
// Fall through
default:
node = atom();
break;
}
node = closure(node);
if (head == null) {
head = tail = node;
} else {
tail.next = node;
tail = node;
}
}
if (head == null) {
return end;
}
tail.next = end;
root = tail; //double return
return head;
}
Обратите внимание на строку throw error("Dangling meta character '" + ((char)ch) + "'");
. Здесь возникает ошибка, если +
, *
, ?
свисают и не являются частью предыдущего токена. Как вы можете видеть, {
не относится к числу случаев, чтобы вызывать ошибку. Фактически, его нет в списке случаев в sequence()
, и процесс компиляции будет передаваться на default
случай непосредственно на atom()
.
@SuppressWarnings("fallthrough")
/**
* Parse and add a new Single or Slice.
*/
private Node atom() {
int first = 0;
int prev = -1;
boolean hasSupplementary = false;
int ch = peek();
for (;;) {
switch (ch) {
case '*':
case '+':
case '?':
case '{':
if (first > 1) {
cursor = prev; // Unwind one character
first--;
}
break;
// Irrelevant cases omitted
// [...]
}
break;
}
if (first == 1) {
return newSingle(buffer[0]);
} else {
return newSlice(buffer, first, hasSupplementary);
}
}
Когда процесс входит в atom()
, так как он сразу встречает {
, он прерывается от цикла switch
и for
, а создается новый срез с длиной 0 ( длина составляет от first
, что равно 0).
Когда этот фрагмент возвращается, квантификатор анализируется на closure()
, что приводит к тому, что мы видим.
Сравнивая исходный код Java 1.4.0, Java 5 и Java 8, в исходном коде sequence()
и atom()
, похоже, не так много изменений. Кажется, что эта ошибка была с самого начала.
Стандарт для регулярного выражения
опрос с высоким рейтингом, цитируя IEEE-Standard 1003.1 (или POSIX стандарт) не имеет отношения к обсуждению, поскольку Java не реализует BRE и ERE.
Существует много синтаксисов, приводящих к поведению undefined в соответствии со стандартом, но хорошо определенное поведение во многих других вариантах регулярных выражений (хотя согласны они или нет, это другое дело). Например, \d
соответствует undefined в соответствии со стандартом, но он соответствует цифрам (ASCII/Unicode) во многих вариантах регулярных выражений.
К сожалению, нет другого стандарта в синтаксисе регулярных выражений.
Однако в Unicode Regular Expression имеется стандарт, который фокусируется на особенностях, которые должен иметь механизм регулярных выражений Unicode. Класс Java Pattern
более или менее реализует поддержку уровня 1, как описано в UTS # 18: Unicode Regular Expression и RL2.1 (хотя и чрезвычайно багги).
Ответ 7
Я предполагаю, что в определении {}
есть что-то вроде "оглянуться назад, чтобы найти действительное выражение (исключая себя - {}
", поэтому в вашем примере между }
и {
нет ничего.
В любом случае, если вы завернете его в скобки, он будет работать так, как вы ожидали: http://refiddle.com/gv6.