36

Instead of using {0} {1}, etc. I want to use {title} instead. Then fill that data in somehow (below I used a Dictionary). This code is invalid and throws an exception. I wanted to know if i can do something similar to what i want. Using {0 .. N} is not a problem. I was just curious.

Dictionary<string, string> d = new Dictionary<string, string>();
d["a"] = "he";
d["ba"] = "llo";
d["lol"] = "world";
string a = string.Format("{a}{ba}{lol}", d);
Salah Akbari
  • 39,330
  • 10
  • 79
  • 109
  • 4
    See also: http://stackoverflow.com/questions/1322037/how-can-i-create-a-more-user-friendly-string-format-syntax/1322103#1322103 – Marc Gravell Oct 06 '09 at 21:36

12 Answers12

18

No, but this extension method will do it

static string FormatFromDictionary(this string formatString, Dictionary<string, string> valueDict) 
{
    int i = 0;
    StringBuilder newFormatString = new StringBuilder(formatString);
    Dictionary<string, int> keyToInt = new Dictionary<string,int>();
    foreach (var tuple in valueDict)
    {
        newFormatString = newFormatString.Replace("{" + tuple.Key + "}", "{" + i.ToString() + "}");
        keyToInt.Add(tuple.Key, i);
        i++;                    
    }
    return String.Format(newFormatString.ToString(), valueDict.OrderBy(x => keyToInt[x.Key]).Select(x => x.Value).ToArray());
}
Beakie
  • 1,948
  • 3
  • 20
  • 46
LPCRoy
  • 935
  • 2
  • 12
  • 19
  • 5
    Careful, having "{{thing}}" in `formatString` and having a key called "thing" in `ValueDict` will replace "thing" in the string for a number. – Juan Jun 23 '13 at 05:01
  • This worked well. ReSharper didn't like casting string to object though so i changed your dictionary to a string,object pair. Also, a nitpick but I didn't agree that a keyvaluepair is a tuple. It's not really an ordered list of things. The ValueDict is more a tuple than anything. – JDPeckham Jun 08 '15 at 17:17
8

Check this one, it supports formating:

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

        return words.Aggregate(
            format,
            (current, key) =>
                {
                    int colonIndex = key.IndexOf(':');
                    return current.Replace(
                        "{" + key + "}",
                        colonIndex > 0
                            ? string.Format("{0:" + key.Substring(colonIndex + 1) + "}", values[key.Substring(0, colonIndex)])
                            : values[key] == null ? string.Empty : values[key].ToString());
                });
    }

How to use:

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

        
Amir Touitou
  • 3,141
  • 1
  • 35
  • 31
Pavlo Neiman
  • 7,438
  • 3
  • 28
  • 28
4

You can implement your own:

public static string StringFormat(string format, IDictionary<string, string> values)
{
    foreach(var p in values)
        format = format.Replace("{" + p.Key + "}", p.Value);
    return format;
}
eulerfx
  • 36,769
  • 7
  • 61
  • 83
  • 6
    This loses a lot of the functionality of String.Format though. – Craig Jun 18 '09 at 00:15
  • Note with this function you cannot repeat the key multiple times. Example, `Hi {{Name}}, is your name really {{Name}}?` – Fred Apr 07 '15 at 13:42
  • I think @Fred is wrong. String.Replace will replace each {name} https://learn.microsoft.com/en-us/dotnet/api/system.string.replace?view=netframework-4.7.2 – Janne Harju Apr 09 '19 at 09:38
  • If you are expecting to use this often with longer strings, using `StringBuilder` yields better performance. – datchung Aug 21 '19 at 18:35
3

Phil Haack discussed several methods of doing this on his blog a while back: http://haacked.com/archive/2009/01/14/named-formats-redux.aspx. I've used the "Hanselformat" version on two projects with no complaints.

Sean Carpenter
  • 7,681
  • 3
  • 37
  • 38
  • I too liked the Hansel formatter. It does not depend on the System.Web, like the other formatters, which is great for inserting into my Utilities library. I like that i can take any object and format from it. – Valamas May 12 '14 at 23:47
2

It's possible now

With Interpolated Strings of C# 6.0 you can do this:

string name = "John";
string message = $"Hi {name}!";
//"Hi John!"
brett rogers
  • 6,501
  • 7
  • 33
  • 43
fabriciorissetto
  • 9,475
  • 5
  • 65
  • 73
  • 5
    This is not what is asked for. If you don't know what the field is called, you can't have a variable with that identifier ready. – Moberg Jan 21 '17 at 20:05
  • You can do `$"Hi {d["name"]}" which should match what the OP was asking – Juan Feb 09 '18 at 17:07
  • you can't parameterize the format string with String Interpolation. eg:var format = "Time is {DateTime.Now}"; Console.WriteLine($format); – Kasun Feb 08 '19 at 12:27
  • 1
    @Kasun how about format = $"Time is {DateTime.Now}"; You missed $ from beginning of string. – Janne Harju Apr 09 '19 at 07:54
1
static public class StringFormat
{
    static private char[] separator = new char[] { ':' };
    static private Regex findParameters = new Regex(
        "\\{(?<param>.*?)\\}",
        RegexOptions.Compiled | RegexOptions.Singleline);

    static string FormatNamed(
        this string format,
        Dictionary<string, object> args)
    {
        return findParameters.Replace(
            format,
            delegate(Match match)
            {
                string[] param = match.Groups["param"].Value.Split(separator, 2);

                object value;
                if (!args.TryGetValue(param[0], out value))
                    value = match.Value;

                if ((param.Length == 2) && (param[1].Length != 0))
                    return string.Format(
                        CultureInfo.CurrentCulture,
                        "{0:" + param[1] + "}",
                        value);
                else
                    return value.ToString();
            });
    }
}

A little more involved than the other extension method, but this should also allow non-string values and formatting patterns used on them, so in your original example:

Dictionary<string, object> d = new Dictionary<string, object>();
d["a"] = DateTime.Now;
string a = string.FormatNamed("{a:yyyyMMdd-HHmmss}", d);

Will also work...

jerryjvl
  • 19,723
  • 7
  • 40
  • 55
0

Since C# 6 released you are able to use String Interpolation feature

Code that solves your question:

string a = $"{d["a"]}{d["ba"]}{d["lol"]}";
Pavlo Neiman
  • 7,438
  • 3
  • 28
  • 28
0

Why a Dictionary? It's unnecessary and overly complicated. A simple 2 dimensional array of name/value pairs would work just as well:

public static string Format(this string formatString, string[,] nameValuePairs)
{
    if (nameValuePairs.GetLength(1) != 2)
    {
        throw new ArgumentException("Name value pairs array must be [N,2]", nameof(nameValuePairs));
    }
    StringBuilder newFormat = new StringBuilder(formatString);
    int count = nameValuePairs.GetLength(0);
    object[] values = new object[count];
    for (var index = 0; index < count; index++)
    {
        newFormat = newFormat.Replace(string.Concat("{", nameValuePairs[index,0], "}"), string.Concat("{", index.ToString(), "}"));
        values[index] = nameValuePairs[index,1];
    }
    return string.Format(newFormat.ToString(), values);
}

Call the above with:

string format = "{foo} = {bar} (really, it's {bar})";
string formatted = format.Format(new[,] { { "foo", "Dictionary" }, { "bar", "unnecessary" } });

Results in: "Dictionary = unnecessary (really, it's unnecessary)"

Dan Sorak
  • 141
  • 6
  • Is there a reason to replace the key by an int, and then doing a string format, instead of replacing directly the key by the value ? – Persi Jul 10 '20 at 23:16
  • Scenario: ETL transforms converting data pushed from a database to an API. DB data is converted to dictionaries to serialize into JSON for the API. I can create configurable templates like this to add new fields to the outgoing data. `templates.Add("newField", "{id}: {name_short}, {position}");` `for (var kvp in templates) record.Add(kvp.Key, kvp.Value.FormatFromDictionary(record));`. – jwatts1980 Apr 13 '22 at 03:48
0
public static string StringFormat(this string format, IDictionary<string, object> values)
{
    return Regex.Matches(format, @"\{(?!\{)(.+?)\}")
            .Select(m => m.Groups[1].Value)
            .Aggregate(format, (current, key) =>
            {
                string[] splits = key.Split(":");
                string replacement = splits.Length > 1
                    ? string.Format($"{{0:{splits[1]}}}", values[splits[0]])
                    : values[key].ToString();
                return Regex.Replace(current, "(.|^)("+ Regex.Escape($"{{{key}}}")+")(.|$)", 
                                     m => m.Groups[1].ToString() == "{" && m.Groups[3].ToString() == "}"
                                        ? m.Groups[2].ToString()
                                        : m.Groups[1] + replacement + m.Groups[3]
                                    );
            });
}

This is similar to another answer, but it considers escaping with {{text}}.

Deloo
  • 96
  • 6
0

I took @LPCRoy's answer and refactored for generic dictionary types, added parameter validation, it only replaces fields it can find, and attempts to solve the "double curly brace" issue by escaping them first, then replacing them at the end with single braces in the same way that string.Format() does.

public static string FormatFromDictionary<K, T>(this string formatString, IDictionary<K, T> valueDict)
{
    if (string.IsNullOrWhiteSpace(formatString)) return formatString;
    if (valueDict == null || !valueDict.Any()) return formatString;

    bool escapedDoubleCurlyBraces = false;
    if (formatString.Contains("{{") || formatString.Contains("}}"))
    {
        formatString = formatString.Replace("{{", "\\{\\{").Replace("}}", "\\}\\}");
        escapedDoubleCurlyBraces = true;
    }

    int i = 0;
    StringBuilder newFormatString = new StringBuilder(formatString);
    Dictionary<K, int> keyToInt = new Dictionary<K, int>();
    string field;

    //StringBuilder.Replace() is faster than string.Replace().
    foreach (var kvp in valueDict)
    {
        field = "{" + kvp.Key.ToString() + "}";
        if (formatString.Contains(field))
        {
            newFormatString = newFormatString.Replace(field, "{" + i.ToString() + "}");
            keyToInt.Add(kvp.Key, i);
            i++;
        }
    }

    //Any replacements to make?
    if (keyToInt.Any())
    {
        formatString = string.Format(
            newFormatString.ToString(),
            keyToInt.OrderBy(kvp => kvp.Value).Select(kvp => (object)valueDict[kvp.Key]).ToArray()
        );
    }
    if (escapedDoubleCurlyBraces)
    {
        //Converts "{{" and "}}" to single "{" and "}" for the final result just like string.format().
        formatString = formatString.Replace("\\{\\{", "{").Replace("\\}\\}", "}");
    }

    return formatString;
}
jwatts1980
  • 7,254
  • 2
  • 28
  • 44
-2

Here is a nice solution that is very useful when formatting emails: http://www.c-sharpcorner.com/UploadFile/e4ff85/string-replacement-with-named-string-placeholders/

Edited:

public static class StringExtension  
{  
    public static string Format( this string str, params Expression<Func<string,object>>[] args)  
    {  
        var parameters = args.ToDictionary( e=>string.Format("{{{0}}}",e.Parameters[0].Name), e=>e.Compile()(e.Parameters[0].Name));  

        var sb = new StringBuilder(str);  
        foreach(var kv in parameters)  
        {  
            sb.Replace( kv.Key, kv.Value != null ? kv.Value.ToString() : "");  
        }  

        return sb.ToString();  
    }  
}

Example usage:

public string PopulateString(string emailBody)  
{  
  User person = _db.GetCurrentUser();  
  string firstName = person.FirstName;    //  Peter  
  string lastName = person.LastName;      //  Pan  
  return StringExtension.Format(emailBody.Format(  
    firstname => firstName,  
    lastname => lastName  
  ));   
} 
danpop
  • 967
  • 13
  • 25
-3

(your Dictionary + foreach + string.Replace) wrapped in a sub-routine or extension method?

Obviously unoptimized, but...

micahtan
  • 18,530
  • 1
  • 38
  • 33