Named string formatting in C#

2019-01-01 07:50发布

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.

18条回答
素衣白纱
2楼-- · 2019-01-01 08:31

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.

查看更多
回忆,回不去的记忆
3楼-- · 2019-01-01 08:32

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.

查看更多
浮光初槿花落
4楼-- · 2019-01-01 08:32
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."

查看更多
初与友歌
5楼-- · 2019-01-01 08:33

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!

查看更多
旧人旧事旧时光
6楼-- · 2019-01-01 08:34
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 }

查看更多
与风俱净
7楼-- · 2019-01-01 08:35

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.

查看更多
登录 后发表回答