2009-06-04 19 views
9

que tienen una cadena de plantilla y una serie de parámetros que provienen de diferentes fuentes, pero necesitan ser emparejados para crear una nueva cadena "rellenado":¿Hay una manera mejor de contar marcadores de posición de formato de cadena en una cadena en C#?

string templateString = GetTemplate(); // e.g. "Mr {0} has a {1}" 
string[] dataItems = GetDataItems();  // e.g. ["Jones", "ceiling cat"} 

string resultingString = String.Format(templateString, dataItems); 
// e.g. "Mr Jones has a ceiling cat" 

Con este código, estoy asumiendo que la cantidad de marcadores de posición de formato de cadena en la plantilla será igual a la cantidad de elementos de datos. En general, es una suposición razonable en mi caso, pero quiero poder producir un resultingString sin fallar incluso si la suposición es incorrecta. No me importa si hay espacios vacíos para los datos faltantes.

Si hay demasiados elementos en dataItems, el método String.Format lo maneja bien. Si no hay suficiente, obtengo una excepción.

Para superar esto, estoy contando el número de marcadores de posición y agregando nuevos elementos a la matriz de elementos de datos si no hay suficientes.

para contar los marcadores de posición, el código que estoy trabajando en este momento es:

private static int CountOccurrences(string haystack) 
{ 
    // Loop through all instances of the string "}". 
    int count = 0; 
    int i = 0; 
    while ((i = text.IndexOf("}", i)) != -1) 
    { 
     i++; 
     count++; 
    } 
    return count; 
} 

Obviamente esto hace la suposición de que no hay llaves de cierre que no están siendo utilizados para el formato marcadores de posición. También solo se siente mal. :)

¿Hay una manera mejor de contar los marcadores de posición de formato de cadena en una cadena?


Varias personas han señalado correctamente que la respuesta que marca como correcto no funcionará en muchas circunstancias. Las razones principales son:

  • expresiones regulares que cuentan el número de marcadores de posición no tiene en cuenta para los frenos literales ({{0}})
  • marcadores de posición de conteo no tiene en cuenta repetidas o saltado marcadores de posición (por ejemplo "{0} has a {1} which also has a {1}")
+0

Mientras no conteste su pregunta, esta publicación puede ofrecerle una alternativa que puede resultarle interesante http://stackoverflow.com/questions/159017/named-string-formatting-in-c – Kane

Respuesta

7

Fusionando las respuestas de Damovisa y Joe. He actualizado la respuesta a los comentarios de Aydsman's nad activa.

int count = Regex.Matches(templateString, @"(?<!\{)\{([0-9]+).*?\}(?!})") //select all placeholders - placeholder ID as separate group 
       .Cast<Match>() // cast MatchCollection to IEnumerable<Match>, so we can use Linq 
       .Max(m => int.Parse(m.Groups[1].Value)) + 1; // select maximum value of first group (it's a placegolder ID) converted to int 

Este enfoque de trabajo para las plantillas como:

"{0} aa {2} bb {1}" => count = 3

"{4} aa {0} bb {0}, {0}"=> count = 5

"{0} {3}, {{7}}"=> count = 4

+0

Para manejar correctamente llaves literalmente, cambie la expresión regular para ignorarlas: @ "(?

+1

Necesario para usar esto hoy, y encontré un problema ... Si no hay marcadores de lugar en un trozo de texto, se cae ... Así que moví el trozo regex.matches a var (var matches = Regex. Coincidencias (inputText, @ "(? () .Max (m => int.Parse (m.Groups [1] .Value)) + 1;) comprobando primero que los resultados tenían más de 0 resultados ... – TiernanO

+0

Error de "{0} {{3} }}, {{7}} "- devuelve 1 en lugar de 4. Si realmente quieres hacer esto, copiaría el código en' StringBuilder.AppendFormat' en lugar de esperar que un Regex (difícil de leer y, sobre todo, difícil de probar) será equivalente. – Joe

7

siempre se puede usar expresiones regulares:

using System.Text.RegularExpressions; 
// ... more code 
string templateString = "{0} {2} .{{99}}. {3}"; 
Match match = Regex.Matches(templateString, 
      @"(?<!\{)\{(?<number>[0-9]+).*?\}(?!\})") 
      .Cast<Match>() 
      .OrderBy(m => m.Groups["number"].Value) 
      .LastOrDefault(); 
Console.WriteLine(match.Groups["number"].Value); // Display 3 
+0

Gracias por eso, lo haré darle una oportunidad. – Damovisa

+0

Como referencia, el código que funcionó fue: int len ​​= new System.Text.RegularExpressions.Regex ("{[0-9] +. *?}"). Coincidencias (plantilla) .Count; – Damovisa

+0

El problema es que los caracteres {y} son especiales en una expresión regular, según la documentación: http://msdn.microsoft.com/en-us/library/3206d374.aspx –

0

se puede utilizar una expresión regular para contar los pares {} que hav e solo el formato que usará entre ellos. @ "\ {\ d + \}" es lo suficientemente bueno, a menos que use opciones de formato.

+0

Gracias, sí, no estoy formateando nada, todo aparece como una cadena. – Damovisa

3

No es en realidad una respuesta a su pregunta, pero es una posible solución a su problema (aunque no perfectamente elegante); puede rellenar su colección dataItems con un número de instancias string.Empty, ya que string.Format no se preocupa por los elementos redundantes.

+0

Cierto, y algo en lo que pensé. Sin embargo, estaría haciendo una suposición sobre el número máximo de marcadores de posición. Eso y si el recuento coincide (lo que generalmente ocurre), es una pérdida de tiempo y espacio ... – Damovisa

+0

La cantidad de desperdicio depende un poco de cómo se crea la matriz 'dataItems' ... si usted es construirlo de una 'nueva' ya entonces la pérdida de tiempo será realmente insignificante, y el desperdicio de espacio está limitado por el hecho de que utilizas una referencia a 'string.Empty', que es una instancia única sin importar la frecuencia con la que referirse a eso; siempre y cuando la matriz no permanezca por mucho tiempo, el alcance del desperdicio espacial es realmente bastante mínimo ... todo esto obviamente depende fuertemente de cómo y con qué frecuencia se crean estas matrices. – jerryjvl

16

Contando los marcadores de posición no ayuda - considerar los siguientes casos:

"{0} ... {1} ... {0}" - necesita 2 valores

"{1} {3} "- necesita 4 valores de los cuales dos se ignoran

El segundo ejemplo no es descabellado.

Por ejemplo, es posible que tenga algo como esto en los Estados Unidos Inglés:

String.Format("{0} {1} {2} has a {3}", firstName, middleName, lastName, animal); 

En algunas culturas, el segundo nombre no puede ser utilizado y puede que tenga:

String.Format("{0} {2} ... {3}", firstName, middleName, lastName, animal); 

Si desea para hacer esto, debe buscar los especificadores de formato {index [, length] [: formatString]} con el índice máximo, ignorando las llaves repetidas (por ejemplo, {{n}}). Las llaves repetidas se usan para insertar llaves como literales en la cuerda de salida. Dejaré la codificación como un ejercicio :) - pero no creo que se pueda o deba hacer con Regex en el caso más general (es decir, con length y/o formatString).

E incluso si no está usando length o formatString hoy, un futuro desarrollador puede pensar que es un cambio inofensivo agregar uno; sería una pena que esto rompa su código.

Me gustaría intentar imitar el código en StringBuilder.AppendFormat (que se llama por String.Format) aunque sea un poco feo: use Lutz Reflector para obtener este código. Básicamente itere a través de la cadena buscando especificadores de formato, y obtenga el valor del índice para cada especificador.

+0

Excelente elaboración. +1 – peSHIr

+0

Sí, buen punto. Enseña que definitivamente debería esperar un poco más antes de marcar una respuesta correcta. – Damovisa

1

Dado que no tengo la autoridad para editar publicaciones, propondré mi versión más corta (y correcta) de la respuesta de Marqus:

int num = Regex.Matches(templateString,@"(?<!\{)\{([0-9]+).*?\}(?!})") 
      .Cast<Match>() 
      .Max(m => int.Parse(m.Groups[0].Value)) + 1; 

Estoy usando la expresión regular propuesta por Aydsman, pero no la he probado.

2

¿Quizás estás tratando de romper una nuez con un mazo?

¿Por qué no acaba de poner try/catch alrededor de su llamada a String.Format.

Es un poco feo, pero resuelve su problema de una manera que requiere un mínimo esfuerzo, pruebas mínimas, y está garantizado que funciona incluso si hay algo más sobre cadenas de formato que no consideró (como {{literales, o cadenas de formato más complejo, con caracteres no numéricos dentro de ellos: {0: $ #, ## 0.00; ($ #, ## 0.00);} cero)

(Y sí, esto significa que no se detectará más elementos de datos que los especificadores de formato, pero ¿es esto un problema? ¿Presumiblemente el usuario de su software notará que truncaron su salida y rectificó su cadena de formato?)

+0

Sugerencia válida, pero necesito que acepte la entrada y genere un mensaje independientemente de qué "equivocado" es. – Damovisa

+0

La captura de excepciones es una operación costosa. –

+0

@NineTails: solo si son lanzados. –

3

La respuesta de Marqus falla si no hay marcadores de posición en la plantilla cuerda.

La adición de los .DefaultIfEmpty() y m==null resuelve condicionales esta cuestión.

Regex.Matches(templateString, @"(?<!\{)\{([0-9]+).*?\}(?!})") 
    .Cast<Match>() 
    .DefaultIfEmpty() 
    .Max(m => m==null?-1:int.Parse(m.Groups[1].Value)) + 1; 
+0

Lo siento, no pude hacer que funcione su versión, mi prueba de unidad con una cadena de plantilla sin marcador de posición falla con su código linq, aunque la idea se ve bien (estoy aprendiendo). –

3

Hay un problema con la expresión regular propuesto anteriormente en la que coincidirá con "{0}}":.

Regex.Matches(templateString, @"(?<!\{)\{([0-9]+).*?\}(?!})") 
... 

El problema es cuando se busca el cierre} se utiliza * cuales permite una} inicial como una coincidencia. Así que cambiar eso para detenerse en el primero} hace que esa comprobación de sufijo funcione. En otras palabras, utilizar esto como la expresión regular:

Regex.Matches(templateString, @"(?<!\{)\{([0-9]+)[^\}]*?\}(?!\})") 
... 

Hice un par de funciones estáticas en base a todo esto, tal vez te sean de utilidad.

public static class StringFormat 
{ 
    static readonly Regex FormatSpecifierRegex = new Regex(@"(?<!\{)\{([0-9]+)[^\}]*?\}(?!\})", RegexOptions.Compiled); 

    public static IEnumerable<int> EnumerateArgIndexes(string formatString) 
    { 
     return FormatSpecifierRegex.Matches(formatString) 
     .Cast<Match>() 
     .Select(m => int.Parse(m.Groups[1].Value)); 
    } 

    /// <summary> 
    /// Finds all the String.Format data specifiers ({0}, {1}, etc.), and returns the 
    /// highest index plus one (since they are 0-based). This lets you know how many data 
    /// arguments you need to provide to String.Format in an IEnumerable without getting an 
    /// exception - handy if you want to adjust the data at runtime. 
    /// </summary> 
    /// <param name="formatString"></param> 
    /// <returns></returns> 
    public static int GetMinimumArgCount(string formatString) 
    { 
     return EnumerateArgIndexes(formatString).DefaultIfEmpty(-1).Max() + 1; 
    } 

} 
+0

Este también falla en "{0} {3}, {{{7}}}" => devuelve 4 en lugar de 8. – Joe

1

Muy tarde a la pregunta, pero sucedió sobre esto desde otra tangente.

String.Format es problemático incluso con Unit Testing (es decir, que falta un argumento). Un desarrollador coloca el marcador de posición posicional incorrecto o la cadena formateada se edita y compila bien, pero se usa en otra ubicación de código o incluso en otro ensamblaje y se obtiene la FormatException en tiempo de ejecución. Lo ideal es que las pruebas unitarias o las pruebas de integración capten esto.

Si bien esta no es una solución a la respuesta, es una solución. Puede hacer que sea un método auxiliar que acepta la cadena formateada y una lista (o matriz) de objetos. Dentro del método helper, rellene la lista con una longitud fija predefinida que excedería la cantidad de marcadores de posición en sus mensajes. Por ejemplo, a continuación, supongamos que 10 marcadores de posición son suficientes. El elemento de relleno puede ser nulo o una cadena como "[Missing]".

int q = 123456, r = 76543; 
List<object> args = new List<object>() { q, r};  

string msg = "Sample Message q = {2:0,0} r = {1:0,0}"; 

//Logic inside the helper function 
int upperBound = args.Count; 
int max = 10; 

for (int x = upperBound; x < max; x++) 
{ 
    args.Add(null); //"[No Value]" 
} 
//Return formatted string 
Console.WriteLine(string.Format(msg, args.ToArray())); 

¿Es esto ideal? No, pero para el registro o algunos casos de uso, es una alternativa aceptable para evitar la excepción de tiempo de ejecución. Incluso podría reemplazar el elemento nulo con "[Sin valor]" y/o agregar posiciones de matriz, luego probar Sin valor en la cadena formateada y luego registrarlo como un problema.

0

Basado en this answer y la respuesta de David White Aquí se presenta una versión actualizada:

string formatString = "Hello {0:C} Bye {{300}} {0,2} {34}"; 
//string formatString = "Hello"; 
//string formatString = null; 

int n; 
var countOfParams = Regex.Matches(formatString?.Replace("{{", "").Replace("}}", "") ?? "", @"\{([0-9]+)") 
    .OfType<Match>() 
    .DefaultIfEmpty() 
    .Max(m => Int32.TryParse(m?.Groups[1]?.Value, out n) ? n : -1) 
    + 1; 

Console.Write(countOfParams); 

A tener en cuenta:

  1. Sustitución de una manera más fácil de cuidar de los apoyos dobles rizadas. Esto es similar a cómo StringBuilder.AppendFormatHelper se ocupa de ellos internamente.
  2. A medida que se están eliminando '{{' y '}}', expresión regular se puede simplificar a '{([0-9] +)'
  3. Esto funciona incluso si formatString es nulo
  4. Esto funcionará incluso si hay un formato no válido, diga '{3444444456}'. Normalmente, esto provocará un desbordamiento de enteros.
Cuestiones relacionadas