Регулярное выражение для разделения camelCase или TitleCase (дополнительно)


нашел блестящее регулярное выражение для извлечения части выражения camelCase или TitleCase.

 (?<!^)(?=[A-Z])

он работает так, как ожидалось:

  • value - > value
  • camelValue -> camel / Value
  • TitleValue -> Title / Value

например с Java:

String s = "loremIpsum";
words = s.split("(?<!^)(?=[A-Z])");
//words equals words = new String[]{"lorem","Ipsum"}

моя проблема заключается в том, что он не работает в некоторых случаях:

  • Case 1: VALUE - > V / A / L / U / E
  • Case 2: eclipseRCPExt - > eclipse / R / C / P / Ext

на мой взгляд, результат должен быть:

  • Случай 1: значение
  • случай 2: eclipse / RCP / Ext

другими словами, учитывая N символов верхнего регистра:

  • если за n символами следуют символы нижнего регистра, то группы должны быть: (n-1 символ) / (n-й символ + Нижний символ)
  • если n символов находятся в конце, группа должна быть: (n пеструшки.)

есть идеи о том, как улучшить это регулярное выражение?

8 68

8 ответов:

следующее регулярное выражение работает для всех приведенных выше примерах:

public static void main(String[] args)
{
    for (String w : "camelValue".split("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])")) {
        System.out.println(w);
    }
}   

он работает, заставляя отрицательный lookbehind не только игнорировать совпадения в начале строки, но также игнорировать совпадения, где заглавная буква предшествует другой заглавной букве. Это обрабатывает случаи, такие как"значение".

первая часть регулярного выражения сама по себе не работает на "eclipseRCPExt", не сумев разделить между "RPC" и "Ext". Это цель второго пункта: (?<!^)(?=[A-Z][a-z]. Этот предложение допускает разделение перед каждой заглавной буквой, за которой следует строчная буква, за исключением начала строки.

кажется, вы делаете это более сложным, чем он должен быть. Ибо camelCase, расположение разделения просто в любом месте заглавная буква сразу следует за строчной буквой:

(?<=[a-z])(?=[A-Z])

вот как это регулярное выражение разбивает свой пример:

  • value -> value
  • camelValue -> camel / Value
  • TitleValue -> Title / Value
  • VALUE -> VALUE
  • eclipseRCPExt -> eclipse / RCPExt

только отличие от вашего желаемого выхода заключается в eclipseRCPExt, который я бы сказал, правильно разделен здесь.

дополнения - улучшенная версия

Примечание: этот ответ недавно получил upvote и я понял, что есть лучший путь...

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

(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])

вот как улучшенное регулярное выражение разбивает пример данные:

  • value -> value
  • camelValue -> camel / Value
  • TitleValue -> Title / Value
  • VALUE -> VALUE
  • eclipseRCPExt -> eclipse / RCP / Ext

Edit: 20130824 добавлена улучшенная версия для ручки RCPExt -> RCP / Ext случае.

другим решением было бы использовать выделенный метод в commons-lang:StringUtils#splitByCharacterTypeCamelCase

Я не мог заставить решение aix работать (и оно также не работает на RegExr), поэтому я придумал свой собственный, который я протестировал и, похоже, делает именно то, что вы ищете:

((^[a-z]+)|([A-Z]{1}[a-z]+)|([A-Z]+(?=([A-Z][a-z])|($))))

и вот пример его использования:

; Regex Breakdown:  This will match against each word in Camel and Pascal case strings, while properly handling acrynoms.
;   (^[a-z]+)                       Match against any lower-case letters at the start of the string.
;   ([A-Z]{1}[a-z]+)                Match against Title case words (one upper case followed by lower case letters).
;   ([A-Z]+(?=([A-Z][a-z])|($)))    Match against multiple consecutive upper-case letters, leaving the last upper case letter out the match if it is followed by lower case letters, and including it if it's followed by the end of the string.
newString := RegExReplace(oldCamelOrPascalString, "((^[a-z]+)|([A-Z]{1}[a-z]+)|([A-Z]+(?=([A-Z][a-z])|($))))", " ")
newString := Trim(newString)

здесь я разделяю каждое слово пробелом, поэтому вот несколько примеров того, как строка преобразуется:

  • ThisIsATitleCASEString => это строка заголовка
  • andThisOneIsCamelCASE => и это один случай верблюда

это решение выше делает то, что запрашивает исходный пост, но мне также нужно было регулярное выражение, чтобы найти строки camel и pascal, которые включали числа, поэтому я также придумал этот вариант, чтобы включить числа:

((^[a-z]+)|([0-9]+)|([A-Z]{1}[a-z]+)|([A-Z]+(?=([A-Z][a-z])|($)|([0-9]))))

и пример его использования:

; Regex Breakdown:  This will match against each word in Camel and Pascal case strings, while properly handling acrynoms and including numbers.
;   (^[a-z]+)                               Match against any lower-case letters at the start of the command.
;   ([0-9]+)                                Match against one or more consecutive numbers (anywhere in the string, including at the start).
;   ([A-Z]{1}[a-z]+)                        Match against Title case words (one upper case followed by lower case letters).
;   ([A-Z]+(?=([A-Z][a-z])|($)|([0-9])))    Match against multiple consecutive upper-case letters, leaving the last upper case letter out the match if it is followed by lower case letters, and including it if it's followed by the end of the string or a number.
newString := RegExReplace(oldCamelOrPascalString, "((^[a-z]+)|([0-9]+)|([A-Z]{1}[a-z]+)|([A-Z]+(?=([A-Z][a-z])|($)|([0-9]))))", " ")
newString := Trim(newString)

и вот несколько примеров того, как строка с числами преобразуется с помощью этого регулярного выражения:

  • myVariable123 = > моя переменная 123
  • my2Variables = > мои 2 переменные
  • The3rdVariableIsHere = > 3 rdVariable здесь
  • 12345NumsAtTheStartIncludedToo => 12345 Nums в начале включены тоже

обрабатывать больше букв, чем просто A-Z:

s.split("(?<=\p{Ll})(?=\p{Lu})|(?<=\p{L})(?=\p{Lu}\p{Ll})");

либо:

  • разделить после любой строчной буквы, за которой следует заглавная буква.

например parseXML ->parse,XML.

или

  • разделить после любой буквы, за которой следует буква верхнего регистра и строчная буква.

например.XMLParser ->XML,Parser.


в более читаемая форма:

public class SplitCamelCaseTest {

    static String BETWEEN_LOWER_AND_UPPER = "(?<=\p{Ll})(?=\p{Lu})";
    static String BEFORE_UPPER_AND_LOWER = "(?<=\p{L})(?=\p{Lu}\p{Ll})";

    static Pattern SPLIT_CAMEL_CASE = Pattern.compile(
        BETWEEN_LOWER_AND_UPPER +"|"+ BEFORE_UPPER_AND_LOWER
    );

    public static String splitCamelCase(String s) {
        return SPLIT_CAMEL_CASE.splitAsStream(s)
                        .collect(joining(" "));
    }

    @Test
    public void testSplitCamelCase() {
        assertEquals("Camel Case", splitCamelCase("CamelCase"));
        assertEquals("lorem Ipsum", splitCamelCase("loremIpsum"));
        assertEquals("XML Parser", splitCamelCase("XMLParser"));
        assertEquals("eclipse RCP Ext", splitCamelCase("eclipseRCPExt"));
        assertEquals("VALUE", splitCamelCase("VALUE"));
    }    
}

этой

оба верхних ответа здесь предоставляют код с использованием положительных lookbehinds, который не поддерживается всеми вкусами регулярных выражений. Регулярное выражение ниже будет захватывать оба PascalCase и camelCase и может использоваться на нескольких языках.

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

код

смотрите это регулярное выражение в использовании здесь

([A-Z]+|[A-Z]?[a-z]+)(?=[A-Z]|\b)

результаты

Пример Ввода

eclipseRCPExt

SomethingIsWrittenHere

TEXTIsWrittenHERE

VALUE

loremIpsum

Пример Вывода

eclipse
RCP
Ext

Something
Is
Written
Here

TEXT
Is
Written
HERE

VALUE

lorem
Ipsum

объяснение

  • сопоставьте один или несколько буквенных символов верхнего регистра [A-Z]+
  • или соответствует нулю или одному прописному Альфа-символу [A-Z]?, за которым следует одна или более строчных букв [a-z]+
  • убедитесь, что ниже находится заглавный Альфа-символ [A-Z] или символ границы слова \b

вы можете использовать выражение ниже для Java:

(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?=[A-Z][a-z])|(?<=\d)(?=\D)|(?=\d)(?<=\D)

вместо того, чтобы искать разделители,нет вы также можете рассмотреть вопрос о поиске компонентов имени (они, безусловно, есть):

String test = "_eclipse福福RCPExt";

Pattern componentPattern = Pattern.compile("_? (\p{Upper}?\p{Lower}+ | (?:\p{Upper}(?!\p{Lower}))+ \p{Digit}*)", Pattern.COMMENTS);

Matcher componentMatcher = componentPattern.matcher(test);
List<String> components = new LinkedList<>();
int endOfLastMatch = 0;
while (componentMatcher.find()) {
    // matches should be consecutive
    if (componentMatcher.start() != endOfLastMatch) {
        // do something horrible if you don't want garbage in between

        // we're lenient though, any Chinese characters are lucky and get through as group
        String startOrInBetween = test.substring(endOfLastMatch, componentMatcher.start());
        components.add(startOrInBetween);
    }
    components.add(componentMatcher.group(1));
    endOfLastMatch = componentMatcher.end();
}

if (endOfLastMatch != test.length()) {
    String end = test.substring(endOfLastMatch, componentMatcher.start());
    components.add(end);
}

System.out.println(components);

выводит [eclipse, 福福, RCP, Ext]. Преобразование в массив, конечно, просто.