Formato de cadena con nombre en C #

156

¿Hay alguna forma de formatear una cadena por nombre en lugar de posición en C #?

En python, puedo hacer algo como este ejemplo (descaradamente robado de aquí ):

>>> print '%(language)s has %(#)03d quote types.' % \
      {'language': "Python", "#": 2}
Python has 002 quote types.

¿Hay alguna forma de hacer esto en C #? Digamos por ejemplo:

String.Format("{some_variable}: {some_other_variable}", ...);

Sería capaz de hacer esto usando un nombre de variable sería bueno, pero un diccionario también es aceptable.

Jason Baker
fuente
También me estoy perdiendo esto de Ruby.
JesperE
Creo que su ejemplo es demasiado simplista y está llevando a las personas a darle respuestas inútiles. Quizás usar una variable más de una vez en la cadena sería más demostrativo.
Wedge
En realidad, la confusión ESPECÍFICA es el uso de String.Format. Eso se presta a respuestas como la mía, que no son útiles porque no están orientadas a variables, pero son precisas en lo que respecta a String.Format.
John Rudy
1
La llamada a String.Format es obviamente un ejemplo artificial. A menos que, por supuesto, no supieras que llamar a String.Format con puntos suspensivos no es posible. El problema fue que no dije que quería que el formateo se realizara con parámetros con nombre en lugar de posición, que se ha corregido.
Jason Baker, el
FYI: enviado a la voz de usuario de MS Connect para solicitar que se convierta en una característica estándar del marco. Para todos los interesados, por favor upvote: visualstudio.uservoice.com/forums/121579-visual-studio/...
JohnLBevan

Respuestas:

130

No hay un método incorporado para manejar esto.

Aquí hay un método

string myString = "{foo} is {bar} and {yadi} is {yada}".Inject(o);

Aquí está otro

Status.Text = "{UserName} last logged in at {LastLoginDate}".FormatWith(user);

Un tercer método mejorado parcialmente basado en los dos anteriores , de Phil Haack

John Sheehan
fuente
11
He sido muy feliz con FormatWith (), pero quería señalar un problema que encontré recientemente. La implementación se basa en el DataBinder de System.Web.UI, que no es compatible con SQL CLR. Inject (o) no se basa en el enlazador de datos, lo que lo hizo útil para el reemplazo de múltiples tokens en mi objeto SQL CLR.
EBarr
1
Tal vez pueda actualizar la primera oración de su respuesta. La interpolación de cadenas está presente en C # y VB durante algunos meses (finalmente ...). Su respuesta está en la parte superior, por lo que podría ser útil para los lectores si puede vincularlos a algunos recursos .NET actualizados.
miroxlav
1
@miroxlav no es realmente lo mismo. No puede pasar cadenas interpoladas: stackoverflow.com/q/31987232/213725
DixonD
@DixonD: definitivamente tienes razón, pero no era su propósito. En las preguntas y respuestas que vinculó, OP intenta hacer referencia al nombre de la variable antes de que exista. No es una muy buena idea, pero si alguien insiste en eso, puede construir un analizador especializado. Pero no me equivocaría con el concepto general de interpolación de cadenas.
miroxlav
44

Tengo una implementación que acabo de publicar en mi blog aquí: http://haacked.com/archive/2009/01/04/fun-with-named-formats-string-parsing-and-edge-cases.aspx

Aborda algunos problemas que tienen estas otras implementaciones con el escape de llaves. La publicación tiene detalles. También hace lo DataBinder.Eval, pero sigue siendo muy rápido.

Hackeado
fuente
3
El código disponible para descargar en ese artículo 404's. Realmente me gustaría verlo también.
quentin-starin
2
@qes: se publicó un enlace actualizado en los comentarios: code.haacked.com/util/NamedStringFormatSolution.zip
Der Hochstapler
3
@OliverSalzburg: He estado usando SmartFormat para todas mis necesidades de formato desde hace algún tiempo, me encanta. github.com/scottrippey/SmartFormat
quentin-starin
@qes: ¿Te importaría escribir y responder al respecto y mostrar cómo funciona? Parece interesante
Der Hochstapler
@qes: Definitivamente deberías agregar SmartFormat como respuesta, ya que es muy agradable y está activamente respaldado (2015).
Răzvan Flavius ​​Panda
42

Se agregaron cadenas interpoladas en C # 6.0 y Visual Basic 14

Ambos se presentaron a través del nuevo compilador de Roslyn en Visual Studio 2015 .

  • C # 6.0:

    return "\{someVariable} and also \{someOtherVariable}" O
    return $"{someVariable} and also {someOtherVariable}"

  • VB 14:

    return $"{someVariable} and also {someOtherVariable}"

Características notables (en Visual Studio 2015 IDE):

  • se admite el color de sintaxis : las variables contenidas en las cadenas se resaltan
  • se admite la refactorización : al cambiar el nombre, las variables contenidas en las cadenas también se renombran
  • en realidad no solo se admiten nombres de variables, sino que también se admiten expresiones ; por ejemplo, no solo {index}funciona, sino que también{(index + 1).ToString().Trim()}

¡Disfrutar! (y haz clic en "Enviar una sonrisa" en el VS)

miroxlav
fuente
2
La pregunta está etiquetada con .net 3.5, por lo tanto, su información es válida, pero no es una alternativa
Douglas Gandini
1
@miroxlav: tienes razón sobre la versión de marco. La interpolación de cadenas solo depende del nuevo compilador de Roslyn utilizado en VS 2015.
Douglas Gandini
2
Esto tampoco funcionará a menos que su cadena de formato se coloque en el código mismo. es decir, no funcionará si su cadena de formato proviene de una fuente externa, como un archivo de configuración o una base de datos.
Craig Brett
40

También puede usar tipos anónimos como este:

    public string Format(string input, object p)
    {
        foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(p))
            input = input.Replace("{" + prop.Name + "}", (prop.GetValue(p) ?? "(null)").ToString());

        return input;
    }

Por supuesto, requeriría más código si también desea analizar el formato, pero puede formatear una cadena usando esta función como:

Format("test {first} and {another}", new { first = "something", another = "something else" })
Doggett
fuente
1
Perfecto para aquellos de nosotros que todavía tenemos 2.0. Sí, lo sé ... Esta solución es sencilla y fácil de entender. ¡¡¡Y FUNCIONA!!!
Brad Bruce el
14

No parece haber una manera de hacer esto fuera de la caja. Sin embargo, parece factible implementar uno propio IFormatProviderque se vincule a IDictionaryvalores for.

var Stuff = new Dictionary<string, object> {
   { "language", "Python" },
   { "#", 2 }
};
var Formatter = new DictionaryFormatProvider();

// Interpret {0:x} where {0}=IDictionary and "x" is hash key
Console.WriteLine string.Format(Formatter, "{0:language} has {0:#} quote types", Stuff);

Salidas:

Python tiene 2 tipos de cotización

La advertencia es que no se puede mezclar FormatProviders, por lo que el formato de texto elegante no se puede usar al mismo tiempo.

Spoulson
fuente
1
+1 para describir, en mi humilde opinión, el mejor método conceptual, que tiene una buena implementación en mo.notono.us/2008/07/c-stringinject-format-strings-by-key.html - las otras publicaciones incluyen esto pero también propongo los métodos basados ​​en la reflexión que, en mi humilde opinión, son bastante malvados
Adam Ralph
9

El marco en sí no proporciona una manera de hacer esto, pero puedes echar un vistazo a esta publicación de Scott Hanselman. Ejemplo de uso:

Person p = new Person();  
string foo = p.ToString("{Money:C} {LastName}, {ScottName} {BirthDate}");  
Assert.AreEqual("$3.43 Hanselman, {ScottName} 1/22/1974 12:00:00 AM", foo); 

Este código de James Newton-King es similar y funciona con subpropiedades e índices,

string foo = "Top result for {Name} was {Results[0].Name}".FormatWith(student));

El código de James se basa en System.Web.UI.DataBinder para analizar la cadena y requiere hacer referencia a System.Web, que a algunas personas no les gusta hacer en aplicaciones que no son web.

EDITAR: Ah, y funcionan muy bien con tipos anónimos, si no tiene un objeto con propiedades listas para ello:

string name = ...;
DateTime date = ...;
string foo = "{Name} - {Birthday}".FormatWith(new { Name = name, Birthday = date });
Lucas
fuente
6

Ver /programming/271398?page=2#358259

Con la extensión vinculada a puede escribir esto:

var str = "{foo} {bar} {baz}".Format(foo=>"foo", bar=>2, baz=>new object());

y obtendrás "foo 2 System.Object".

Mark Cidade
fuente
4

Creo que lo más cercano que obtendrás es un formato indexado:

String.Format("{0} has {1} quote types.", "C#", "1");

También hay String.Replace (), si está dispuesto a hacerlo en varios pasos y tiene fe en que no encontrará sus 'variables' en ningún otro lugar de la cadena:

string MyString = "{language} has {n} quote types.";
MyString = MyString.Replace("{language}", "C#").Replace("{n}", "1");

Expandiendo esto para usar una Lista:

List<KeyValuePair<string, string>> replacements = GetFormatDictionary();  
foreach (KeyValuePair<string, string> item in replacements)
{
    MyString = MyString.Replace(item.Key, item.Value);
}

También puede hacer eso con un Dictionary <string, string> iterando sus colecciones .Keys, pero usando List <KeyValuePair <string, string >> podemos aprovechar el método .ForEach () de List y condensarlo nuevamente en una línea:

replacements.ForEach(delegate(KeyValuePair<string,string>) item) { MyString = MyString.Replace(item.Key, item.Value);});

Una lambda sería aún más simple, pero todavía estoy en .Net 2.0. También tenga en cuenta que el rendimiento .Replace () no es estelar cuando se usa de forma iterativa, ya que las cadenas en .Net son inmutables. Además, esto requiere que la MyStringvariable se defina de tal manera que sea accesible para el delegado, por lo que aún no es perfecta.

Joel Coehoorn
fuente
Bueno, esa no es la solución más bonita, pero es con lo que voy por ahora. Lo único que hice de manera diferente fue usar un StringBuilder en lugar de una cadena para no seguir creando nuevas cadenas.
Jason Baker, el
3

Mi biblioteca de código abierto, Regextra , admite el formato con nombre (entre otras cosas). Actualmente apunta a .NET 4.0+ y está disponible en NuGet . También tengo una entrada de blog introductoria al respecto: Regextra: que te ayuda a reducir tus (problemas) {2} .

El bit de formato con nombre admite:

  • Formato básico
  • Formato de propiedades anidadas
  • Formato de diccionario
  • Escape de delimitadores
  • Formato de cadena estándar / personalizado / IFormatProvider

Ejemplo:

var order = new
{
    Description = "Widget",
    OrderDate = DateTime.Now,
    Details = new
    {
        UnitPrice = 1500
    }
};

string template = "We just shipped your order of '{Description}', placed on {OrderDate:d}. Your {{credit}} card will be billed {Details.UnitPrice:C}.";

string result = Template.Format(template, order);
// or use the extension: template.FormatTemplate(order);

Resultado:

Acabamos de enviar su pedido de 'Widget', realizado el 28/02/2014. Su tarjeta de {crédito} se facturará a $ 1,500.00.

Consulte el enlace de GitHub del proyecto (arriba) y la wiki para ver otros ejemplos.

Ahmad Mageed
fuente
Wow, esto se ve increíble, particularmente cuando se trata con algunos de los ejemplos de formatos más difíciles que uno encuentra.
Nicholas Petersen
2

Revisa este:

public static string StringFormat(string format, object source)
{
    var matches = Regex.Matches(format, @"\{(.+?)\}");
    List<string> keys = (from Match matche in matches select matche.Groups[1].Value).ToList();

    return keys.Aggregate(
        format,
        (current, key) =>
        {
            int colonIndex = key.IndexOf(':');
            return current.Replace(
                "{" + key + "}",
                colonIndex > 0
                    ? DataBinder.Eval(source, key.Substring(0, colonIndex), "{0:" + key.Substring(colonIndex + 1) + "}")
                    : DataBinder.Eval(source, key).ToString());
        });
}

Muestra:

string format = "{foo} is a {bar} is a {baz} is a {qux:#.#} is a really big {fizzle}";
var o = new { foo = 123, bar = true, baz = "this is a test", qux = 123.45, fizzle = DateTime.Now };
Console.WriteLine(StringFormat(format, o));

El rendimiento es bastante bueno en comparación con otras soluciones.

Pavlo Neiman
fuente
1

Dudo que esto sea posible. Lo primero que viene a la mente es cómo vas a acceder a los nombres de las variables locales.

Sin embargo, puede haber alguna forma inteligente de usar expresiones LINQ y Lambda para hacer esto.

leppie
fuente
@leppie: +1 si puedes darme algo de LINQ + Lambda para hacer eso; D (ok +1 por tener una respuesta relevante)
user7116
¡Me encantaría verlo también! ¡Quizás tome ese desafío!
Leppie
Pensé que sería imposible hacerlo con nombres de variables, pero puse eso allí en caso de que estuviera equivocado. :) ¿Tampoco hay forma de hacer esto con un diccionario?
Jason Baker
Lo intenté y conseguí un poco en alguna parte, pero lo consideré demasiado feo y difícil de usar. Se habría visto así: cadena s = formato (f => f ("{hola} {mundo}", hola, mundo));
leppie
1

Aquí hay uno que hice hace un tiempo. Extiende String con un método Format tomando un solo argumento. Lo bueno es que usará la cadena estándar. Formatee si proporciona un argumento simple como un int, pero si usa algo como tipo anónimo, también funcionará.

Ejemplo de uso:

"The {Name} family has {Children} children".Format(new { Children = 4, Name = "Smith" })

Resultaría en "La familia Smith tiene 4 hijos".

No hace cosas locas vinculantes como matrices e indexadores. Pero es súper simple y de alto rendimiento.

    public static class AdvancedFormatString
{

    /// <summary>
    /// An advanced version of string.Format.  If you pass a primitive object (string, int, etc), it acts like the regular string.Format.  If you pass an anonmymous type, you can name the paramters by property name.
    /// </summary>
    /// <param name="formatString"></param>
    /// <param name="arg"></param>
    /// <returns></returns>
    /// <example>
    /// "The {Name} family has {Children} children".Format(new { Children = 4, Name = "Smith" })
    /// 
    /// results in 
    /// "This Smith family has 4 children
    /// </example>
    public static string Format(this string formatString, object arg, IFormatProvider format = null)
    {
        if (arg == null)
            return formatString;

        var type = arg.GetType();
        if (Type.GetTypeCode(type) != TypeCode.Object || type.IsPrimitive)
            return string.Format(format, formatString, arg);

        var properties = TypeDescriptor.GetProperties(arg);
        return formatString.Format((property) =>
            {
                var value = properties[property].GetValue(arg);
                return Convert.ToString(value, format);
            });
    }


    public static string Format(this string formatString, Func<string, string> formatFragmentHandler)
    {
        if (string.IsNullOrEmpty(formatString))
            return formatString;
        Fragment[] fragments = GetParsedFragments(formatString);
        if (fragments == null || fragments.Length == 0)
            return formatString;

        return string.Join(string.Empty, fragments.Select(fragment =>
            {
                if (fragment.Type == FragmentType.Literal)
                    return fragment.Value;
                else
                    return formatFragmentHandler(fragment.Value);
            }).ToArray());
    }


    private static Fragment[] GetParsedFragments(string formatString)
    {
        Fragment[] fragments;
        if ( parsedStrings.TryGetValue(formatString, out fragments) )
        {
            return fragments;
        }
        lock (parsedStringsLock)
        {
            if ( !parsedStrings.TryGetValue(formatString, out fragments) )
            {
                fragments = Parse(formatString);
                parsedStrings.Add(formatString, fragments);
            }
        }
        return fragments;
    }

    private static Object parsedStringsLock = new Object();
    private static Dictionary<string,Fragment[]> parsedStrings = new Dictionary<string,Fragment[]>(StringComparer.Ordinal);

    const char OpeningDelimiter = '{';
    const char ClosingDelimiter = '}';

    /// <summary>
    /// Parses the given format string into a list of fragments.
    /// </summary>
    /// <param name="format"></param>
    /// <returns></returns>
    static Fragment[] Parse(string format)
    {
        int lastCharIndex = format.Length - 1;
        int currFragEndIndex;
        Fragment currFrag = ParseFragment(format, 0, out currFragEndIndex);

        if (currFragEndIndex == lastCharIndex)
        {
            return new Fragment[] { currFrag };
        }

        List<Fragment> fragments = new List<Fragment>();
        while (true)
        {
            fragments.Add(currFrag);
            if (currFragEndIndex == lastCharIndex)
            {
                break;
            }
            currFrag = ParseFragment(format, currFragEndIndex + 1, out currFragEndIndex);
        }
        return fragments.ToArray();

    }

    /// <summary>
    /// Finds the next delimiter from the starting index.
    /// </summary>
    static Fragment ParseFragment(string format, int startIndex, out int fragmentEndIndex)
    {
        bool foundEscapedDelimiter = false;
        FragmentType type = FragmentType.Literal;

        int numChars = format.Length;
        for (int i = startIndex; i < numChars; i++)
        {
            char currChar = format[i];
            bool isOpenBrace = currChar == OpeningDelimiter;
            bool isCloseBrace = isOpenBrace ? false : currChar == ClosingDelimiter;

            if (!isOpenBrace && !isCloseBrace)
            {
                continue;
            }
            else if (i < (numChars - 1) && format[i + 1] == currChar)
            {//{{ or }}
                i++;
                foundEscapedDelimiter = true;
            }
            else if (isOpenBrace)
            {
                if (i == startIndex)
                {
                    type = FragmentType.FormatItem;
                }
                else
                {

                    if (type == FragmentType.FormatItem)
                        throw new FormatException("Two consequtive unescaped { format item openers were found.  Either close the first or escape any literals with another {.");

                    //curr character is the opening of a new format item.  so we close this literal out
                    string literal = format.Substring(startIndex, i - startIndex);
                    if (foundEscapedDelimiter)
                        literal = ReplaceEscapes(literal);

                    fragmentEndIndex = i - 1;
                    return new Fragment(FragmentType.Literal, literal);
                }
            }
            else
            {//close bracket
                if (i == startIndex || type == FragmentType.Literal)
                    throw new FormatException("A } closing brace existed without an opening { brace.");

                string formatItem = format.Substring(startIndex + 1, i - startIndex - 1);
                if (foundEscapedDelimiter)
                    formatItem = ReplaceEscapes(formatItem);//a format item with a { or } in its name is crazy but it could be done
                fragmentEndIndex = i;
                return new Fragment(FragmentType.FormatItem, formatItem);
            }
        }

        if (type == FragmentType.FormatItem)
            throw new FormatException("A format item was opened with { but was never closed.");

        fragmentEndIndex = numChars - 1;
        string literalValue = format.Substring(startIndex);
        if (foundEscapedDelimiter)
            literalValue = ReplaceEscapes(literalValue);

        return new Fragment(FragmentType.Literal, literalValue);

    }

    /// <summary>
    /// Replaces escaped brackets, turning '{{' and '}}' into '{' and '}', respectively.
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    static string ReplaceEscapes(string value)
    {
        return value.Replace("{{", "{").Replace("}}", "}");
    }

    private enum FragmentType
    {
        Literal,
        FormatItem
    }

    private class Fragment
    {

        public Fragment(FragmentType type, string value)
        {
            Type = type;
            Value = value;
        }

        public FragmentType Type
        {
            get;
            private set;
        }

        /// <summary>
        /// The literal value, or the name of the fragment, depending on fragment type.
        /// </summary>
        public string Value
        {
            get;
            private set;
        }


    }

}
Steve Potter
fuente
1
private static Regex s_NamedFormatRegex = new Regex(@"\{(?!\{)(?<key>[\w]+)(:(?<fmt>(\{\{|\}\}|[^\{\}])*)?)?\}", RegexOptions.Compiled);

public static StringBuilder AppendNamedFormat(this StringBuilder builder,IFormatProvider provider, string format, IDictionary<string, object> args)
{
    if (builder == null) throw new ArgumentNullException("builder");
    var str = s_NamedFormatRegex.Replace(format, (mt) => {
        string key = mt.Groups["key"].Value;
        string fmt = mt.Groups["fmt"].Value;
        object value = null;
        if (args.TryGetValue(key,out value)) {
            return string.Format(provider, "{0:" + fmt + "}", value);
        } else {
            return mt.Value;
        }
    });
    builder.Append(str);
    return builder;
}

public static StringBuilder AppendNamedFormat(this StringBuilder builder, string format, IDictionary<string, object> args)
{
    if (builder == null) throw new ArgumentNullException("builder");
    return builder.AppendNamedFormat(null, format, args);
}

Ejemplo:

var builder = new StringBuilder();
builder.AppendNamedFormat(
@"你好,{Name},今天是{Date:yyyy/MM/dd}, 这是你第{LoginTimes}次登录,积分{Score:{{ 0.00 }}}",
new Dictionary<string, object>() { 
    { "Name", "wayjet" },
    { "LoginTimes",18 },
    { "Score", 100.4 },
    { "Date",DateTime.Now }
});

Salida: 你好, wayjet, 今天 是 2011-05-04, 这 是 你 第 18 次 登录 , 积分 {100.40}

wayjet
fuente
1

Aquí hay un método simple para cualquier objeto:

    using System.Text.RegularExpressions;
    using System.ComponentModel;

    public static string StringWithFormat(string format, object args)
    {
        Regex r = new Regex(@"\{([A-Za-z0-9_]+)\}");

        MatchCollection m = r.Matches(format);

        var properties = TypeDescriptor.GetProperties(args);

        foreach (Match item in m)
        {
            try
            {
                string propertyName = item.Groups[1].Value;
                format = format.Replace(item.Value, properties[propertyName].GetValue(args).ToString());
            }
            catch
            {
                throw new FormatException("The format string is not valid");
            }
        }

        return format;
    }

Y aquí cómo usarlo:

 DateTime date = DateTime.Now;
 string dateString = StringWithFormat("{Month}/{Day}/{Year}", date);

salida: 27/02/2012

Ashkan Ghodrat
fuente
0

Implementé esta es una clase simple que duplica la funcionalidad de String.Format (excepto cuando se usan clases). Puede usar un diccionario o un tipo para definir campos.

https://github.com/SergueiFedorov/NamedFormatString

C # 6.0 está agregando esta funcionalidad directamente a la especificación del lenguaje, por lo que NamedFormatStringes para compatibilidad con versiones anteriores.

Serguei Fedorov
fuente
0

Resolví esto de una manera ligeramente diferente a las soluciones existentes. Realiza el núcleo del reemplazo del elemento nombrado (no el bit de reflexión que algunos han hecho). Es extremadamente rápido y simple ... Esta es mi solución:

/// <summary>
/// Formats a string with named format items given a template dictionary of the items values to use.
/// </summary>
public class StringTemplateFormatter
{
    private readonly IFormatProvider _formatProvider;

    /// <summary>
    /// Constructs the formatter with the specified <see cref="IFormatProvider"/>.
    /// This is defaulted to <see cref="CultureInfo.CurrentCulture">CultureInfo.CurrentCulture</see> if none is provided.
    /// </summary>
    /// <param name="formatProvider"></param>
    public StringTemplateFormatter(IFormatProvider formatProvider = null)
    {
        _formatProvider = formatProvider ?? CultureInfo.CurrentCulture;
    }

    /// <summary>
    /// Formats a string with named format items given a template dictionary of the items values to use.
    /// </summary>
    /// <param name="text">The text template</param>
    /// <param name="templateValues">The named values to use as replacements in the formatted string.</param>
    /// <returns>The resultant text string with the template values replaced.</returns>
    public string FormatTemplate(string text, Dictionary<string, object> templateValues)
    {
        var formattableString = text;
        var values = new List<object>();
        foreach (KeyValuePair<string, object> value in templateValues)
        {
            var index = values.Count;
            formattableString = ReplaceFormattableItem(formattableString, value.Key, index);
            values.Add(value.Value);
        }
        return String.Format(_formatProvider, formattableString, values.ToArray());
    }

    /// <summary>
    /// Convert named string template item to numbered string template item that can be accepted by <see cref="string.Format(string,object[])">String.Format</see>
    /// </summary>
    /// <param name="formattableString">The string containing the named format item</param>
    /// <param name="itemName">The name of the format item</param>
    /// <param name="index">The index to use for the item value</param>
    /// <returns>The formattable string with the named item substituted with the numbered format item.</returns>
    private static string ReplaceFormattableItem(string formattableString, string itemName, int index)
    {
        return formattableString
            .Replace("{" + itemName + "}", "{" + index + "}")
            .Replace("{" + itemName + ",", "{" + index + ",")
            .Replace("{" + itemName + ":", "{" + index + ":");
    }
}

Se usa de la siguiente manera:

    [Test]
    public void FormatTemplate_GivenANamedGuid_FormattedWithB_ShouldFormatCorrectly()
    {
        // Arrange
        var template = "My guid {MyGuid:B} is awesome!";
        var templateValues = new Dictionary<string, object> { { "MyGuid", new Guid("{A4D2A7F1-421C-4A1D-9CB2-9C2E70B05E19}") } };
        var sut = new StringTemplateFormatter();
        // Act
        var result = sut.FormatTemplate(template, templateValues);
        //Assert
        Assert.That(result, Is.EqualTo("My guid {a4d2a7f1-421c-4a1d-9cb2-9c2e70b05e19} is awesome!"));
    }

¡Espero que alguien encuentre esto útil!

Mark Whitfeld
fuente
0

A pesar de que la respuesta aceptada da algunos buenos ejemplos, el .Inject y algunos de los ejemplos de Haack no manejan el escape. Muchos también dependen en gran medida de Regex (más lento) o DataBinder.Eval, que no está disponible en .NET Core, y en algunos otros entornos.

Con eso en mente, he escrito un analizador basado en una máquina de estado simple que fluye a través de caracteres, escribiendo en una StringBuildersalida, carácter por carácter. Se implementa como Stringmétodo (s) de extensión y puede tomar tanto Dictionary<string, object>ao objectcon parámetros como entrada (usando la reflexión).

Maneja niveles ilimitados {{{escaping}}}y tira FormatExceptioncuando la entrada contiene llaves desequilibradas y / u otros errores.

public static class StringExtension {
    /// <summary>
    /// Extension method that replaces keys in a string with the values of matching object properties.
    /// </summary>
    /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
    /// <param name="injectionObject">The object whose properties should be injected in the string</param>
    /// <returns>A version of the formatString string with keys replaced by (formatted) key values.</returns>
    public static string FormatWith(this string formatString, object injectionObject) {
        return formatString.FormatWith(GetPropertiesDictionary(injectionObject));
    }

    /// <summary>
    /// Extension method that replaces keys in a string with the values of matching dictionary entries.
    /// </summary>
    /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
    /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
    /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
    public static string FormatWith(this string formatString, IDictionary<string, object> dictionary) {
        char openBraceChar = '{';
        char closeBraceChar = '}';

        return FormatWith(formatString, dictionary, openBraceChar, closeBraceChar);
    }
        /// <summary>
        /// Extension method that replaces keys in a string with the values of matching dictionary entries.
        /// </summary>
        /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
        /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
        /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
    public static string FormatWith(this string formatString, IDictionary<string, object> dictionary, char openBraceChar, char closeBraceChar) {
        string result = formatString;
        if (dictionary == null || formatString == null)
            return result;

        // start the state machine!

        // ballpark output string as two times the length of the input string for performance (avoids reallocating the buffer as often).
        StringBuilder outputString = new StringBuilder(formatString.Length * 2);
        StringBuilder currentKey = new StringBuilder();

        bool insideBraces = false;

        int index = 0;
        while (index < formatString.Length) {
            if (!insideBraces) {
                // currently not inside a pair of braces in the format string
                if (formatString[index] == openBraceChar) {
                    // check if the brace is escaped
                    if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) {
                        // add a brace to the output string
                        outputString.Append(openBraceChar);
                        // skip over braces
                        index += 2;
                        continue;
                    }
                    else {
                        // not an escaped brace, set state to inside brace
                        insideBraces = true;
                        index++;
                        continue;
                    }
                }
                else if (formatString[index] == closeBraceChar) {
                    // handle case where closing brace is encountered outside braces
                    if (index < formatString.Length - 1 && formatString[index + 1] == closeBraceChar) {
                        // this is an escaped closing brace, this is okay
                        // add a closing brace to the output string
                        outputString.Append(closeBraceChar);
                        // skip over braces
                        index += 2;
                        continue;
                    }
                    else {
                        // this is an unescaped closing brace outside of braces.
                        // throw a format exception
                        throw new FormatException($"Unmatched closing brace at position {index}");
                    }
                }
                else {
                    // the character has no special meaning, add it to the output string
                    outputString.Append(formatString[index]);
                    // move onto next character
                    index++;
                    continue;
                }
            }
            else {
                // currently inside a pair of braces in the format string
                // found an opening brace
                if (formatString[index] == openBraceChar) {
                    // check if the brace is escaped
                    if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) {
                        // there are escaped braces within the key
                        // this is illegal, throw a format exception
                        throw new FormatException($"Illegal escaped opening braces within a parameter - index: {index}");
                    }
                    else {
                        // not an escaped brace, we have an unexpected opening brace within a pair of braces
                        throw new FormatException($"Unexpected opening brace inside a parameter - index: {index}");
                    }
                }
                else if (formatString[index] == closeBraceChar) {
                    // handle case where closing brace is encountered inside braces
                    // don't attempt to check for escaped braces here - always assume the first brace closes the braces
                    // since we cannot have escaped braces within parameters.

                    // set the state to be outside of any braces
                    insideBraces = false;

                    // jump over brace
                    index++;

                    // at this stage, a key is stored in current key that represents the text between the two braces
                    // do a lookup on this key
                    string key = currentKey.ToString();
                    // clear the stringbuilder for the key
                    currentKey.Clear();

                    object outObject;

                    if (!dictionary.TryGetValue(key, out outObject)) {
                        // the key was not found as a possible replacement, throw exception
                        throw new FormatException($"The parameter \"{key}\" was not present in the lookup dictionary");
                    }

                    // we now have the replacement value, add the value to the output string
                    outputString.Append(outObject);

                    // jump to next state
                    continue;
                } // if }
                else {
                    // character has no special meaning, add it to the current key
                    currentKey.Append(formatString[index]);
                    // move onto next character
                    index++;
                    continue;
                } // else
            } // if inside brace
        } // while

        // after the loop, if all braces were balanced, we should be outside all braces
        // if we're not, the input string was misformatted.
        if (insideBraces) {
            throw new FormatException("The format string ended before the parameter was closed.");
        }

        return outputString.ToString();
    }

    /// <summary>
    /// Creates a Dictionary from an objects properties, with the Key being the property's
    /// name and the Value being the properties value (of type object)
    /// </summary>
    /// <param name="properties">An object who's properties will be used</param>
    /// <returns>A <see cref="Dictionary"/> of property values </returns>
    private static Dictionary<string, object> GetPropertiesDictionary(object properties) {
        Dictionary<string, object> values = null;
        if (properties != null) {
            values = new Dictionary<string, object>();
            PropertyDescriptorCollection props = TypeDescriptor.GetProperties(properties);
            foreach (PropertyDescriptor prop in props) {
                values.Add(prop.Name, prop.GetValue(properties));
            }
        }
        return values;
    }
}

En última instancia, toda la lógica se reduce a 10 estados principales: porque cuando la máquina de estado está fuera de un paréntesis y del mismo modo dentro de un paréntesis, el siguiente carácter es una llave abierta, una llave abierta con escape, una llave cerrada con escape, una llave cerrada con escape, o un personaje ordinario. Cada una de estas condiciones se maneja individualmente a medida que avanza el ciclo, agregando caracteres a una salida StringBuffero una clave StringBuffer. Cuando se cierra un parámetro, el valor de la clave StringBufferse utiliza para buscar el valor del parámetro en el diccionario, que luego se introduce en la salida StringBuffer. Al final, StringBufferse devuelve el valor de la salida .

Ryan
fuente
-6
string language = "Python";
int numquotes = 2;
string output = language + " has "+ numquotes + " language types.";

Editar: Lo que debería haber dicho fue: "No, no creo que lo que quieres hacer esté respaldado por C #. Esto es lo más cercano que vas a tener".

Kevin
fuente
1
Tengo curiosidad por los votos negativos. ¿Alguien quiere decirme por qué?
Kevin
1
Entonces, string.format realizaría esta operación 4 / diezmilésimas de segundo más rápido. Si esta función se llamará una tonelada, es posible que note esa diferencia. Pero al menos responde a su pregunta en lugar de decirle que lo haga de la misma manera que ya dijo que no quería hacerlo.
Kevin
44
No te rechacé, pero no implementaría esto principalmente porque, bueno, encuentro que hacer muchas concatenaciones de cadenas es feo. Pero esa es mi opinión personal.
Jason Baker, el
Extraño que esto haya caído votó demasiado. Considere ampliar su respuesta, que cuando la concatenación no se llama con frecuencia, podría considerar "someString" + someVariable + "someOtherString"más legible. Este artículo está de acuerdo contigo.
Steven Jeuris el