Как работает {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.