Разделить строку переменной длины с помощью регулярного выражения

Я не знаю, возможно ли это с помощью regex. Я просто спрашиваю, если кто-то знает ответ.

У меня есть string ="hellohowareyou??". Мне нужно разбить его так:

[h, el, loh, owar, eyou?, ?].

Разделение выполняется таким образом, что первая строка будет иметь длину 1, вторую длину 2 и так далее. Последняя строка будет содержать оставшиеся символы. Я могу сделать это легко без регулярного выражения, используя такую ​​функцию.

public ArrayList<String> splitString(String s)
    {
        int cnt=0,i;
        ArrayList<String> sList=new ArrayList<String>();
        for(i=0;i+cnt<s.length();i=i+cnt)
        {
         cnt++;
         sList.add(s.substring(i,i+cnt));    
        }
        sList.add(s.substring(i,s.length()));
        return sList;
    }

Мне было просто интересно, можно ли это сделать с помощью регулярного выражения.

Ответ 1

Решение

Следующий фрагмент генерирует шаблон, который выполняет задание (видеть, как он работает на ideone.com):

// splits at indices that are triangular numbers
class TriangularSplitter {

  // asserts that the prefix of the string matches pattern
  static String assertPrefix(String pattern) {
    return "(?<=(?=^pattern).*)".replace("pattern", pattern);
  }
  // asserts that the entirety of the string matches pattern
  static String assertEntirety(String pattern) {
    return "(?<=(?=^pattern$).*)".replace("pattern", pattern);
  }
  // repeats an assertion as many times as there are dots behind current position
  static String forEachDotBehind(String assertion) {
    return "(?<=^(?:.assertion)*?)".replace("assertion", assertion);
  }

  public static void main(String[] args) {
    final String TRIANGULAR_SPLITTER =
      "(?x) (?<=^.) | measure (?=(.*)) check"
        .replace("measure", assertPrefix("(?: notGyet . +NBefore +1After)*"))
        .replace("notGyet", assertPrefix("(?! \\1 \\G)"))
        .replace("+NBefore", forEachDotBehind(assertPrefix("(\\1? .)")))
        .replace("+1After", assertPrefix(".* \\G (\\2?+ .)"))
        .replace("check", assertEntirety("\\1 \\G \\2 . \\3"))
        ;
    String text = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    System.out.println(
        java.util.Arrays.toString(text.split(TRIANGULAR_SPLITTER))
    );
    // [a, bc, def, ghij, klmno, pqrstu, vwxyzAB, CDEFGHIJ, KLMNOPQRS, TUVWXYZ]
  }
}

Обратите внимание, что это решение использует методы, уже описанные в моей серии статей regex. Единственное, что есть здесь, это \G и прямые ссылки.

Ссылки

Это краткое описание основных конструкций регулярных выражений:

  • (?x) - встроенный флаг modifier, чтобы включить free-spacing, где игнорируются пробелы без пробелов (и # может использоваться для комментариев).
  • ^ и $ - это начало и конец строки anchors. \G - конец предыдущего совпадения якорь.
  • | обозначает alternation (т.е. "или" ).
  • ? как спецификатор повторения обозначает optional (т.е. нуль или один). В качестве квантификатора повторения, например, .*? это означает, что повторение * (т.е. ноль или более) неохотно/non-greedy.
  • (…) используются для группировки. (?:…) - не захватывающая группа. Группа захвата сохраняет строку, в которой она соответствует; он позволяет, среди прочего, сопоставлять обратные/пересылаемые/вложенные ссылки (например, \1).
  • (?=…) является положительным lookahead; он имеет право утверждать, что существует совпадение заданного шаблона. (?<=…) - это положительный lookbehind; он смотрит влево.
  • (?!…) - отрицательный результат; он имеет право утверждать, что нет совпадения с шаблоном.

Связанные вопросы


Объяснение

Образец соответствует утверждениям с нулевой шириной. Достаточно сложный алгоритм используется для утверждения, что текущая позиция представляет собой треугольное число. Существуют две основные альтернативы:

  • (?<=^.), то есть мы можем смотреть и видеть начало строки в одной точке
    • Это соответствует индексу 1 и является важной отправной точкой для остальной части процесса.
  • В противном случае мы measure восстановим, как было выполнено последнее совпадение (используя \G в качестве контрольной точки), сохраняя результат измерения в группах "до" \G и "после" \G. Тогда check, если текущая позиция является той, которую предписывает измерение, чтобы найти, где должно быть выполнено следующее совпадение.

Таким образом, первая альтернатива - это тривиальный "базовый случай", а вторая альтернатива определяет, как сделать все последующие совпадения после этого. В Java нет пользовательских групп, но вот семантика для трех групп захвата:

  • \1 фиксирует строку "before" \G
  • \2 фиксирует строку "after" \G
  • Если длина \1 равна, например, 1 + 2 + 3 +... + k, то длина \2 должна быть k.
    • Следовательно, \2 . имеет длину k + 1 и должна быть следующей частью в нашем split!
  • \3 фиксирует строку справа от нашей текущей позиции
    • Следовательно, когда мы можем assertEntirety на \1 \G \2 . \3, мы сопоставляем и устанавливаем новый \G

Вы можете использовать математическую индукцию, чтобы строго доказать правильность этого алгоритма.

Чтобы проиллюстрировать, как это работает, позвольте работать с примером. Возьмем abcdefghijklm в качестве входных данных и скажем, что мы уже частично отделились от [a, bc, def].

          \G     we now need to match here!
           ↓       ↓
a b c d e f g h i j k l m n
\____1____/ \_2_/ . \__3__/   <--- \1 G \2 . \3
  L=1+2+3    L=3           

Помните, что \G знаменует окончание последнего совпадения, и это происходит при индексах треугольных чисел. Если \G произошло при 1 + 2 + 3 +... + k, то следующее соответствие должно быть k + 1 позиций после \G, чтобы быть индексом треугольного числа.

Таким образом, в нашем примере, где \G находится где мы просто разделились def, мы измерили, что k = 3, и следующее совпадение будет отделяться ghij, как ожидалось.

Чтобы построить \1 и \2 в соответствии с приведенной выше спецификацией, мы в основном выполняем цикл while: до тех пор, пока он notGyet, мы рассчитываем до k следующим образом:

  • +NBefore, т.е. мы расширяем \1 на один forEachDotBehind
  • +1After, т.е. мы расширяем \2 только одним

Обратите внимание, что notGyet содержит прямую ссылку на группу 1, которая определена позже в шаблоне. По существу, мы делаем цикл до тех пор, пока \1 "не ударит" \G.


Заключение

Излишне говорить, что это конкретное решение имеет ужасную производительность. Механизм регулярных выражений запоминает WHERE последнее совпадение (с \G) и забывает HOW (т.е. Все группы захвата reset, когда выполняется следующая попытка сопоставления). Затем наша модель должна восстанавливать HOW (ненужный шаг в традиционных решениях, где переменные не так "забывчивы" ), путем тщательного построения строк путем добавления одного символа за раз (т.е. O(N^2)). Каждое простое измерение является линейным, а не постоянным (поскольку оно выполняется как строка, соответствующая, где длина является фактором), и, кроме того, мы делаем много измерений, которые являются избыточными (т.е. Чтобы увеличить на единицу, нам нужно сначала повторить что у нас уже есть).

Есть, вероятно, много "лучших" решений регулярных выражений, чем этот. Тем не менее, сложность и неэффективность этого конкретного решения должны по праву предполагать, что регулярное выражение не предназначено для такого соответствия шаблонов.

Тем не менее, для учебных целей это абсолютно замечательная проблема, поскольку существует множество знаний в области исследования и формулирования ее решений. Надеюсь, это конкретное решение и его объяснение были поучительными.

Ответ 2

Задача регулярного выражения - распознавать шаблоны. Здесь вы не ищите шаблоны, а разделите длину. Так что регулярное выражение не подходит.

Это возможно, но не с одним регулярным выражением: чтобы найти первые n символов с помощью регулярного выражения, вы используете: "^ (. {n}). *"

Итак, вы можете искать с этим регулярным выражением для 1-го символа. Затем вы создаете подстроку, и вы ищете два следующих символа. Etc.

Как и @splash, это сделает код более сложным и неэффективным, поскольку вы используете регулярное выражение для чего-то вне их цели.

Ответ 3

String a = "hellohowareyou??";
int i = 1;

    while(true) {

        if(i >= a.length()) {
            System.out.println(a);
            break;
        }

        else {
            String b = a.substring(i++);
            String[] out = a.split(Pattern.quote(b) + "$");
            System.out.println(out[0]);
            a = b;
            if(b.isEmpty())
                break;
        }

    }