Reputation: 1412
I have a class of allowed substitution values:
class MaskDictionary
{
public int id { get; set; }
public string last { get; set; }
public string lastinitial { get; set; }
public string first { get; set; }
public string firstinitial { get; set; }
public string salutation { get; set; }
public DateTime today { get; set; }
}
and I want to take a formatting string as user input, like:
string userFormat = "{last}, {first} {today}";
and generate the interpolated value. Conceptually similar to:
string.Format("{last}, {first} {today}", MaskDictionary);
but making the input string dynamic fails:
string.Format(userFormat, MaskDictionary);
What is a simple, clean way to provide runtime formatting?
There are some clunky options that use reflection and recursive replaces, like
string userFormat = "{last}, {first} {today}";
PropertyInfo[] properties = typeof(MaskDictionary).GetProperties();
foreach (PropertyInfo property in properties)
{
userFormat = string.Replace(property.name, property.GetValue(mask));
}
but there has to be a better way.
--update with comparison of answers--
I tested the two proposed solutions in the answers for performance and got pretty surprising results.
static class Format2
{
static public string Format(string format, MaskDictionary md)
{
string val = format;
foreach (PropertyInfo property in typeof(MaskDictionary).GetProperties())
{
val = val.Replace("{" + property.Name + "}", property.GetValue(md).ToString());
}
return val;
}
}
static class Format1
{
public static string FormatWith(this string format, IFormatProvider provider, object source)
{
if (format == null)
throw new ArgumentNullException("format");
Regex r = new Regex(@"(?<start>\{)+(?<property>[\w\.\[\]]+)(?<format>:[^}]+)?(?<end>\})+",
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
List<object> values = new List<object>();
string rewrittenFormat = r.Replace(format, delegate (Match m)
{
Group startGroup = m.Groups["start"];
Group propertyGroup = m.Groups["property"];
Group formatGroup = m.Groups["format"];
Group endGroup = m.Groups["end"];
values.Add((propertyGroup.Value == "0")
? source
: DataBinder.Eval(source, propertyGroup.Value));
return new string('{', startGroup.Captures.Count) + (values.Count - 1) + formatGroup.Value
+ new string('}', endGroup.Captures.Count);
});
return string.Format(provider, rewrittenFormat, values.ToArray());
}
}
The Regex solution is slower, much slower. Using 1000 iterations of a short format string (20 characters, 3 replacements) with a 5 property dictionary object and a 105 property dictionary object, and a long format string (2000 characters, 3 replacements) and a long dictionary object, I got to following results:
Short format, small dictionary
Regex - 2150 ms
Replace - 3 ms
Short format, large dictionary
Regex - 2160 ms
Replace - 30 ms
Long format, short dictionary
Regex - 2170 ms
Replace - 26 ms
Long format, large dictionary
Regex - 2250 ms
Replace - 330 ms
Replace doesn't scale as well with a large dictionary, but it starts so much faster that it takes a large dictionary plus a very long format string to be slower. With the 105 property dictionary, it took about 16,000 character format string to take the same amount of time to process, ~2500ms. With the 5 property small dictionary, the regex was never as fast. A 600K character format string took 14000 ms for regex and 7000 ms for replace and a 1.7M character format string took 38000 ms vs 21000 ms. Replace wins as long as the dictionary object is reasonably sized and the format string is shorter than 80 pages.
Upvotes: 2
Views: 858
Reputation: 2020
I have a library called FormatWith that does this with string extension methods (FormatWith()
). It's similar to James Newton King's implementation, but has some advantages:
No regex. The extensions use a state machine parser to handle the input string, which is faster and handles escaped brackets correctly.
Does not rely on DataBinder. DataBinder is not available on .NET Core and is also dangerous when used with unsanitized input.
Works with anything that implements .NET Standard 2.0, so it can be used in .NET Core and ASP.NET Core applications.
Upvotes: 2
Reputation: 1808
You could convert your code into a ToString(...) function:
using System.Reflection;
using System.ComponentModel;
class MaskDictionary
{
// ... properties ...
public string ToString(string format)
{
string val = format;
foreach (PropertyInfo property in typeof(MaskDictionary).GetProperties())
{
val = val.Replace("{" + property.Name + "}", property.GetValue(this).ToString());
}
return val;
}
}
Edit: here's a version that lets you rename the user format tags without renaming the property:
class MaskDictionary
{
// ... properties ...
[DisplayName("bar")]
public string foo {get;set;}
public int baz {get;set;}
public string ToString(string format)
{
string val = format;
foreach (PropertyInfo property in typeof(MaskDictionary).GetProperties())
{
var dispAttr = (DisplayNameAttribute)Attribute.GetCustomAttribute(property, typeof(DisplayNameAttribute));
string pName = dispAttr != null ? dispAttr.DisplayName : property.Name;
val = val.Replace("{" + pName + "}", property.GetValue(this).ToString());
}
return val;
}
}
usage:
var m = new MaskDictionary();
m.foo = "hello";
m.baz = 111;
Console.WriteLine(m.ToString("{foo} {bar} {baz}"));
//output: {foo} hello 111
Upvotes: 1
Reputation: 76547
James Newton King (the JSON guy) uses a FormatWith()
extension method defined in this blog post that would essentially accomplish what you are attempting to do :
public static string FormatWith(this string format, IFormatProvider provider, object source)
{
if (format == null)
throw new ArgumentNullException("format");
Regex r = new Regex(@"(?<start>\{)+(?<property>[\w\.\[\]]+)(?<format>:[^}]+)?(?<end>\})+",
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
List<object> values = new List<object>();
string rewrittenFormat = r.Replace(format, delegate(Match m)
{
Group startGroup = m.Groups["start"];
Group propertyGroup = m.Groups["property"];
Group formatGroup = m.Groups["format"];
Group endGroup = m.Groups["end"];
values.Add((propertyGroup.Value == "0")
? source
: DataBinder.Eval(source, propertyGroup.Value));
return new string('{', startGroup.Captures.Count) + (values.Count - 1) + formatGroup.Value
+ new string('}', endGroup.Captures.Count);
});
return string.Format(provider, rewrittenFormat, values.ToArray());
}
It basically relies on Regular Expressions along with the .NET Databinder class to handle performing the actual matching and replacements.
Upvotes: 2
Reputation: 152556
There's nothing in the framework, but there's a clever extension method that you could use to inject properties into your format string by name:
string result = "{last}, {first} {today}".FormatWith(MaskDictionary);
The closest you can get woithout an extension is to use string interpolation in C#6:
string result = $"{MaskDictionary.last}, {MaskDictionary.first} {MaskDictionary.today}";
Upvotes: 1