Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / string

Custom String FormatWith using Reflection

5.00/5 (2 votes)
9 Jul 2012CPOL1 min read 12.1K  
This is an alternative for Custom String FormatWith using Reflection

Background

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:

C#
 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!)

C#
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';              // **very** unlikely
    char substRightDouble = substLeftDouble;  // initially equal
    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 = str.Replace("{" + pnv.Name, "{" + index.ToString(CultureInfo.InvariantCulture));
      str = Regex.Replace(str, "{" + pnv.Name + @"\b", "{" + index.ToString(CultureInfo.InvariantCulture));
      index++;
    }
    if (substRightDouble != substLeftDouble)  // if they differ, then we need to handle this case
    {
      str = str.Replace(new string(substLeftDouble, 1), "{{").Replace(new string(substRightDouble, 1), "}}");
    }
    // this depends on the Select enumerating in the same order as foreach
    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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)