Named string formatting in C#
Asked Answered
H

18

157

Is there any way to format a string by name rather than position in C#?

In python, I can do something like this example (shamelessly stolen from here):

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

Is there any way to do this in C#? Say for instance:

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

Being able to do this using a variable name would be nice, but a dictionary is acceptable too.

Hull answered 1/10, 2008 at 18:30 Comment(5)
I'm missing this from Ruby as well.Fatsoluble
I think your example is too simplistic and is leading people to give you unhelpful answers. Maybe using a variable more than once in the string would be more demonstrative.Stamp
Actually, the SPECIFIC confusion is the use of String.Format. That lends itself to answers such as mine, which are not helpful because they're not variable oriented, but are accurate insofar as String.Format is concerned.Mcgann
The call to String.Format is obviously a contrived example. Unless of course you weren't aware that calling String.Format with ellipses isn't possible. The problem was that I didn't put that I wanted the formatting to happen by named parameters rather than position, which has been fiexed.Hull
FYI: Submitted to MS Connect's User Voice to request this be made a standard feature of the framework. For anyone interested, please upvote: visualstudio.uservoice.com/forums/121579-visual-studio/…Ivers
W
131

There is no built-in method for handling this.

Here's one method

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

Here's another

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

A third improved method partially based on the two above, from Phil Haack


Update: This is now built-in as of C# 6 (released in 2015).

String Interpolation

$"{some_variable}: {some_other_variable}"
Whipcord answered 1/10, 2008 at 18:49 Comment(4)
I've been very happy using FormatWith(), but wanted to point an in issue I recently came across. The implementation relies on the DataBinder from System.Web.UI, which isn't supported in SQL CLR. Inject(o) doesn't rely on the data binder, which made it useful for multi-token-replace in my SQL CLR object.Buffum
Maybe you can update first sentence of your answer. String interpolation is present in C# and VB for few months (finally...). Your answer is at the top so it might be useful for readers if you can link them to some updated .NET resources.Acid
@Acid it is not really the same. You cannot pass interpolated strings around: https://mcmap.net/q/138576/-is-it-possible-to-pass-interpolated-strings-as-parameter-to-a-method/213725Psychogenesis
@Psychogenesis – you are definitely right but it was not their purpose. In Q&A you linked, OP tries to reference variable name yet before it exists. Not very good idea, but if someone insists on that, he can construct specialized parser. But I wouldn't mess this with general string interpolation concept.Acid
R
45

I have an implementation I just posted to my blog here: http://haacked.com/archive/2009/01/04/fun-with-named-formats-string-parsing-and-edge-cases.aspx

It addresses some issues that these other implementations have with brace escaping. The post has details. It does the DataBinder.Eval thing too, but is still very fast.

Reviewer answered 5/1, 2009 at 4:32 Comment(6)
The code available for download in that article 404's. I'd really like to see it, too.Gangboard
@qes: An updated link was posted in the comments: code.haacked.com/util/NamedStringFormatSolution.zipEnigmatic
@OliverSalzburg : I've been using SmartFormat for all my formatting needs for some time now, love it. github.com/scottrippey/SmartFormatGangboard
@qes: Would you mind possibly writing and answer about it and showing how it works? Looks interestingEnigmatic
@qes: You should definitely add SmartFormat as an answer as it is very nice and actively supported (2015).Kalisz
NamedFormat("{a} is not, {b}", new {a:"my a",b:"my b"}); That is as short as magicHeaves
A
43

Interpolated strings were added into C# 6.0 and Visual Basic 14

Both were introduced through new Roslyn compiler in Visual Studio 2015.

  • C# 6.0:

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

  • VB 14:

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

Noteworthy features (in Visual Studio 2015 IDE):

  • syntax coloring is supported - variables contained in strings are highlighted
  • refactoring is supported - when renaming, variables contained in strings get renamed, too
  • actually not only variable names, but expressions are supported - e.g. not only {index} works, but also {(index + 1).ToString().Trim()}

Enjoy! (& click "Send a Smile" in the VS)

Acid answered 1/12, 2014 at 12:55 Comment(3)
The question is tagged with .net 3.5 therefore your information is valid but it's not an alternativePacian
@Acid - You're right about the framework version. The string interpolation just depends on the new Roslyn compiler used in VS 2015.Pacian
This also won't work unless your format string is put into the code itself. i.e it won't work if your format string comes from an external source, like a config file or database.Favouritism
U
40

You can also use anonymous types like this:

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

Of course it would require more code if you also want to parse formatting, but you can format a string using this function like:

Format("test {first} and {another}", new { first = "something", another = "something else" })
Ury answered 2/11, 2010 at 11:21 Comment(1)
Perfect for those of us still on 2.0. Yeah, I know.... This solution is straightforward and easy to understand. AND IT WORKS!!!Dawnedawson
F
14

There doesn't appear to be a way to do this out of the box. Though, it looks feasible to implement your own IFormatProvider that links to an IDictionary for values.

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

Outputs:

Python has 2 quote types

The caveat is that you can't mix FormatProviders, so the fancy text formatting can't be used at the same time.

Feeble answered 1/10, 2008 at 19:1 Comment(1)
+1 for outlining, IMHO, the best conceptual method, which has a nice implementation at mo.notono.us/2008/07/c-stringinject-format-strings-by-key.html - the other posts include this but they also propose the reflection based methods which, IMHO, are rather evilDanika
C
9

The framework itself does not provide a way to do this, but you can take a look at this post by Scott Hanselman. Example usage:

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

This code by James Newton-King is similar and works with sub-properties and indexes,

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

James's code relies on System.Web.UI.DataBinder to parse the string and requires referencing System.Web, which some people don't like to do in non-web applications.

EDIT: Oh and they work nicely with anonymous types, if you don't have an object with properties ready for it:

string name = ...;
DateTime date = ...;
string foo = "{Name} - {Birthday}".FormatWith(new { Name = name, Birthday = date });
Cunaxa answered 1/10, 2008 at 19:22 Comment(0)
G
7

See https://stackoverflow.com/questions/271398?page=2#358259

With the linked-to extension you can write this:

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

and you'll get "foo 2 System.Object".

Gimmick answered 5/1, 2009 at 15:38 Comment(0)
P
4

I think the closest you'll get is an indexed format:

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

There's also String.Replace(), if you're willing to do it in multiple steps and take it on faith that you won't find your 'variables' anywhere else in the string:

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

Expanding this to use a List:

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

You could do that with a Dictionary<string, string> too by iterating it's .Keys collections, but by using a List<KeyValuePair<string, string>> we can take advantage of the List's .ForEach() method and condense it back to a one-liner:

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

A lambda would be even simpler, but I'm still on .Net 2.0. Also note that the .Replace() performance isn't stellar when used iteratively, since strings in .Net are immutable. Also, this requires the MyString variable be defined in such a way that it's accessible to the delegate, so it's not perfect yet.

Parasympathetic answered 1/10, 2008 at 18:30 Comment(1)
Well, that's not the prettiest solution, but it's what I'm going with for now. The only thing I did differently was use a StringBuilder instead of a string so that I don't keep making new strings.Hull
V
3

My open source library, Regextra, supports named formatting (amongst other things). It currently targets .NET 4.0+ and is available on NuGet. I also have an introductory blog post about it: Regextra: helping you reduce your (problems){2}.

The named formatting bit supports:

  • Basic formatting
  • Nested properties formatting
  • Dictionary formatting
  • Escaping of delimiters
  • Standard/Custom/IFormatProvider string formatting

Example:

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

Result:

We just shipped your order of 'Widget', placed on 2/28/2014. Your {credit} card will be billed $1,500.00.

Check out the project's GitHub link (above) and wiki for other examples.

Varnish answered 19/4, 2014 at 17:37 Comment(1)
Wow, this looks amazing, particularly for when dealing with some of the more difficult format examples one comes across.Graminivorous
S
2
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);
}

Example:

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

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

Stillbirth answered 4/5, 2011 at 6:56 Comment(0)
C
2

Check this one:

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

Sample:

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

Performance is pretty ok compared to other solutions.

Contractile answered 22/9, 2011 at 5:53 Comment(0)
S
1

I doubt this will be possible. The first thing that comes to mind is how are you going to get access to local variable names?

There might be some clever way using LINQ and Lambda expressions to do this however.

Speleology answered 1/10, 2008 at 18:33 Comment(4)
@leppie: +1 if you can give me some LINQ+Lambda to do that ;D (ok +1 for having a relevant answer)Merriment
I would love to see it too! Maybe I will take that challenge!Speleology
I figured it would be impossible to do with variable names, but put that in there in case I was wrong. :) There's not any way to do this with a dictionary either?Hull
I tried, and got a little somewhere, but I deemed it too ugly and difficult to use. It would have looked like: string s = format(f => f("{hello} {world}", hello, world));Speleology
T
1

Here's one I made a while back. It extends String with a Format method taking a single argument. The nice thing is that it'll use the standard string.Format if you provide a simple argument like an int, but if you use something like anonymous type it'll work too.

Example usage:

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

Would result in "The Smith family has 4 children."

It doesn't do crazy binding stuff like arrays and indexers. But it is super simple and high performance.

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


    }

}
Tiling answered 29/1, 2011 at 18:51 Comment(0)
M
1

here is a simple method for any object:

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

And here how to use it:

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

output : 2/27/2012

Moller answered 26/2, 2012 at 23:50 Comment(0)
M
1

Even though the accepted answer gives some good examples, the .Inject as well as some of the Haack examples do not handle escaping. Many also rely heavily on Regex (slower), or DataBinder.Eval which is not available on .NET Core, and in some other environments.

With that in mind, I've written a simple state machine based parser that streams through characters, writing to a StringBuilder output, character by character. It is implemented as String extension method(s) and can take both a Dictionary<string, object> or object with parameters as input (using reflection).

It handles unlimited levels of {{{escaping}}} and throws FormatException when input contains unbalanced braces and/or other errors.

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

Ultimately, all the logic boils down into 10 main states - For when the state machine is outside a bracket and likewise inside a bracket, the next character is either an open brace, an escaped open brace, a closed brace, an escaped closed brace, or an ordinary character. Each of these conditions is handled individually as the loop progresses, adding characters to either an output StringBuffer or a key StringBuffer. When a parameter is closed, the value of the key StringBuffer is used to look up the parameter's value in the dictionary, which then gets pushed into the output StringBuffer. At the end, the value of the output StringBuffer is returned.

Monostylous answered 23/2, 2016 at 3:8 Comment(0)
M
0

I implemented this is a simple class that duplicates the functionality of String.Format (except for when using classes). You can either use a dictionary or a type to define fields.

https://github.com/SergueiFedorov/NamedFormatString

C# 6.0 is adding this functionality right into the language spec, so NamedFormatString is for backwards compatibility.

Maravedi answered 14/5, 2015 at 21:6 Comment(0)
M
0

I solved this in a slightly different way to the existing solutions. It does the core of the named item replacement (not the reflection bit that some have done). It is extremely fast and simple... This is my 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 + ":");
    }
}

It is used in the following way:

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

Hope someone finds this useful!

Magdalenamagdalene answered 19/11, 2015 at 11:10 Comment(0)
S
-7
string language = "Python";
int numquotes = 2;
string output = language + " has "+ numquotes + " language types.";

Edit: What I should have said was, "No, I don't believe what you want to do is supported by C#. This is as close as you are going to get."

Sophrosyne answered 1/10, 2008 at 18:42 Comment(4)
I'm curious about the down votes. Anybody want to tell me why?Sophrosyne
So the string.format would perform this operation 4/TenThousandths of a second faster If this function is going to get called a ton you might notice that difference. But it at least answers his question instead of just telling him to do it the same way he already said he didn't want to do it.Sophrosyne
I didn't vote you down, but I wouldn't implement this mainly because well, I find doing lots of string concatenations ugly. But that's my personal view.Hull
Weird that this got down voted so much. Consider expanding your answer, that when the concatenation isn't called often you could consider "someString" + someVariable + "someOtherString" more readable. This article agrees with you.Bughouse

© 2022 - 2024 — McMap. All rights reserved.