Wyrażenie regularne, aby podzielić camelCase lub TitleCase (zaawansowane)


81

Znalazłem genialne wyrażenie RegEx do wyodrębnienia części wyrażenia camelCase lub TitleCase.

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

Działa zgodnie z oczekiwaniami:

  • wartość -> wartość
  • camelValue -> camel / Wartość
  • TitleValue -> Title / Value

Na przykład w Javie:

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

Mój problem polega na tym, że w niektórych przypadkach nie działa:

  • Przypadek 1: WARTOŚĆ -> V / A / L / U / E
  • Przypadek 2: eclipseRCPExt -> eclipse / R / C / P / Ext

Moim zdaniem wynik powinien być:

  • Przypadek 1: VALUE
  • Przypadek 2: zaćmienie / RCP / Ext

Innymi słowy, mając n wielkich liter:

  • jeśli po n znakach następuje małe litery, grupami powinny być: (n-1 znaków) / (n-ty znak + dolne znaki)
  • jeśli n znaków jest na końcu, grupa powinna być: (n znaków).

Masz jakiś pomysł, jak ulepszyć to wyrażenie regularne?


Wygląda na to, że prawdopodobnie będziesz potrzebować modyfikatora warunkowego dla ^i innego warunkowego przypadku dla wielkich liter w ujemnej pozycji wstecznej. Nie testowałem na pewno, ale myślę, że byłby to najlepszy sposób na naprawienie problemu.
Nightfirecat

Jeśli ktoś bada
Clam

Odpowiedzi:


112

Następujące wyrażenie regularne działa dla wszystkich powyższych przykładów:

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

Działa poprzez wymuszenie negatywnego lookbehind, aby nie tylko ignorować dopasowania na początku ciągu, ale także ignorować dopasowania, w których duża litera jest poprzedzona inną wielką literą. Obsługuje przypadki takie jak „VALUE”.

Pierwsza część wyrażenia regularnego sama z siebie nie działa na „eclipseRCPExt”, ponieważ nie można podzielić między „RPC” i „Ext”. Taki jest cel drugiej klauzuli: (?<!^)(?=[A-Z][a-z]. Ta klauzula umożliwia podział przed każdą wielką literą, po której następuje mała litera, z wyjątkiem początku ciągu.


1
ten nie działa na PHP, a @ ridgerunner tak. W PHP mówi się, że „asercja typu lookbehind nie ma stałej długości przy przesunięciu 13”.
igorsantos 07

15
@Igoru: Smaki Regex są różne. Pytanie dotyczy Javy, a nie PHP, i taka jest odpowiedź.
NPE

1
podczas gdy pytanie jest oznaczone jako „java”, pytanie jest nadal ogólne - poza przykładami kodu (które nigdy nie mogą być ogólne). Tak więc, jeśli istnieje prostsza wersja tego wyrażenia regularnego, która działa również w wielu językach, pomyślałem, że ktoś powinien to wskazać :)
igorsantos 07

7
@Igoru: „Generic regex” to wyimaginowana koncepcja.
Casimir et Hippolyte

3
@ igorsantos07: Nie, wbudowane implementacje wyrażeń regularnych różnią się znacznie między platformami. Niektórzy próbują być podobni do Perla, niektórzy próbują być podobni do POSIX, a niektórzy są czymś pośrednim lub zupełnie innym.
Christoffer Hammarström

78

Wygląda na to, że komplikujesz to bardziej niż powinno. W przypadku camelCase lokalizacja podziału jest po prostu wszędzie tam, gdzie wielka litera występuje bezpośrednio po małej:

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

Oto jak to wyrażenie regularne dzieli przykładowe dane:

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

Jedyna różnica w stosunku do pożądanego wyniku polega na tym eclipseRCPExt, że według mnie jest on tutaj poprawnie podzielony.

Dodatek - ulepszona wersja

Uwaga: ta odpowiedź ostatnio zyskała uznanie i zdałem sobie sprawę, że jest lepszy sposób ...

Dodając drugą alternatywę do powyższego wyrażenia regularnego, wszystkie przypadki testowe OP są poprawnie podzielone.

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

Oto jak ulepszone wyrażenie regularne dzieli przykładowe dane:

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

Edycja: 20130824 Dodano ulepszoną wersję do obsługi RCPExt -> RCP / Extobudowy.


Dzięki za wkład. W tym przykładzie muszę oddzielić RCP i Ext, ponieważ konwertuję części na stałą nazwę (wskazówka dotycząca stylu: „wszystkie wielkie litery z podkreśleniem w celu oddzielenia słów.”) W tym przypadku wolę ECLIPSE_RCP_EXT od ECLIPSE_RCPEXT.
Jmini,

4
Dzięki za pomoc; Zmodyfikowałem twoje wyrażenie regularne, aby dodać kilka opcji dbania o cyfry w ciągu:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[0-9])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=[0-9])
thoroc

To najlepsza odpowiedź! Proste i przejrzyste. Jednak ta odpowiedź i oryginalne wyrażenie RegEx przez OP nie działają dla Javascript i Golang!
Viet,


10

Nie mogłem sprawić, aby rozwiązanie aix działało (i nie działa też na RegExr), więc wymyśliłem własne, które przetestowałem i wydaje się, że robi dokładnie to, czego szukasz:

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

a oto przykład użycia:

; 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])|($))))", "$1 ")
newString := Trim(newString)

Tutaj oddzielam każde słowo spacją, więc oto kilka przykładów przekształcania ciągu:

  • ThisIsATitleCASEString => To jest ciąg tytułu CASE
  • andThisOneIsCamelCASE => i This One Is Camel CASE

Powyższe rozwiązanie robi to, o co prosi oryginalny post, ale potrzebowałem również wyrażenia regularnego, aby znaleźć ciągi wielbłąda i pascala zawierające liczby, więc wymyśliłem również tę odmianę, aby uwzględnić liczby:

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

i przykład użycia:

; 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]))))", "$1 ")
newString := Trim(newString)

A oto kilka przykładów tego, jak ciąg z liczbami jest przekształcany za pomocą tego wyrażenia regularnego:

  • myVariable123 => moja zmienna 123
  • my2Variables => my 2 Variables
  • The3rdVariableIsHere => 3 rdVariable jest tutaj
  • 12345NumsAtTheStartIncludedToo => 12345 Numery na początku również wliczone

1
Zbyt wiele niepotrzebnych grup przechwytywania. Mógłbyś to zapisać jako: (^[a-z]+|[A-Z][a-z]+|[A-Z]+(?=[A-Z][a-z]|$))dla pierwszego i (^[a-z]+|[0-9]+|[A-Z][a-z]+|[A-Z]+(?=[A-Z][a-z]|$|[0-9]))dla drugiego. Najbardziej zewnętrzną można również usunąć, ale składnia odnosząca się do całego dopasowania nie jest przenośna między językami ( $0i $&są 2 możliwości).
nhahtdh

To samo uproszczone ([A-Z]?[a-z]+)|([A-Z]+(?=[A-Z][a-z]))
wyrażenie regularne

3

Aby obsłużyć więcej liter niż tylko A-Z:

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

Zarówno:

  • Podziel po dowolnej małej literze, po której następuje wielka litera.

Eg parseXML-> parse, XML.

lub

  • Podziel po dowolnej literze, po której następuje duża i mała litera.

Eg XMLParser-> XML, Parser.


W bardziej czytelnej formie:

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"));
    }    
}

3

Krótki

Obie najpopularniejsze odpowiedzi tutaj dostarczają kodu używającego pozytywnego lookbehinds, który nie jest obsługiwany przez wszystkie odmiany regex. Poniższe wyrażenie regularne przechwyci zarówno PascalCaseicamelCase i może być używany w wielu językach.

Uwaga: zdaję sobie sprawę, że to pytanie dotyczy Javy, jednak widzę również wiele wzmianek o tym poście w innych pytaniach oznaczonych dla różnych języków, a także kilka komentarzy do tego pytania w tym samym.

Kod

Zobacz używane wyrażenie regularne tutaj

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

Wyniki

Przykładowe wejście

eclipseRCPExt

SomethingIsWrittenHere

TEXTIsWrittenHERE

VALUE

loremIpsum

Przykładowe wyjście

eclipse
RCP
Ext

Something
Is
Written
Here

TEXT
Is
Written
HERE

VALUE

lorem
Ipsum

Wyjaśnienie

  • Dopasuj co najmniej jedną wielką literę alfabetu [A-Z]+
  • Lub dopasuj zero lub jedną wielką literę alfabetu [A-Z]?, po której następuje jeden lub więcej małych znaków alfa[a-z]+
  • Upewnij się, że to, co następuje, jest wielką literą alfabetu [A-Z]lub znakiem granicy słowa\b


0

Możesz użyć poniższego wyrażenia dla Java:

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

3
Cześć Maicon, witaj w StackOverflow i dziękuję za odpowiedź. Chociaż może to odpowiedzieć na pytanie, nie zapewnia żadnego wyjaśnienia dla innych, aby dowiedzieć się, jak rozwiązuje problem. Czy możesz zmienić swoją odpowiedź, aby zawierała wyjaśnienie kodu? Dziękuję Ci!
Tim Malone,

0

Zamiast szukać separatorów, których tam nie ma, możesz również rozważyć znalezienie składników nazwy (z pewnością istnieją):

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);

To wychodzi [eclipse, 福福, RCP, Ext]. Konwersja do tablicy jest oczywiście prosta.


0

Mogę potwierdzić, że ciąg wyrażenia regularnego ([A-Z]+|[A-Z]?[a-z]+)(?=[A-Z]|\b) podany przez ctwheels powyżej działa z odmianą wyrażenia regularnego firmy Microsoft.

Chciałbym również zasugerować następującą alternatywę, opartą na wyrażeniu regularnym ctwheels, które obsługuje znaki numeryczne: ([A-Z0-9]+|[A-Z]?[a-z]+)(?=[A-Z0-9]|\b) .

To jest w stanie podzielić ciągi, takie jak:

DrivingB2BTradeIn2019Onwards

do

Napędzanie handlu B2B w 2019 r


0

Rozwiązanie JavaScript

/**
 * howToDoThis ===> ["", "how", "To", "Do", "This"]
 * @param word word to be split
 */
export const splitCamelCaseWords = (word: string) => {
    if (typeof word !== 'string') return [];
    return word.replace(/([A-Z]+|[A-Z]?[a-z]+)(?=[A-Z]|\b)/g, '!$&').split('!');
};

Proszą o rozwiązanie JavaScript, a dlaczego dajesz dwa razy to samo rozwiązanie ? Jeśli uważasz, że te pytania są niezależne, zagłosuj, aby zamknąć jedno jako duplikat.
Toto
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.