Mise en forme de chaîne nommée en C #

156

Existe-t-il un moyen de formater une chaîne par son nom plutôt que par sa position en C #?

En python, je peux faire quelque chose comme cet exemple (volé sans vergogne d' ici ):

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

Existe-t-il un moyen de le faire en C #? Dites par exemple:

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

Pouvoir le faire en utilisant un nom de variable serait bien, mais un dictionnaire est également acceptable.

Jason Baker
la source
Cela me manque aussi de Ruby.
JesperE
Je pense que votre exemple est trop simpliste et amène les gens à vous donner des réponses inutiles. Peut-être que l'utilisation d'une variable plus d'une fois dans la chaîne serait plus démonstrative.
Wedge
En fait, la confusion SPÉCIFIQUE est l'utilisation de String.Format. Cela se prête à des réponses telles que la mienne, qui ne sont pas utiles car elles ne sont pas orientées vers des variables, mais sont précises en ce qui concerne String.Format.
John Rudy
1
L'appel à String.Format est évidemment un exemple artificiel. À moins bien sûr que vous ne saviez pas que l'appel de String.Format avec des ellipses n'est pas possible. Le problème était que je n'ai pas dit que je voulais que le formatage se fasse par des paramètres nommés plutôt que par la position, qui a été fiexée.
Jason Baker
FYI: Soumis à la voix de l'utilisateur de MS Connect pour demander que cela devienne une fonctionnalité standard du cadre. Pour toute personne intéressée, veuillez voter pour: visualstudio.uservoice.com/forums/121579-visual-studio
...

Réponses:

130

Il n'y a pas de méthode intégrée pour gérer cela.

Voici une méthode

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

Voici un autre

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

Une troisième méthode améliorée partiellement basée sur les deux ci - dessus , de Phil Haack

John Sheehan
la source
11
J'ai été très heureux d'utiliser FormatWith (), mais je voulais signaler un problème que j'ai récemment rencontré. L'implémentation repose sur le DataBinder de System.Web.UI, qui n'est pas pris en charge dans SQL CLR. Inject (o) ne repose pas sur le classeur de données, ce qui l'a rendu utile pour le remplacement multi-jeton dans mon objet SQL CLR.
EBarr
1
Vous pouvez peut-être mettre à jour la première phrase de votre réponse. L'interpolation de chaîne est présente en C # et VB depuis quelques mois (enfin ...). Votre réponse est en haut, il pourrait donc être utile pour les lecteurs si vous pouvez les lier à des ressources .NET mises à jour.
miroxlav
1
@miroxlav ce n'est pas vraiment la même chose. Vous ne pouvez pas passer de chaînes interpolées autour de: stackoverflow.com/q/31987232/213725
DixonD
@DixonD - vous avez certainement raison mais ce n'était pas leur but. Dans Q&A que vous avez lié, OP essaie de référencer le nom de la variable avant qu'il n'existe. Ce n'est pas une très bonne idée, mais si quelqu'un insiste là-dessus, il peut construire un analyseur spécialisé. Mais je ne voudrais pas gâcher cela avec le concept général d'interpolation de chaîne.
miroxlav le
44

J'ai une implémentation que je viens de publier sur mon blog ici: http://haacked.com/archive/2009/01/04/fun-with-named-formats-string-parsing-and-edge-cases.aspx

Il résout certains problèmes que ces autres implémentations ont avec l'échappement d'accolades. Le message contient des détails. Il fait aussi la chose DataBinder.Eval, mais est toujours très rapide.

Haacked
la source
3
Le code disponible en téléchargement dans cet article 404. J'aimerais vraiment le voir aussi.
quentin-star du
2
@qes: Un lien mis à jour a été posté dans les commentaires: code.haacked.com/util/NamedStringFormatSolution.zip
Der Hochstapler
3
@OliverSalzburg: J'utilise SmartFormat pour tous mes besoins de formatage depuis un certain temps maintenant, j'adore ça. github.com/scottrippey/SmartFormat
quentin-starin
@qes: Pourriez-vous écrire et répondre à ce sujet et montrer comment cela fonctionne? Ça a l'air intéressant
Der Hochstapler
@qes: Vous devez absolument ajouter SmartFormat comme réponse car il est très agréable et activement pris en charge (2015).
Răzvan Flavius ​​Panda
42

Des chaînes interpolées ont été ajoutées dans C # 6.0 et Visual Basic 14

Les deux ont été introduits via le nouveau compilateur Roslyn dans Visual Studio 2015 .

  • C # 6.0:

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

  • VB 14:

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

Fonctionnalités remarquables (dans l'IDE de Visual Studio 2015):

  • la coloration de la syntaxe est prise en charge - les variables contenues dans les chaînes sont mises en évidence
  • la refactorisation est prise en charge - lors du changement de nom, les variables contenues dans les chaînes sont également renommées
  • en fait, non seulement les noms de variables, mais les expressions sont pris en charge - par exemple, non seulement {index}fonctionne, mais aussi{(index + 1).ToString().Trim()}

Prendre plaisir! (& cliquez sur "Envoyer un sourire" dans le VS)

miroxlav
la source
2
La question est taguée avec .net 3.5 donc vos informations sont valides mais ce n'est pas une alternative
Douglas Gandini
1
@miroxlav - Vous avez raison sur la version du framework. L'interpolation de chaîne dépend uniquement du nouveau compilateur Roslyn utilisé dans VS 2015.
Douglas Gandini
2
Cela ne fonctionnera pas non plus à moins que votre chaîne de format ne soit insérée dans le code lui-même. c'est-à-dire que cela ne fonctionnera pas si votre chaîne de format provient d'une source externe, comme un fichier de configuration ou une base de données.
Craig Brett
40

Vous pouvez également utiliser des types anonymes comme celui-ci:

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

Bien sûr, il faudrait plus de code si vous souhaitez également analyser le formatage, mais vous pouvez formater une chaîne en utilisant cette fonction comme:

Format("test {first} and {another}", new { first = "something", another = "something else" })
Doggett
la source
1
Parfait pour ceux d'entre nous encore sur 2.0. Ouais, je sais ... Cette solution est simple et facile à comprendre. ET IL FONCTIONNE!!!
Brad Bruce
14

Il ne semble pas y avoir de moyen de le faire hors de la boîte. Cependant, il semble possible de mettre en œuvre les vôtres IFormatProviderqui sont liés à IDictionarydes valeurs 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);

Les sorties:

Python a 2 types de devis

La mise en garde est que vous ne pouvez pas mélanger FormatProviders, donc le formatage de texte sophistiqué ne peut pas être utilisé en même temps.

Spoulson
la source
1
+1 pour décrire, à mon humble avis , la meilleure méthode conceptuelle, qui a une belle implémentation sur mo.notono.us/2008/07/c-stringinject-format-strings-by-key.html - les autres messages incluent cela mais ils aussi proposer les méthodes basées sur la réflexion qui, à mon humble avis, sont plutôt mauvaises
Adam Ralph
9

Le cadre lui-même ne fournit pas un moyen de le faire, mais vous pouvez jeter un œil à cet article de Scott Hanselman. Exemple d'utilisation:

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

Ce code de James Newton-King est similaire et fonctionne avec des sous-propriétés et des index,

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

Le code de James s'appuie sur System.Web.UI.DataBinder pour analyser la chaîne et nécessite de référencer System.Web, ce que certaines personnes n'aiment pas faire dans des applications non Web.

EDIT: Oh et ils fonctionnent bien avec les types anonymes, si vous n'avez pas d'objet avec des propriétés prêtes pour cela:

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

Voir /programming/271398?page=2#358259

Avec l'extension liée, vous pouvez écrire ceci:

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

et vous obtiendrez "foo 2 System.Object".

Mark Cidade
la source
4

Je pense que le plus proche que vous obtiendrez est un format indexé:

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

Il y a aussi String.Replace (), si vous êtes prêt à le faire en plusieurs étapes et à croire que vous ne trouverez vos `` variables '' nulle part ailleurs dans la chaîne:

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

Étendre ceci pour utiliser une liste:

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

Vous pouvez également le faire avec un Dictionary <string, string> en itérant ses collections .Keys, mais en utilisant une List <KeyValuePair <string, string >>, nous pouvons tirer parti de la méthode .ForEach () de List et la condenser en un monoplace:

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

Un lambda serait encore plus simple, mais je suis toujours sur .Net 2.0. Notez également que les performances de .Replace () ne sont pas excellentes lorsqu'elles sont utilisées de manière itérative, car les chaînes de .Net sont immuables. De plus, cela nécessite que la MyStringvariable soit définie de telle manière qu'elle soit accessible au délégué, donc ce n'est pas encore parfait.

Joel Coehoorn
la source
Eh bien, ce n'est pas la plus jolie solution, mais c'est ce que je veux pour l'instant. La seule chose que j'ai faite différemment a été d'utiliser un StringBuilder au lieu d'une chaîne pour ne pas continuer à créer de nouvelles chaînes.
Jason Baker
3

Ma bibliothèque open source, Regextra , prend en charge le formatage nommé (entre autres). Il cible actuellement .NET 4.0+ et est disponible sur NuGet . J'ai également un article de blog à ce sujet: Regextra: vous aider à réduire vos (problèmes) {2} .

Le bit de formatage nommé prend en charge:

  • Formatage de base
  • Mise en forme des propriétés imbriquées
  • Formatage du dictionnaire
  • Échappement des délimiteurs
  • Formatage de chaîne Standard / Personnalisé / IFormatProvider

Exemple:

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

Résultat:

Nous venons d'expédier votre commande de 'Widget', passée le 28/02/2014. Votre carte de {crédit} sera facturée 1 500,00 $.

Consultez le lien GitHub du projet (ci-dessus) et le wiki pour d'autres exemples.

Ahmad Mageed
la source
Wow, cela semble incroyable, en particulier pour certains des exemples de format les plus difficiles que l'on rencontre.
Nicholas Petersen
2

Vérifier celui-ci:

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

Échantillon:

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

Les performances sont assez bonnes par rapport aux autres solutions.

Pavlo Neiman
la source
1

Je doute que ce soit possible. La première chose qui vous vient à l'esprit est de savoir comment allez-vous accéder aux noms de variables locales?

Il peut cependant y avoir un moyen intelligent d'utiliser les expressions LINQ et Lambda pour le faire.

leppie
la source
@leppie: +1 si vous pouvez me donner un LINQ + Lambda pour le faire; D (ok +1 pour avoir une réponse pertinente)
user7116
J'adorerais le voir aussi! Peut-être que je vais relever ce défi!
leppie
J'ai pensé qu'il serait impossible de faire avec des noms de variables, mais mettez-le là au cas où je me trompais. :) Il n'y a aucun moyen de faire cela avec un dictionnaire non plus?
Jason Baker
J'ai essayé et je suis arrivé un peu quelque part, mais je l'ai trouvé trop moche et difficile à utiliser. Cela aurait ressemblé à: string s = format (f => f ("{bonjour} {monde}", bonjour, monde));
leppie
1

En voici un que j'ai fait il y a quelque temps. Il étend String avec une méthode Format prenant un seul argument. La bonne chose est qu'il utilisera la chaîne standard.Format si vous fournissez un argument simple comme un int, mais si vous utilisez quelque chose comme le type anonyme, cela fonctionnera aussi.

Exemple d'utilisation:

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

Cela aboutirait à "La famille Smith a 4 enfants".

Il ne fait pas de liaison folle comme les tableaux et les indexeurs. Mais c'est super simple et performant.

    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
la source
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);
}

Exemple:

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

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

wayjet
la source
1

voici une méthode simple pour tout objet:

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

Et voici comment l'utiliser:

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

sortie: 27/02/2012

Ashkan Ghodrat
la source
0

J'ai implémenté c'est une classe simple qui duplique la fonctionnalité de String.Format (sauf lors de l'utilisation de classes). Vous pouvez utiliser un dictionnaire ou un type pour définir des champs.

https://github.com/SergueiFedorov/NamedFormatString

C # 6.0 ajoute cette fonctionnalité directement dans la spécification du langage, il en NamedFormatStringva de même pour la compatibilité descendante.

Serguei Fedorov
la source
0

J'ai résolu cela d'une manière légèrement différente des solutions existantes. Il fait le noyau du remplacement de l'élément nommé (pas le bit de réflexion que certains ont fait). C'est extrêmement simple et rapide ... Voici ma solution:

/// <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 + ":");
    }
}

Il est utilisé de la manière suivante:

    [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!"));
    }

J'espère que quelqu'un trouvera cela utile!

Mark Whitfeld
la source
0

Même si la réponse acceptée donne de bons exemples, le .Inject ainsi que certains des exemples Haack ne gèrent pas les échappements. Beaucoup dépendent également fortement de Regex (plus lent) ou de DataBinder.Eval qui n'est pas disponible sur .NET Core et dans certains autres environnements.

Dans cet esprit, j'ai écrit un analyseur simple basé sur une machine à états qui diffuse les caractères, en écrivant dans une StringBuildersortie, caractère par caractère. Il est implémenté en tant que Stringméthode (s) d'extension et peut prendre à la fois un Dictionary<string, object>ouobject avec des paramètres en entrée (en utilisant la réflexion).

Il gère des niveaux illimités de {{{escaping}}}et jette FormatExceptionlorsque l'entrée contient des accolades déséquilibrées et / ou d'autres erreurs.

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 fin de compte, toute la logique se résume en 10 états principaux - Pour lorsque la machine à états est en dehors d'un crochet et également à l'intérieur d'un crochet, le caractère suivant est soit une accolade ouverte, une accolade ouverte échappée, une accolade fermée, une accolade fermée échappée, ou un personnage ordinaire. Chacune de ces conditions est gérée individuellement à mesure que la boucle progresse, ajoutant des caractères à une sortie StringBufferou à une clé StringBuffer. Lorsqu'un paramètre est fermé, la valeur de la clé StringBufferest utilisée pour rechercher la valeur du paramètre dans le dictionnaire, qui est ensuite poussée dans la sortie StringBuffer. À la fin, la valeur de la sortie StringBufferest renvoyée.

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

Edit: Ce que j'aurais dû dire était: "Non, je ne crois pas que ce que vous voulez faire soit pris en charge par C #. C'est aussi proche que vous allez l'être."

Kevin
la source
1
Je suis curieux de connaître les votes négatifs. Quelqu'un veut me dire pourquoi?
Kevin
1
Ainsi, string.format effectuerait cette opération 4 / dix mille fois plus vite. Si cette fonction doit être appelée une tonne, vous remarquerez peut-être cette différence. Mais cela répond au moins à sa question au lieu de simplement lui dire de le faire de la même manière qu'il a déjà dit qu'il ne voulait pas le faire.
Kevin
4
Je ne vous ai pas voté contre, mais je ne l'implémenterais pas principalement parce que je trouve moche de faire beaucoup de concaténations de chaînes. Mais c'est mon opinion personnelle.
Jason Baker
Bizarre que cela ait tant voté. Pensez à élargir votre réponse, que lorsque la concaténation n'est pas souvent appelée, vous pourriez considérer "someString" + someVariable + "someOtherString"plus lisible. Cet article est d'accord avec vous.
Steven Jeuris