¿Cómo convierto CamelCase en nombres legibles por humanos en Java?


157

Me gustaría escribir un método que convierta CamelCase en un nombre legible para humanos.

Aquí está el caso de prueba:

public void testSplitCamelCase() {
    assertEquals("lowercase", splitCamelCase("lowercase"));
    assertEquals("Class", splitCamelCase("Class"));
    assertEquals("My Class", splitCamelCase("MyClass"));
    assertEquals("HTML", splitCamelCase("HTML"));
    assertEquals("PDF Loader", splitCamelCase("PDFLoader"));
    assertEquals("A String", splitCamelCase("AString"));
    assertEquals("Simple XML Parser", splitCamelCase("SimpleXMLParser"));
    assertEquals("GL 11 Version", splitCamelCase("GL11Version"));
}

55
Primero, deberá especificar las reglas de la conversión. Por ejemplo, ¿cómo se PDFLoaderconvierte PDF Loader?
Jørn Schou-Rode

2
Llamo a ese formato "PascalCase". En "camelCase" la primera letra debe estar en minúscula. Al menos en lo que respecta a los desarrolladores. msdn.microsoft.com/en-us/library/x2dbyw72(v=vs.71).aspx
Muhd

Respuestas:


337

Esto funciona con sus casos de prueba:

static String splitCamelCase(String s) {
   return s.replaceAll(
      String.format("%s|%s|%s",
         "(?<=[A-Z])(?=[A-Z][a-z])",
         "(?<=[^A-Z])(?=[A-Z])",
         "(?<=[A-Za-z])(?=[^A-Za-z])"
      ),
      " "
   );
}

Aquí hay un arnés de prueba:

    String[] tests = {
        "lowercase",        // [lowercase]
        "Class",            // [Class]
        "MyClass",          // [My Class]
        "HTML",             // [HTML]
        "PDFLoader",        // [PDF Loader]
        "AString",          // [A String]
        "SimpleXMLParser",  // [Simple XML Parser]
        "GL11Version",      // [GL 11 Version]
        "99Bottles",        // [99 Bottles]
        "May5",             // [May 5]
        "BFG9000",          // [BFG 9000]
    };
    for (String test : tests) {
        System.out.println("[" + splitCamelCase(test) + "]");
    }

Utiliza expresiones regulares de coincidencia de longitud cero con mirar hacia atrás y hacia adelante para encontrar dónde insertar espacios. Básicamente hay 3 patrones, y los uso String.formatpara juntarlos y hacerlos más legibles.

Los tres patrones son:

UC detrás de mí, UC seguido de LC delante de mí

  XMLParser   AString    PDFLoader
    /\        /\           /\

no UC detrás de mí, UC delante de mí

 MyClass   99Bottles
  /\        /\

Carta detrás de mí, sin carta delante de mí

 GL11    May5    BFG9000
  /\       /\      /\

Referencias

Preguntas relacionadas

Usando búsquedas de coincidencia de longitud cero para dividir:


1
El concepto también funciona en C # (con las mismas expresiones regulares, pero un marco de expresión regular diferente, por supuesto). Excelente trabajo. ¡Gracias!
mmm

No parece estar funcionando para mí en Python, podría ser porque el motor de expresiones regulares no es el mismo. Me temo que tendré que intentar hacer algo menos elegante. :)
MarioVilas

2
¿Alguien podría explicar qué significan% s |% s |% s con respecto a los casos de prueba y también en general?
Ari53nN3o

1
@ Ari53nN3o: Los " %s" son marcadores de posición para los String.format(String format, args...)argumentos. También puede llamar por índice:String.format("%$1s|%$2s|%$3s", ...
Sr. Polywhirl

¿Cómo funcionará esto en C #? No hay relaceAlltampoco quiero agregar división si la cadena tiene " ." en eso.
sarojanand

119

Puedes hacerlo usando org.apache.commons.lang.StringUtils

StringUtils.join(
     StringUtils.splitByCharacterTypeCamelCase("ExampleTest"),
     ' '
);

9
Esta solución es mucho mejor que la más votada porque: a) No reinventa la rueda: commons-lang es un estándar de facto y funciona bien, muy centrado en el rendimiento. b) Cuando la conversión se realiza muchas veces, este método es mucho más rápido que el basado en expresiones regulares: este es mi punto de referencia para ejecutar las pruebas mencionadas 100,000 veces: `` el método basado en expresiones regulares tomó 4820 milisegundos ///// ///// el método basado en commons-lang tomó 232 milisegundos `` ¡eso es aproximadamente 20 veces más rápido que el que usa expresiones regulares!
Clint Eastwood

2
Definitivamente estoy de acuerdo con Clint en este caso, esta debería ser la respuesta aceptada. El rendimiento es una cosa, pero usar una biblioteca probada en batalla es definitivamente una buena práctica de programación.
Julien

1
O utilizando el método String.join () de Java 8: String.join ("", StringUtils.splitByCharacterTypeCamelCase ("ExampleTest"));
dk7

¿Cómo no estar de acuerdo con Clint Eastwood? :)
daneejela

19

La solución ordenada y más corta:

StringUtils.capitalize(StringUtils.join(StringUtils.splitByCharacterTypeCamelCase("yourCamelCaseText"), StringUtils.SPACE)); // Your Camel Case Text

Como se muestra en la primera assertpregunta, no se desea la capitalización.
slartidan

Gracias por atrapar el error, actualizará la respuesta.
Sahil Chhabra

10

Si no te gustan las expresiones regulares "complicadas" y no te preocupa en absoluto la eficiencia, entonces he usado este ejemplo para lograr el mismo efecto en tres etapas.

String name = 
    camelName.replaceAll("([A-Z][a-z]+)", " $1") // Words beginning with UC
             .replaceAll("([A-Z][A-Z]+)", " $1") // "Words" of only UC
             .replaceAll("([^A-Za-z ]+)", " $1") // "Words" of non-letters
             .trim();

Pasa todos los casos de prueba anteriores, incluidos los que tienen dígitos.

Como digo, esto no es tan bueno como usar la única expresión regular en algunos otros ejemplos aquí, pero alguien podría encontrarlo útil.


1
Gracias, esto fue genial. Hice una versión de JavaScript .
Sr. Polywhirl

Esta es también la única forma de hacerlo si está trabajando con una biblioteca / herramienta de expresiones regulares que no admite mirar hacia atrás / hacia adelante (como el paquete de expresiones regulares de golang). Buen trabajo.
mdwhatcott

6

Puede usar org.modeshape.common.text.Inflector .

Específicamente:

String humanize(String lowerCaseAndUnderscoredWords,
    String... removableTokens) 

Capitaliza la primera palabra y convierte los guiones bajos en espacios y tiras detrás de "_id" y cualquier token extraíble suministrado.

El artefacto de Maven es: org.modeshape: modeshape-common: 2.3.0.Final

en el repositorio JBoss: https://repository.jboss.org/nexus/content/repositories/releases

Aquí está el archivo JAR: https://repository.jboss.org/nexus/content/repositories/releases/org/modeshape/modeshape-common/2.3.0.Final/modeshape-common-2.3.0.Final.jar


1

La siguiente expresión regular se puede utilizar para identificar las mayúsculas dentro de las palabras:

"((?<=[a-z0-9])[A-Z]|(?<=[a-zA-Z])[0-9]]|(?<=[A-Z])[A-Z](?=[a-z]))"

Coincide con cada letra mayúscula, es decir, después de una letra o dígito que no es mayúscula o seguida de una letra minúscula y cada dígito después de una letra.

Cómo insertar un espacio antes de ellos está más allá de mis habilidades Java =)

Editado para incluir el caso de dígitos y el caso de PDF Loader.


@Yaneeve: Acabo de ver los dígitos ... esto podría complicar las cosas. Probablemente otro Regex para atraparlos sería la manera fácil.
Jens

@Jens: ¿Va a coincidir con el Lde PDFLoader?
Jørn Schou-Rode

¿qué tal (? <= [a-z0-9]) [A-Z0-9]?
Yaneeve

3
Ahora, admiro enormemente tu habilidad Regex, pero odiaría tener que mantener eso.
Chris Knight

1
@ Chris: Sí, eso es cierto. Regex es más un lenguaje de solo escritura. =) Aunque esta expresión en particular no es muy difícil de leer, si lee |como "o". Bueno ... tal vez es ... He visto algo peor = /
Jens

1

Creo que tendrá que iterar sobre la cadena y detectar cambios de minúsculas a mayúsculas, mayúsculas a minúsculas, alfabético a numérico, numérico a alfabético. Sin embargo, en cada cambio que detecte, inserte un espacio con una excepción: en un cambio de mayúsculas a minúsculas, inserte el espacio un carácter antes.


1

Esto funciona en .NET ... optimízalo a tu gusto. Agregué comentarios para que puedas entender lo que está haciendo cada pieza. (RegEx puede ser difícil de entender)

public static string SplitCamelCase(string str)
{
    str = Regex.Replace(str, @"([A-Z])([A-Z][a-z])", "$1 $2");  // Capital followed by capital AND a lowercase.
    str = Regex.Replace(str, @"([a-z])([A-Z])", "$1 $2"); // Lowercase followed by a capital.
    str = Regex.Replace(str, @"(\D)(\d)", "$1 $2"); //Letter followed by a number.
    str = Regex.Replace(str, @"(\d)(\D)", "$1 $2"); // Number followed by letter.
    return str;
}

0

Para el registro, aquí hay una versión de Scala casi (*) compatible:

  object Str { def unapplySeq(s: String): Option[Seq[Char]] = Some(s) }

  def splitCamelCase(str: String) =
    String.valueOf(
      (str + "A" * 2) sliding (3) flatMap {
        case Str(a, b, c) =>
          (a.isUpper, b.isUpper, c.isUpper) match {
            case (true, false, _) => " " + a
            case (false, true, true) => a + " "
            case _ => String.valueOf(a)
          }
      } toArray
    ).trim

Una vez compilado, se puede usar directamente desde Java si el scala-library.jar correspondiente está en el classpath.

(*) falla para la entrada "GL11Version"por la que regresa "G L11 Version".


0

Tomé el Regex de polygenelubricants y lo convertí en un método de extensión en objetos:

    /// <summary>
    /// Turns a given object into a sentence by:
    /// Converting the given object into a <see cref="string"/>.
    /// Adding spaces before each capital letter except for the first letter of the string representation of the given object.
    /// Makes the entire string lower case except for the first word and any acronyms.
    /// </summary>
    /// <param name="original">The object to turn into a proper sentence.</param>
    /// <returns>A string representation of the original object that reads like a real sentence.</returns>
    public static string ToProperSentence(this object original)
    {
        Regex addSpacesAtCapitalLettersRegEx = new Regex(@"(?<=[A-Z])(?=[A-Z][a-z]) | (?<=[^A-Z])(?=[A-Z]) | (?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.IgnorePatternWhitespace);
        string[] words = addSpacesAtCapitalLettersRegEx.Split(original.ToString());
        if (words.Length > 1)
        {
            List<string> wordsList = new List<string> { words[0] };
            wordsList.AddRange(words.Skip(1).Select(word => word.Equals(word.ToUpper()) ? word : word.ToLower()));
            words = wordsList.ToArray();
        }
        return string.Join(" ", words);
    }

Esto convierte todo en una oración legible. Hace un ToString en el objeto pasado. Luego, usa la expresión regular dada por los polieleubricantes para dividir la cadena. Luego ToLowers cada palabra, excepto la primera palabra y cualquier acrónimo. Pensé que podría ser útil para alguien por ahí.


-2

No soy un ninja regex, por lo que iteraría sobre la cadena, manteniendo los índices de la posición actual que se verifica y la posición anterior. Si la posición actual es una letra mayúscula, insertaría un espacio después de la posición anterior e incrementaría cada índice.


2
Psssh! ¿Dónde está la diversión en eso?
vbullinger

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.