RegEx para dividir camelCase o TitleCase (avanzado)

81

Encontré una expresión regular brillante para extraer la parte de una expresión camelCase o TitleCase.

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

Funciona como se esperaba:

  • valor -> valor
  • camelValue -> camel / Value
  • TitleValue -> Título / Valor

Por ejemplo con Java:

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

Mi problema es que no funciona en algunos casos:

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

En mi opinión, el resultado debería ser:

  • Caso 1: VALOR
  • Caso 2: eclipse / RCP / Ext

En otras palabras, dados n caracteres en mayúscula:

  • si los n caracteres van seguidos de minúsculas, los grupos deben ser: (n-1 caracteres) / (n-ésimo carácter + caracteres inferiores)
  • si los n caracteres están al final, el grupo debería ser: (n caracteres).

¿Alguna idea de cómo mejorar esta expresión regular?

Jmini
fuente
Parece que probablemente necesitaría un modificador condicional en ^y otro caso condicional para letras mayúsculas en la búsqueda hacia atrás negativa. No lo he probado con seguridad, pero creo que esa sería su mejor opción para solucionar el problema.
Nightfirecat
Si alguien está examinando
Clam

Respuestas:

112

La siguiente expresión regular funciona para todos los ejemplos anteriores:

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

Funciona obligando a la búsqueda hacia atrás negativa no solo para ignorar las coincidencias al comienzo de la cadena, sino también para ignorar las coincidencias en las que una letra mayúscula está precedida por otra letra mayúscula. Esto maneja casos como "VALOR".

La primera parte de la expresión regular por sí sola falla en "eclipseRCPExt" al no dividirse entre "RPC" y "Ext". Este es el propósito de la segunda cláusula: (?<!^)(?=[A-Z][a-z]. Esta cláusula permite una división antes de cada letra mayúscula seguida de una letra minúscula, excepto al comienzo de la cadena.

NPE
fuente
1
este no funciona en PHP, mientras que @ ridgerunner sí. En PHP dice "la aserción de búsqueda atrás no tiene una longitud fija en el desplazamiento 13".
igorsantos07
15
@Igoru: Los sabores de Regex varían. La pregunta es sobre Java, no sobre PHP, y también lo es la respuesta.
NPE
1
mientras que la pregunta está etiquetada como "java", la pregunta sigue siendo genérica, además de los ejemplos de código (que nunca podrían ser genéricos). Entonces, si hay una versión más simple de esta expresión regular y que también funciona en
varios
7
@Igoru: La "expresión regular genérica" ​​es un concepto imaginario.
Casimir et Hippolyte
3
@ igorsantos07: No, las implementaciones de expresiones regulares integradas varían enormemente entre plataformas. Algunos intentan ser similares a Perl, algunos intentan ser similares a POSIX y algunos son algo intermedio o completamente diferente.
Christoffer Hammarström
78

Parece que está haciendo esto más complicado de lo necesario. Para camelCase , la ubicación de división es simplemente cualquier lugar donde una letra mayúscula siga inmediatamente a una letra minúscula:

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

Así es como esta expresión regular divide sus datos de ejemplo:

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

La única diferencia con el resultado deseado es con el eclipseRCPExt, que yo diría que está dividido correctamente aquí.

Anexo - Versión mejorada

Nota: Esta respuesta recientemente recibió un voto positivo y me di cuenta de que hay una mejor manera ...

Al agregar una segunda alternativa a la expresión regular anterior, todos los casos de prueba del OP se dividen correctamente.

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

Así es como la expresión regular mejorada divide los datos de ejemplo:

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

Editar: 20130824 Se agregó una versión mejorada para manejar el RCPExt -> RCP / Extestuche.

ridgerunner
fuente
Gracias por tu contribución. Necesito separar RCP y Ext en este ejemplo, porque convierto las partes en un nombre constante (Pauta de estilo: "todo en mayúsculas con guión bajo para separar palabras"). En este caso, prefiero ECLIPSE_RCP_EXT a ECLIPSE_RCPEXT.
Jmini
4
Gracias por la ayuda; He modificado su expresión regular para agregar un par de opciones para cuidar los dígitos en la cadena:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[0-9])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=[0-9])
thoroc
¡Esta es la mejor respuesta! Sencillo y claro. Sin embargo, esta respuesta y la expresión regular original del OP no funcionan para Javascript y Golang.
viet
10

No pude hacer que la solución de aix funcionara (y tampoco funciona en RegExr), así que se me ocurrió la mía propia que probé y parece hacer exactamente lo que estás buscando:

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

y aquí hay un ejemplo de cómo usarlo:

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

Aquí estoy separando cada palabra con un espacio, así que aquí hay algunos ejemplos de cómo se transforma la cadena:

  • ThisIsATitleCASEString => Esta es una cadena de título CASE
  • andThisOneIsCamelCASE => y este es Camel CASE

Esta solución anterior hace lo que pide la publicación original, pero también necesitaba una expresión regular para encontrar cadenas de camello y pascal que incluían números, por lo que también se me ocurrió esta variación para incluir números:

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

y un ejemplo de su uso:

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

Y aquí hay algunos ejemplos de cómo una cadena con números se transforma con esta expresión regular:

  • myVariable123 => mi Variable 123
  • my2Variables => mis 2 variables
  • The3rdVariableIsHere => La 3 rdVariable está aquí
  • 12345NumsAtTheStartIncludedToo => 12345 Nums al principio también incluidos
perro mortal
fuente
1
Demasiados grupos de captura innecesarios. Podrías haberlo escrito como: (^[a-z]+|[A-Z][a-z]+|[A-Z]+(?=[A-Z][a-z]|$))para el primero y (^[a-z]+|[0-9]+|[A-Z][a-z]+|[A-Z]+(?=[A-Z][a-z]|$|[0-9]))para el segundo. La mayoría externa también se puede eliminar, pero la sintaxis para referirse a la coincidencia completa no es portátil entre idiomas ( $0y $&hay 2 posibilidades).
nhahtdh
La misma ([A-Z]?[a-z]+)|([A-Z]+(?=[A-Z][a-z]))
expresión regular
3

Para manejar más letras que solo A-Z:

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

Ya sea:

  • Dividir después de cualquier letra minúscula, seguida de una letra mayúscula.

Por ejemplo parseXML-> parse, XML.

o

  • Dividir después de cualquier letra, seguida de mayúscula y minúscula.

Por ejemplo XMLParser-> XML, Parser.


En forma más legible:

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"));
    }    
}
Christoffer Hammarström
fuente
3

Breve

Ambas respuestas principales aquí proporcionan código usando búsquedas retrospectivas positivas, que no es compatible con todos los sabores de expresiones regulares. La expresión regular a continuación se captura tanto PascalCasey camelCasey se puede utilizar en múltiples idiomas.

Nota: Me doy cuenta de que esta pregunta está relacionada con Java, sin embargo, también veo varias menciones de esta publicación en otras preguntas etiquetadas para diferentes idiomas, así como algunos comentarios sobre esta pregunta para el mismo.

Código

Vea esta expresión regular en uso aquí

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

Resultados

Entrada de muestra

eclipseRCPExt

SomethingIsWrittenHere

TEXTIsWrittenHERE

VALUE

loremIpsum

Salida de muestra

eclipse
RCP
Ext

Something
Is
Written
Here

TEXT
Is
Written
HERE

VALUE

lorem
Ipsum

Explicación

  • Coincidir con uno o más caracteres alfabéticos en mayúscula [A-Z]+
  • O haga coincidir cero o un carácter alfabético en mayúscula [A-Z]?, seguido de uno o más caracteres alfabéticos en minúscula[a-z]+
  • Asegúrese de que lo que sigue sea un carácter alfabético en mayúsculas [A-Z]o un carácter de límite de palabra\b
ctwheels
fuente
0

Puede utilizar la siguiente expresión para Java:

(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?=[A-Z][a-z])|(?<=\\d)(?=\\D)|(?=\\d)(?<=\\D)
Maicon Zucco
fuente
3
Hola Maicon, bienvenido a StackOverflow y gracias por tu respuesta. Si bien esto puede responder la pregunta, no proporciona ninguna explicación para que otros aprendan cómo resuelve el problema. ¿Podrías editar tu respuesta para incluir una explicación de tu código? ¡Gracias!
Tim Malone
0

En lugar de buscar separadores que no están allí , también podría considerar buscar los componentes del nombre (ciertamente están allí):

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

Esto salidas [eclipse, 福福, RCP, Ext]. La conversión a una matriz es, por supuesto, simple.

Maarten Bodewes
fuente
0

Puedo confirmar que la cadena ([A-Z]+|[A-Z]?[a-z]+)(?=[A-Z]|\b)de expresiones regulares dada por ctwheels, arriba, funciona con el sabor de expresiones regulares de Microsoft.

También me gustaría sugerir la siguiente alternativa, basada en expresiones regulares ctwheels', que se ocupa de caracteres numéricos: ([A-Z0-9]+|[A-Z]?[a-z]+)(?=[A-Z0-9]|\b).

Esta capaz de dividir cadenas como:

ConducciónB2BTradeIn2019 Adelante

a

Impulsar el comercio B2B en 2019 en adelante

William Bell
fuente
0

Una solución de 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('!');
};
Akshay Vijay Jain
fuente
Te piden una solución de JavaScript, ¿y por qué das el doble de la misma solución ? Si cree que esas preguntas son idénticas, vote para cerrar una como duplicada.
Toto