Background
Sumit Chawla posted a Tip that was pretty cool, but forfeited the value-formatting ability of string.Format().
I thought there ought to be a way to accomplish both.
Using the code
This alternative is called in the same manner as the original Tip. I added an overload that takes an IFormatProvider
to inform the formatting. This example is extended from the original Tip:
UserInformation user = new UserInformation {
FirstName = "Joe",
LastName = "Doe",
Address1 = "Joe's House",
City = "San Jose",
Zipcode = "94101",
Email = "joe@doe.com",
PhoneNumber = 408000000
};
var userInfoXml = @"
<userinfo>
<firstname>{FirstName,15}</firstname>
<lastname>{LastName,-15}</lastname>
<email>{{{Email}}}</email>
<phone>{{PhoneNumber}}--{PhoneNumber:N0}</phone>
</userinfo>";
Console.WriteLine(userInfoXml.FormatWithObject(user));
Which displays:
<userinfo>
<firstname> Joe</firstname>
<lastname>Doe </lastname>
<email>{joe@doe.com}</email>
<phone>{PhoneNumber}--408,000,000</phone>
</userinfo>
All of the normal Composite Formatting should work correctly.
Implementation
I changed this to rewrite the input string as a normal composite formatting string and then just called string.Format()
.
I simplified the construction of propertyNamesAndValues
.
There is special case checking for the doubled curley braces ({{
or }}
) appearing in the input string. They are subtituted out for characters that are "safe" (i.e. don't appear in the string) before rewriting the formatting string, and are restored before calling string.Format()
. See the {{{Email}}}
and {{PhoneNumber}}
above. (This can be comparatively expensive!)
public static class CustomStringFormattingExtensionMethods
{
public static string FormatWithObject(this string str, object o)
{
return FormatWithObject(str, o, CultureInfo.CurrentCulture);
}
public static string FormatWithObject(this string str, object o, IFormatProvider formatProvider)
{
if (o == null)
return str;
var propertyNamesAndValues = o.GetType()
.GetProperties()
.Where(pi => pi.CanRead)
.Select(pi => new {
pi.Name,
Value = pi.GetValue(o, null)
});
char substLeftDouble = '\0';
char substRightDouble = substLeftDouble;
if (str.Contains("{{") || str.Contains("}}"))
{
var strAndDigits = "0123456789" + str;
while (strAndDigits.Contains(++substLeftDouble));
substRightDouble = substLeftDouble;
while (strAndDigits.Contains(++substRightDouble));
str = Regex.Replace(str, "{{", new string(substLeftDouble, 1));
str = Regex.Replace(str, "}}", new string(substRightDouble, 1), RegexOptions.RightToLeft);
}
var index = 0;
foreach (var pnv in propertyNamesAndValues)
{
str = Regex.Replace(str, "{" + pnv.Name + @"\b", "{" + index.ToString(CultureInfo.InvariantCulture));
index++;
}
if (substRightDouble != substLeftDouble)
{
str = str.Replace(new string(substLeftDouble, 1), "{{").Replace(new string(substRightDouble, 1), "}}");
}
return string.Format(formatProvider, str, propertyNamesAndValues.Select(p => p.Value).ToArray());
}
}
Points of Interest
I started by using Regex heavily and kept refining it to simpler forms. Dealing with the doubled braces is kind of ugly, but I couldn't think of something simpler. strAndDigits
is necessary because we're about to put numbers into the formatting string and must avoid using digits as the substitution characters.
This probably will not behave well if any property name is a proper prefix of another property name. This was fixed by changing the str.Replace()
in the foreach
to Regex.Replace()
and adding the "\b" word boundary anchor to the match pattern.
History
- July 7, 2012 Initial posting.
- July 8, 2012 Updated to actually USE the IFormatProvider argument.
- July 9, 2012 Fixed to work correctly if a property name is a proper prefix of another property name.