2010-01-25 21 views
6

Tengo una cadena con oraciones múltiples. ¿Cómo capitalizo la primera letra de la primera palabra en cada oración? Algo así como el formato de párrafo en word.Formatear oraciones en una cadena usando C#

por ejemplo, "este es un código. El código está en C#." La salida debe ser "Este es un código. El código está en C#".

una forma sería dividir la cadena en función de '.' y luego escribe en mayúscula la primera letra y luego vuelve a unirte.

¿Existe una solución mejor?

+1

Yo diría: conviértanse en una matriz de caracteres, recorran usando un ciclo while, capitalicen cuando sea apropiado, guarden de nuevo en una cadena. Algunas líneas de código, pero debe ser rápido. –

+0

@Hamish: Esa no es una mala respuesta; ciertamente es mucho mejor que manipular una Cadena repetidamente. Sin embargo, creo que StringBuffer sería más simple. –

+1

No se olvide de '?', '!', ':' Y tal vez '\ n' (si omiten los signos de puntuación) – Andres

Respuesta

4

En mi opinión, cuando se trata de potencialmente compleja cadena coincidente basado en normas y reemplazar - no se puede conseguir mucho mejor que una solución basada en expresiones regulares (a pesar de que son tan difíciles de leer!). Esto ofrece el mejor rendimiento y la mejor eficiencia de memoria, en mi opinión, te sorprenderá lo rápido que será.

Yo usaría el Regex.Replace overload that accepts an input string, regex pattern and a MatchEvaluator delegate. Un MatchEvaluator es una función que acepta un objeto Match como entrada y devuelve un reemplazo de cadena.

Aquí está el código:

public static string Capitalise(string input) 
{ 
    //now the first character 
    return Regex.Replace(input, @"(?<=(^|[.;:])\s*)[a-z]", 
    (match) => { return match.Value.ToUpper(); }); 
} 

La expresión regular utiliza el constructo (búsqueda hacia atrás por cero positivo de ancho) para restringir las capturas sólo para caracteres az precedido por el inicio de la cadena, o la puntuacion (< =?) marcas que quieres En el bit [.;:], puede agregar los adicionales que desee (por ejemplo, [.;:?."] para agregar "y" caracteres.

Esto significa, también, que su MatchEvaluator no tiene que hacer ninguna unión innecesaria de cadenas (que desea evitar por motivos de rendimiento).

Todas las demás cosas mencionadas por uno de los otros que responden sobre el uso de RegexOptions.Compiled también son relevantes desde el punto de vista del rendimiento. Sin embargo, el método Regex.Replace estático ofrece beneficios de rendimiento muy similares (solo hay una búsqueda adicional en el diccionario).

Como digo, me sorprendería si alguna de las otras soluciones no regex aquí funcionara mejor y fuera tan rápida.

EDITAR

poner esta solución en contra de Ahmad como él con toda razón señaló que una mirada alrededor podría ser menos eficiente que hacerlo a su manera.

Aquí está el crudo de referencia que hice:

public string LowerCaseLipsum 
{ 
    get 
    { 
    //went to lipsum.com and generated 10 paragraphs of lipsum 
    //which I then initialised into the backing field with @"[lipsumtext]".ToLower() 
    return _lowerCaseLipsum; 
    } 
} 
[TestMethod] 
public void CapitaliseAhmadsWay() 
{ 
    List<string> results = new List<string>(); 
    DateTime start = DateTime.Now; 
    Regex r = new Regex(@"(^|\p{P}\s+)(\w+)", RegexOptions.Compiled); 
    for (int f = 0; f < 1000; f++) 
    { 
    results.Add(r.Replace(LowerCaseLipsum, m => m.Groups[1].Value 
         + m.Groups[2].Value.Substring(0, 1).ToUpper() 
          + m.Groups[2].Value.Substring(1))); 
    } 
    TimeSpan duration = DateTime.Now - start; 
    Console.WriteLine("Operation took {0} seconds", duration.TotalSeconds); 
} 

[TestMethod] 
public void CapitaliseLookAroundWay() 
{ 
    List<string> results = new List<string>(); 
    DateTime start = DateTime.Now; 
    Regex r = new Regex(@"(?<=(^|[.;:])\s*)[a-z]", RegexOptions.Compiled); 
    for (int f = 0; f < 1000; f++) 
    { 
    results.Add(r.Replace(LowerCaseLipsum, m => m.Value.ToUpper())); 
    } 
    TimeSpan duration = DateTime.Now - start; 
    Console.WriteLine("Operation took {0} seconds", duration.TotalSeconds); 
} 

En una versión de lanzamiento, el mi solución era aproximadamente un 12% más rápido que el de Ahmad (1,48 segundos en lugar de 1,68 segundos).

Curiosamente, sin embargo, si se hizo a través del método Regex.Replace estático, ambos fueron aproximadamente un 80% más lentos, y mi solución fue más lenta que la de Ahmad.

+0

Mi sospecha aquí es que, incluso con una expresión regular precompilada, no va a ser tan rápido como StringBuilder. –

+0

Regex usa Stringbuilder internamente de todos modos, pero supongo que la única forma de averiguarlo es comparar las diferentes soluciones. Hasta que hagamos eso, cualquier otra cosa es pura conjetura :) –

+0

Andras: Gracias por la respuesta. No funcionará en caso de que tengamos signos de puntuación como '?'. Supongo que la respuesta de Ahmad a continuación se acerca. Todavía estoy por evaluarlo completamente. – AlwaysAProgrammer

1
  1. Haga su trabajo en un StringBuffer.
  2. En minúsculas todo el asunto.
  3. Loops en mayúscula y mayúscula en mayúscula.
  4. Llamar a String.
+3

Esto puede tener la consecuencia involuntaria de la inserción inferior de otro texto que debería permanecer en mayúsculas, como nombres, por ejemplo. – LBushkin

+0

@LBushkin: A continuación, omita el paso 2 si está seguro de que está bien hacerlo. –

+0

Dado que no hay muchos lugares para capitalizar, uno probablemente podría usar un StringBuilder efectivamente, pero el diablo está en los detalles. Cuidado de pegar en el código? –

4

Usted tiene algunas opciones diferentes:

  1. Su enfoque de la división de la cadena, la capitalización y luego volver a unirse a
  2. el uso de expresiones regulares para realizar una sustitución de las expresiones (que puede ser una poco complicado para el caso)
  3. Escriba un iterador C# que itere sobre cada carácter y arroje un nuevo IEnumerable<char> con la primera letra después de un período en mayúscula. Puede ofrecer el beneficio de una solución de transmisión.
  4. Coloque un bucle sobre cada char y en mayúsculas los que aparecen inmediatamente después de un punto (se ignora el espacio en blanco): un StringBuffer puede hacer esto más fácil.

El código siguiente utiliza un iterador:

public static string ToSentenceCase(string someString) 
{ 
    var sb = new StringBuilder(someString.Length); 
    bool wasPeriodLastSeen = true; // We want first letter to be capitalized 
    foreach(var c in someString) 
    { 
     if(wasPeriodLastSeen && !c.IsWhiteSpace) 
     { 
      sb.Append(c.ToUpper()); 
      wasPeriodLastSeen = false;   
     }   
     else 
     { 
      if(c == '.') // you may want to expand this to other punctuation 
       wasPeriodLastSeen = true; 
      sb.Append(c); 
     } 
    } 

    return sb.ToString(); 
} 
+0

¿El rendimiento es una consideración? –

+2

LBushkin: ToTitleCase escribirá en mayúscula la primera letra de cada palabra de la cadena. En mi caso, la salida será "Esto es un código. El código está en C#". – AlwaysAProgrammer

+0

Steven: El rendimiento es un problema porque el método se llama en un bucle. – AlwaysAProgrammer

2

No sé por qué, pero decidió darle rendimiento de retorno una oportunidad, sobre la base de lo que se había sugerido LBushkin. Solo por diversión.

static IEnumerable<char> CapitalLetters(string sentence) 
     { 
      //capitalize first letter 
      bool capitalize = true; 
      char lastLetter; 
      for (int i = 0; i < sentence.Length; i++) 
      { 
       lastLetter = sentence[i]; 
       yield return (capitalize) ? Char.ToUpper(sentence[i]) : sentence[i]; 


       if (Char.IsWhiteSpace(lastLetter) && capitalize == true) 
        continue; 

       capitalize = false; 
       if (lastLetter == '.' || lastLetter == '!') //etc 
        capitalize = true; 
      } 
     } 

Para usarlo:

string sentence = new String(CapitalLetters("this is some code. the code is in C#.").ToArray()); 
5

Aquí hay una solución de expresiones regulares que usa la categoría de puntuación para evitar tener que especificar.!? "Etc. aunque debe verificar si cubre sus necesidades o establecerlas explícitamente. Lea en la categoría" P "debajo de" apoyado sección Unicode general Categorías" situada en la MSDN Character Classes page

string input = @"this is some code. the code is in C#? it's great! In ""quotes."" after quotes."; 
string pattern = @"(^|\p{P}\s+)(\w+)"; 

// compiled for performance (might want to benchmark it for your loop) 
Regex rx = new Regex(pattern, RegexOptions.Compiled); 

string result = rx.Replace(input, m => m.Groups[1].Value 
           + m.Groups[2].Value.Substring(0, 1).ToUpper() 
           + m.Groups[2].Value.Substring(1)); 

Si decide no utilizar la clase \p{P} que tendría que especificar los caracteres a sí mismo, similar a:.

string pattern = @"(^|[.?!""]\s+)(\w+)"; 

EDIT: a continuación es un ejemplo actualizado para demostrar 3 patrones. El primero muestra cómo todas las puntuaciones afectan a la carcasa. El segundo muestra cómo seleccionar y elegir ciertas categorías de puntuación mediante el uso de la resta de clase. Utiliza todas las puntuaciones mientras elimina los grupos de puntuación específicos. El tercero es similar al segundo pero usando diferentes grupos.

El enlace de MSDN no explican lo que algunas de las categorías de puntuación se refieren a, así que aquí tiene un desglose:

  • P: todos los signos de puntuación (comprende todas las categorías de abajo)
  • pc: subrayar _
  • Pd: tablero -
  • Sal: abrir paréntesis, corchetes y llaves ([{
  • Pe: paréntesis de cierre, corchetes y llaves )]}
  • Pi: citas individuales/dobles iniciales (MSDN dice que "se puede comportar como ps/Pe en función del uso ")
  • Pf: se aplica finales individuales comillas dobles/(nota MSDN Pi)
  • Po: otra puntuacion tales como comas, dos puntos, punto y coma y rayas verticales ,, :, ;, \, /

comparar cuidadosamente cómo los resultados se ven afectados por estos grupos. Esto debería otorgarle un gran grado de flexibilidad. Si esto no parece deseable, entonces puede usar caracteres específicos en una clase de caracteres como se muestra anteriormente.

string input = @"foo (parens) bar { braces } foo [ brackets ] bar. single ' quote & "" double "" quote. 
dash - test. Connector _ test. Comma, test. Semicolon; test. Colon: test. Slash/test. Slash \ test."; 

string[] patterns = { 
    @"(^|\p{P}\s+)(\w+)", // all punctuation chars 
    @"(^|[\p{P}-[\p{Pc}\p{Pd}\p{Ps}\p{Pe}]]\s+)(\w+)", // all punctuation chars except Pc/Pd/Ps/Pe 
    @"(^|[\p{P}-[\p{Po}]]\s+)(\w+)" // all punctuation chars except Po 
}; 

// compiled for performance (might want to benchmark it for your loop) 
foreach (string pattern in patterns) 
{ 
    Console.WriteLine("*** Current pattern: {0}", pattern); 
    string result = Regex.Replace(input, pattern, 
          m => m.Groups[1].Value 
           + m.Groups[2].Value.Substring(0, 1).ToUpper() 
           + m.Groups[2].Value.Substring(1)); 
    Console.WriteLine(result); 
    Console.WriteLine(); 
} 

en cuenta que "Dash" no se escribe con mayúscula usando el último patrón y está en una nueva línea. Una forma de hacerlo capitalizado es usar la opción RegexOptions.Multiline. Pruebe el fragmento de arriba con eso para ver si cumple con su resultado deseado.

Además, por el bien del ejemplo, no usé RegexOptions.Compiled en el ciclo anterior. Para usar ambas opciones O juntas: RegexOptions.Compiled | RegexOptions.Multiline.

+0

+1 - Buena coincidencia con la clase de caracteres de puntuación, pero tener todas esas cadenas y subcadenas en el MatchEvaluator no está sacando lo mejor de StringBuilder que la operación Regex.Replace va a ser utilizada. Mi solución utiliza capturas de ancho cero para los bits que identifican el 'primer' carácter, lo que significa que OP simplemente devuelve match.Value.ToUpper(). –

+0

Ahmad, como sugirió, ha agregado un punto de referencia crudo, pero justo, creo, de mi respuesta. Mi uno es más rápido cuando ambas expresiones regulares se compilan en instancias de Regex con RegexOptions.Compiled. Tu uno es más rápido cuando usas el método Regex.Replace static, pero el rendimiento en ambos cuando lo haces de esa manera también es bastante pesado (¡nunca volveré a usar el método estático!) :) –

+0

@Andras ¡gracias por el seguimiento! –

Cuestiones relacionadas