Fun With Named Formats, String Parsing, and Edge Cases

code, format 0 comments suggest edit

TRIPLE UPDATE! C# now has string interpolation which pretty much makes this post unnecessary and only interesting as a fun coding exercise.

DOUBLE UPDATE! Be sure to read Peli’s post in which he explores all of these implementations using PEX. Apparently I have a lot more unit tests to write in order to define the expected behavior of the code.

UPDATE: By the way, after you read this post, check out the post in which I revisit this topic and add two more implementations to check out.

Recently I found myself in a situation where I wanted to format a string using a named format string, rather than a positional one. Ignore for the moment the issue on whether this is a good idea or not, just trust me that I’ll be responsible with it.

The existing String.Format method, for example, formats values according to position.

string s = string.Format("{0} first, {1} second", 3.14, DateTime.Now);

But what I wanted was to be able to use the name of properties/fields, rather than position like so:

var someObj = new {pi = 3.14, date = DateTime.Now};
string s = NamedFormat("{pi} first, {date} second", someObj);

Looking around the internet, I quickly found three implementations mentioned in this StackOverflow question.

All three implementations are fairly similar in that they all use regular expressions for the parsing. Hanselman’s approach is to write an extension method of object (note that this won’t work in VB.NET until they allow extending object). James and Oskar wrote extension methods of the string class. James takes it a bit further by using DataBinder.Eval on each token, which allows you to have formats such as {foo.bar.baz} where baz is a property of bar which is a property of foo. This is something else I wanted, which the others do not provide.

He also makes good use of the MatchEvaluator delegate argument to the Regex.Replace method, perhaps one of the most underused yet powerful features of the Regex class. This ends up making the code for his method very succinct.

Handling Brace Escaping

I hade a chat about this sort of string parsing with Eilon recently and he mentioned that many developers tend to ignore or get escaping wrong. So I thought I would see how these methods handle a simple test of escaping the braces.

String.Format with the following:

Console.WriteLine(String.Format("{{{0}}}", 123));

produces the output (sans quotes) of “{123}”

So I would expect with each of these methods, that:

Console.WriteLine(NamedFormat("{{{foo}}}", new {foo = 123}));

Would produce the exact same output, “{123}”. However, only James’s method passed this test. But when I expanded the test to the following format, “{{{{{foo}}}}}”, all three failed. That should have produced “{{123}}”.

Certainly this is not such a big deal as this really is an edge case, but you never know when an edge case might bite you as Zune owners learned recently. More importantly, it poses an interesting problem - how do you handle this correctly? I thought it would be fun to try.

This is possible to handle correctly using regular expressions, but it’s challenging. Not only are you dealing with balanced matching, but the matching depends on whether the number of consecutive braces are odd or even.

For example, the following “{0}}” is not valid because the right end brace is escaped. However, “{0}}}” is valid. The expression is closed with the leftmost end brace, which is followed by an even number of consecutive braces, which means they are all escaped sequences.

Performance

As I mentioned earlier, only James’s method handles evaluation of sub-properties/fields via the use of the DataBinder.Eval method. Critics of his blog post point out that this is a performance killer.

Personally, until I’ve measured it in the scenarios in which I plan to use it, I doubt that the performance will really be an issue compared to everything else going on. But I thought I would check it out anyways, writing a simple console app which runs each method over 1000 iterations, and then divides by 1000 to get the number of milliseconds each method takes. Here’s the result:

format
perf

Notice that James’s method is 43 times slower than Hanselman’s. Even so, it only takes 4.4 milliseconds. So if you don’t use it in a tight loop with a lot of iterations, it’s not horrible, but it could be better.

My Implementation

At this point, I thought it would be fun to write my own implementation using manual string parsing rather than regular expressions. I’m not sure my regex-fu is capable of handling the challenges I mentioned before. After implementing my own version, I ran the performance test and saw the following result.

haackformat
perf

Nice! by removing the overhead of using a regular expression in this particular case, my implementation is faster than the other implementations, despite my use of DataBinder.Eval. Hopefully my implementation is correct, because fast and wrong is even worse than slow and right.

One drawback to not using regular expressions is that the code for my implementation is a bit long. I include the entire source here. I’ve also zipped up the code for this solution which includes unit tests as well as the implementations of the other methods I tested, so you can see which tests they pass and which they don’t pass.

The core of the code is in two parts. One is a private method which parses and splits the string into an enumeration of segments represented by the ITextExpression interface. The method you call joins these segments together, evaluating any expressions against a supplied object, and returning the resulting string.

I think we could optimize the code even more by joining these operations into a single method, but I really liked the separation between the parsing and joining logic as it helped me wrap my head around it. Initially, I hoped that I could cache the parsed representation of the format string since strings are immutable thus I could re-use it. But it didn’t end up giving me any real performance gain when I measured it.

public static class HaackFormatter
{
  public static string HaackFormat(this string format, object source)
  {

    if (format == null) {
        throw new ArgumentNullException("format");
    }

    var formattedStrings = (from expression in SplitFormat(format)
                 select expression.Eval(source)).ToArray();
    return String.Join("", formattedStrings);
  }

  private static IEnumerable<ITextExpression> SplitFormat(string format)
  {
    int exprEndIndex = -1;
    int expStartIndex;

    do
    {
      expStartIndex = format.IndexOfExpressionStart(exprEndIndex + 1);
      if (expStartIndex < 0)
      {
        //everything after last end brace index.
        if (exprEndIndex + 1 < format.Length)
        {
          yield return new LiteralFormat(
              format.Substring(exprEndIndex + 1));
        }
        break;
      }

      if (expStartIndex - exprEndIndex - 1 > 0)
      {
        //everything up to next start brace index
        yield return new LiteralFormat(format.Substring(exprEndIndex + 1
          , expStartIndex - exprEndIndex - 1));
      }

      int endBraceIndex = format.IndexOfExpressionEnd(expStartIndex + 1);
      if (endBraceIndex < 0)
      {
        //rest of string, no end brace (could be invalid expression)
        yield return new FormatExpression(format.Substring(expStartIndex));
      }
      else
      {
        exprEndIndex = endBraceIndex;
        //everything from start to end brace.
        yield return new FormatExpression(format.Substring(expStartIndex
          , endBraceIndex - expStartIndex + 1));

      }
    } while (expStartIndex > -1);
  }

  static int IndexOfExpressionStart(this string format, int startIndex) {
    int index = format.IndexOf('{', startIndex);
    if (index == -1) {
      return index;
    }

    //peek ahead.
    if (index + 1 < format.Length) {
      char nextChar = format[index + 1];
      if (nextChar == '{') {
        return IndexOfExpressionStart(format, index + 2);
      }
    }

    return index;
  }

  static int IndexOfExpressionEnd(this string format, int startIndex)
  {
    int endBraceIndex = format.IndexOf('}', startIndex);
    if (endBraceIndex == -1) {
      return endBraceIndex;
    }
    //start peeking ahead until there are no more braces...
    // }}}}
    int braceCount = 0;
    for (int i = endBraceIndex + 1; i < format.Length; i++) {
      if (format[i] == '}') {
        braceCount++;
      }
      else {
        break;
      }
    }
    if (braceCount % 2 == 1) {
      return IndexOfExpressionEnd(format, endBraceIndex + braceCount + 1);
    }

    return endBraceIndex;
  }
}

And the code for the supporting classes

public class FormatExpression : ITextExpression
{
  bool _invalidExpression = false;

  public FormatExpression(string expression) {
    if (!expression.StartsWith("{") || !expression.EndsWith("}")) {
      _invalidExpression = true;
      Expression = expression;
      return;
    }

    string expressionWithoutBraces = expression.Substring(1
        , expression.Length - 2);
    int colonIndex = expressionWithoutBraces.IndexOf(':');
    if (colonIndex < 0) {
      Expression = expressionWithoutBraces;
    }
    else {
      Expression = expressionWithoutBraces.Substring(0, colonIndex);
      Format = expressionWithoutBraces.Substring(colonIndex + 1);
    }
  }

  public string Expression { 
    get; 
    private set; 
  }

  public string Format
  {
    get;
    private set;
  }

  public string Eval(object o) {
    if (_invalidExpression) {
      throw new FormatException("Invalid expression");
    }
    try
    {
      if (String.IsNullOrEmpty(Format))
      {
        return (DataBinder.Eval(o, Expression) ?? string.Empty).ToString();
      }
      return (DataBinder.Eval(o, Expression, "{0:" + Format + "}") ?? 
        string.Empty).ToString();
    }
    catch (ArgumentException) {
      throw new FormatException();
    }
    catch (HttpException) {
      throw new FormatException();
    }
  }
}

public class LiteralFormat : ITextExpression
{
  public LiteralFormat(string literalText) {
    LiteralText = literalText;
  }

  public string LiteralText { 
    get; 
    private set; 
  }

  public string Eval(object o) {
    string literalText = LiteralText
        .Replace("{{", "{")
        .Replace("}}", "}");
    return literalText;
  }
}

I mainly did this for fun, though I plan to use this method in Subtext for email fomatting.

Let me know if you find any situations or edge cases in which my version fails. I’ll probably be adding more test cases as I integrate this into Subtext. As far as I can tell, it handles normal formatting and brace escaping correctly.

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

47 responses

  1. Avatar for Lance Fisher
    Lance Fisher January 4th, 2009

    Nice work on this. I've looked into this before, and found the same stackoverflow question and solutions. Way to take it a step further!
    You mentioned that renaming could break the formatter, but if you use the anonymous object approach as a mapper, it shouldn't be a problem. e.g.
    var person = repo.FetchPerson(1);
    string s = NamedFormat(
    "First name: {firstName}, Last name: {lastName}",
    new {firstName=person.FirstName, lastName=person.LastName});
    This won't break the output if the FirstName or LastName properties names change. Granted, this is a little more cumbersome than just passing the Person object to NamedFormat, but it gives you the refactoring protection if you want it.

  2. Avatar for haacked
    haacked January 4th, 2009

    @Lance good point! I actually mentioned that approach in a Tweet a few minutes ago! :)
    I also just updated the source code to refactor the perf tests to clean them up a bit. They weren't very DRY. ;)

  3. Avatar for Aaron Powell
    Aaron Powell January 4th, 2009

    Wow, seems like every man and his dog is doing something such as this at the moment :P
    I did a similar concept to this (closest to Scott's though) for email templating - www.aaron-powell.com/.../...o-email-templates.aspx
    Just an example of how to use this for another practical solution :)

  4. Avatar for haacked
    haacked January 4th, 2009

    Ok, I found a bug in the code and fixed it. If you downloaded it before this comment, please re-download. :)

  5. Avatar for James Newton-King
    James Newton-King January 4th, 2009

    I remember explicitly writing a unit test to make sure that triple braces would work. The scenarios beyond that stretched a little bit far outside my regex comfort zone :)
    Good to see a solution that combines the best of all our approaches!

  6. Avatar for Paulo Morgado
    Paulo Morgado January 4th, 2009

    Have you thought of using a mix of named and place holder?
    string.NamedFormat("First Name: {0.FirstName}\nLast Name: {0.LastName}", person)
    It would be nice (also for string.Format) to generate an executable template. Something to be used like this:
    var executableTemplate = new ExecutableTemplate("First Name: {0.FirstName}\nLast Name: {0.LastName}");
    var text = executableTemplate.ToString(person);

  7. Avatar for Giorgio Turrini
    Giorgio Turrini January 4th, 2009

    Good Work!
    There is only one thing missing, support for different cultures, all the tests are wrong if you are an italian and comma is the digit separator :-)

  8. Avatar for Stephen
    Stephen January 4th, 2009

    What, not type safe yet? :P f#'s format string is clever!

  9. Avatar for Atif Aziz
    Atif Aziz January 4th, 2009

    Phil, the main cost in James's implementation comes from, believe it or not, instantiating the Regex object on each call. If you factor out this cost, you'll find that it performs far better than implemenations from Hanselman and Oskar and only marginally less than your hand-made version.
    The obvious way to factor out the Regex instantiation cost is to do it once during type construction and tuck it away into a static field that is then re-used in FormatWith. The less obvious way would be to simply use the static version of Regex.Replace and do away with new-ing a Regex object on each call. The static methods on Regex use an internal cache whereas the using the constructor bypasses the cache (more on this later).
    Here are the numbers from my machine to prove the case. First off, here's just downloading and running NamedStringFormatConsole from your solution.
    Hanselformat took 0.3042 ms
    OskarFormat took 0.2934 ms
    JamesFormat took 11.3133 ms
    HaackFormat took 0.1373 ms
    I then modified JamesFormatters.FormatWith to use the static Regex.Replace method so that the cache gets involved. Here are the numbers with this change:
    Hanselformat took 0.2765 ms
    OskarFormat took 0.3137 ms
    JamesFormat took 0.1699 ms
    HaackFormat took 0.1373 ms
    Given this, your version is around only 20% faster than the next fastest version, which now happens to be JamesFormatter. 20% could still mean a lot for a very tight loop, but what I wanted to point out is that the real cost is not using regular expression parsing as such and therefore may not warrant the need to hand-write the parsing logic and create further abstractions. What one can take away from all this dialogueis that DataBinder.Eval and regular expressions can give you a lot of mileage while keeping the code simple before any need for optimization may arise.
    Finally, see Regex Class Caching Changes between .NET Framework 1.1 and .NET Framework 2.0 [Josh Free] for more information on the caching behavior of the Regex class as well as how that behavior was changed for the constructors to bypass the cache between 1.1 and 2.0.

  10. Avatar for Atif Aziz
    Atif Aziz January 4th, 2009

    Here's the updated JamesFormatter based on the previous comment:
    JamesFormatter.cs

  11. Avatar for Tuna Toksoz
    Tuna Toksoz January 4th, 2009

    Phil, is there any specific reason that you're not using any template engine for email formatting? other than speed? I am using template engines for this purpose and it works good.

  12. Avatar for Peli
    Peli January 4th, 2009

    Hi Haacked,
    Nice to package your solution for us. I could not resist to run Pex on your little formatter :). Try this format string (without the quotes), you'll be surprised:
    "{(\')}" -> ArgumentOutOfRangeException
    Jokes aside, I don't think it is a good advice to advocate this kind of solution. If you don't know how to use String.Format, use StringBuilder instead. IMO, using Regex for parsing is a smell (unless you are a regex expert) as it hides a lot of complexity behind it.
    In Pex, we had to implemented the same kind of feature to render objects using the DebuggerDisplayAttribute format string. Since that format string never changes, we can agressively compile it into a dynamic method that calls into a StringBuilder.

  13. Avatar for Peli
    Peli January 4th, 2009

    Just to put those numbers in perspective, I wrote 2 more formatters:
    Func<string> stringBuilder = () =>
    {
    var sb = new StringBuilder();
    sb.Append(o.foo);
    sb.Append(" is a ");
    sb.Append(o.bar);
    sb.Append(" is a ");
    sb.Append(o.baz);
    sb.Append(" is a ");
    sb.Append(o.qux.ToString("#.#"));
    return sb.ToString();
    };
    And the results are the following:
    StringFormat took 0.0014 ms
    StringBuilder took 0.001 ms
    Hanselformat took 0.0747 ms
    OskarFormat took 0.0934 ms
    JamesFormat took 3.7797 ms
    HaackFormat took 0.0492 ms
    This should give you

  14. Avatar for Peli
    Peli January 4th, 2009

    Previous got cut. Here's the second formatter:
    Func<string> stringformat = () =>
    {
    return String.Format("{0} is a {1} is a {2} is a {3:#.#}",
    o.foo, o.bar, o.baz, o.qux);
    };
    The number are pretty much self-explanatory (altough the StringBuilder time is too small and probably has a large error margin): all hand made implementation are significantly slower.

  15. Avatar for Matt
    Matt January 4th, 2009

    You have also those tiny examples stackoverflow.com/.../c-named-parameters-to-a-s...

  16. Avatar for Filip Ekberg
    Filip Ekberg January 4th, 2009

    Giorgio Turrini, you could easily change the code to support other cultures and other types of delimiters.
    Nicely done Phil, nicely done!

  17. Avatar for Oskar Austegard
    Oskar Austegard January 4th, 2009

    Great. Now I feel an irrepressible urge to integrate your changes (and those in the comments) into my InjectSingleValue method, which drives the whole enchilada.
    Don't you know I have real work to do here?
    ;-p
    Seriously - thanks for being worthy of consideration in your tests, even though I now appear to finish 4th...

  18. Avatar for Mark Cidade
    Mark Cidade January 4th, 2009

    This version uses parameters of lambda expressions for named arguments:

    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();
    }

    With the above extension you can write this:
    var str = "{foo} {bar} {baz}".Format(foo=>foo, bar=>2, baz=>new object());
    and you'll get `"foo 2 System.Object`".

  19. Avatar for haacked
    haacked January 4th, 2009

    @Oskar the performance aspect is not a competition. It's the correctness within reasonable performance boundaries I'm concerned about.

  20. Avatar for haacked
    haacked January 4th, 2009

    @Atif good point about the regex creation perf cost. That brings his method up to speed so its more usable, but it still doesn't pass all my unit tests.

  21. Avatar for haacked
    haacked January 4th, 2009

    @Peli interestingly enough, that ArgumentException is thrown by DataBinder.Eval, not my code. I updated the code to catch that and throw a FormatException.

  22. Avatar for joe
    joe January 4th, 2009

    I like this a lot. I think with a little playing around it would make for a good mvc view engine in a cms system I am working on.

  23. Avatar for Brannon
    Brannon January 5th, 2009

    I've used a similar method for formatting emails as well, but ended up converting to NVelocity because I needed conditional and iteration support. NVelocity also handles simple expressions. FWIW, I used the NVelocity included with MonoRail (svn.castleproject.org:8080/.../NVelocity), since the main NVelocity project looks dead.

  24. Avatar for Paco
    Paco January 5th, 2009

    Nice job!
    Let's extend it with interpolation now!
    docs.codehaus.org/display/BOO/String+Interpolation

  25. Avatar for James Newton-King
    James Newton-King January 5th, 2009

    Well that's something I didn't know: Using the Regex constructor doesn't cache and reuse the compiled expression.
    Thanks Atif.

  26. Avatar for -
    - January 5th, 2009

    did you consider using pex to test for ALL possible inputs?
    http://research.microsoft.com/en-us/projects/Pex/

  27. Avatar for Scott
    Scott January 5th, 2009

    You should probably test a small line and then test a large line. Times do change with the bigger tests...

  28. Avatar for Niki
    Niki January 5th, 2009

    Why not use a templating libary like StringTemplate (http://www.stringtemplate.org)?
    var template = new StringTemplate("$x.pi$ first, $x.date$ second");
    template.SetAttribute("x", someObj);
    It's way more powerful than that, it can do conditionals and loops and it's based on a genuine parser, so it doesn't have the regex problems you describe. I use it all the time.

  29. Avatar for Atif Aziz
    Atif Aziz January 5th, 2009

    Phil, the following updated version also passes all your current tests along with the performance enhancement mentioned earlier:
    JamesFormatter.cs
    The two changes needed were to convert HttpException to FormatException in case of a missing property and to cater for escape cases.

  30. Avatar for Andrew
    Andrew January 5th, 2009

    What about something like this:

    internal static class ObjectFormatter {
    internal static string Format(this object o, string format) {
    if (string.IsNullOrEmpty(format)) throw new ArgumentNullException("format");
    const string propertyPattern = "{(?<PropertyName>[a-z,0-9,_]*)}";
    MatchCollection matches = Regex.Matches(format, propertyPattern, RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
    foreach (Match match in matches) {
    string propertyName = match.Groups["PropertyName"].Value;
    PropertyInfo propertyInfo = o.GetType().GetProperty(propertyName);
    if (propertyInfo != null) {
    object propertyObject = propertyInfo.GetValue(o, null);
    string propertyValue = null;
    if (propertyObject != null) {
    propertyValue = propertyObject.ToString();
    }
    format = format.Replace(match.Value, propertyValue);
    }
    }
    return format;
    }
    }

  31. Avatar for Dave Reed
    Dave Reed January 6th, 2009

    We had a pretty sophisticated parser that did this at a previous company of mine. It too used manual parsing for the best performance. An interesting enhancement it had was that it supported an inline if style syntax so you could deal with null values and non-null values differently. For example, you wanted to format a contact's BirthDate field but Contact may be null, so {Contact.BirthDate} is going to cause problems. You did it something like this: {?Contact:{BirthDate:MM/dd/yyyy}}{!Contact:N/A}. Dealing with brace escaping with these nested expressions was just way too hairy for a regex. The whole thing was critical to operations -- administrators configured the site with the format string the client wanted, which the developers couldn't possibly know ahead of time.

  32. Avatar for Richard
    Richard January 8th, 2009

    Atif,
    Your rewritten version of the JamesFormatter fails with a format string of "{{{foo}" - the rewrittenFormat variable contains the same string, which then throws a FormatException. Calling string.Format("{{{0}", 123) returns "{123" as expected.
    If you change line 41 from:
    return openings > closings || openings % 2 == 0
    to:
    return openings % 2 == 0
    it seems to work.

  33. Avatar for Alski
    Alski January 8th, 2009

    I note this is exactly the same syntax as the [DebuggerDisplayAttribute()]. I've been thinking about implementing this recently, now I don't have to. Thanks

  34. Avatar for Andy
    Andy January 9th, 2009

    Hmmm... I thought this was my idea. :] My own version though, was able to do this in 0.015 milliseconds (on my slow old laptop). It relies on String.Format to worry about the brackets and format parameters. It's not perfect, but it short, sweet, and to the point... and fast.
    Private _props As New Dictionary(Of Integer, Reflection.PropertyInfo())
    <Extension()> _
    Public Function GetCachedProperties(ByVal type As Type) As Reflection.PropertyInfo()
    Dim id As Integer = type.GetHashCode
    If _props.ContainsKey(id) Then Return _props(id)
    Dim props = type.GetProperties()
    SyncLock _props
    _props.Add(id, props)
    End SyncLock
    Return props
    End Function
    ''' <summary>
    ''' Uses object's properties to format the string
    ''' (Ex. "The price is {Price:c2}" becomes "The price is $9,999.99").
    ''' </summary>
    <Extension()> _
    Public Function FormatWith(ByVal input As String, ByVal obj As Object) As String
    Dim vals As New List(Of Object)
    For Each prop In obj.GetType.GetCachedProperties()
    If input.Contains("{" & prop.Name, StringComparison.OrdinalIgnoreCase) Then
    input = input.Replace("{" & prop.Name, "{" & vals.Count, StringComparison.OrdinalIgnoreCase)
    vals.Add(prop.GetValue(obj, Nothing))
    End If
    Next
    Return String.Format(input, vals.ToArray())
    End Function

  35. Avatar for Zack
    Zack January 13th, 2009

    Hi Haacked,
    I Test all the methods you mentioned above and what I want to figure out is that in JamesFromater's performance problem is mainly because the incorrect use of the RegexOptions.Compiled. Just remove it , you will see the improvement.
    By the way, your method is still the fastest one. But I wonder if I can replace the DataBinder with sth else. Still working on it .....
    ^_^

  36. Avatar for Atif Aziz
    Atif Aziz January 15th, 2009

    @Richard: You're probably right and I'm sure there are other defects lurking and begging to be discovered. I was trying to make the code pass the test cases Phil had worked up so far since the tweaks needed were really trivial. By no means can the implementation claimed to be correct with respect to String.Format. In the ideal case, one would make it work, then make it right and eventually make it fast. By the time you're working on making it fast, you hope that you have all the tests written up to prove that the less-than-optimal version was right. They can then serve one to stay sane and stress-free while the guts are re-factored for speed.

  37. Avatar for Chance
    Chance July 31st, 2010

    Phil, I doubt you monitor these older posts anymore but your source for this article is AWOL.

  38. Avatar for Patrick Caldwell
    Patrick Caldwell August 22nd, 2011

    Phil,
    I know this is an old post and has probably been put to bed, but I recently posted something similar on my blog (.Net Object Formatter). I haven't benchmarked it compared to yours, but I followed the concept of String.Format in the manual parsing approach you took as well.
    I'm writing here because I also put the ObjectFormatter on github and was hoping you'd be interested in contributing.
    I know it's a common problem and would love to see something robust come of a collaborative effort.
    Thanks,
    Patrick Caldwell

  39. Avatar for Chris Fewtrell
    Chris Fewtrell September 1st, 2011

    Any chance of resurrecting the zip file?
    Having a look at your unit tests would be very handy.

  40. Avatar for haacked
    haacked September 7th, 2011
  41. Avatar for haacked
    haacked September 8th, 2011

    @Patrick very interesting. For email templates, I wonder if MvcMailer wouldn't also be a good choice. It lets you use Razor syntax.

  42. Avatar for Ramin
    Ramin July 26th, 2012

    Very nice formatter, would u please make a nuget package for this library.

  43. Avatar for Ivan Portugal
    Ivan Portugal October 18th, 2016

    Great work! I started using this but today I think it is not necessary due to an improvement in C# that probably happened after you wrote this article. Here's how you might do named parameters. Add the '$' :) Example:

    var myObject = "steak";
    var adjective = "juicy";
    var myNamedParametersString = $"My {myObject} is very very {adjective}";
    // myNamedParametersString would look like: "My steak is very very juicy"

    This feature I believe is called "string interpolation":
    https://github.com/dotnet/r...

  44. Avatar for haacked
    haacked October 18th, 2016

    Yep! Now that string interpolation is part of C#, this post is pretty much not needed. I'll update it to note it.

  45. Avatar for ZorgoZ
    ZorgoZ November 16th, 2016

    I don't think that interpolation makes this approach obsolote as it is a compile time syntactic sugar as far as I know. You can not construct on the fly the string behing the $ sign. You can expand a string constant only, not one in a variable.

    On the other hand it looks like some of the standard formatting elements can't be reproduced with the methods presented here. Fist of all, elignment.

  46. Avatar for ZorgoZ
    ZorgoZ November 17th, 2016

    I have upgraded JamesFormatter to support alignment too.

    public static string FormatWith(this string format
    , IFormatProvider provider, object source)
    {
    if (format == null)
    throw new ArgumentNullException("format");

    List<object> values = new List<object>();
    string rewrittenFormat = Regex.Replace(format,
    @"(?<start>\{)+(?<property>[\w\.\[\]]+)(,(?<alignment>-?\d+))?(?<format>:[^}]+)?(?<end>\})+",
    delegate(Match m)
    {
    Group startGroup = m.Groups["start"];
    Group propertyGroup = m.Groups["property"];
    Group alignmentGroup = m.Groups["alignment"];
    Group formatGroup = m.Groups["format"];
    Group endGroup = m.Groups["end"];

    values.Add((propertyGroup.Value == "0")
    ? source
    : Eval(source, propertyGroup.Value));

    int openings = startGroup.Captures.Count;
    int closings = endGroup.Captures.Count;

    var result = openings > closings || openings % 2 == 0
    ? m.Value
    : new string('{', openings) + (values.Count - 1)
    + (alignmentGroup.Captures.Count == 1 ? "," + alignmentGroup.Value : string.Empty)
    + formatGroup.Value
    + new string('}', closings);

    return result;
    },
    RegexOptions.Compiled
    | RegexOptions.CultureInvariant
    | RegexOptions.IgnoreCase);

    return string.Format(provider, rewrittenFormat, values.ToArray());
    }

  47. Avatar for Tomas Kouba
    Tomas Kouba May 23rd, 2018

    Interpolated string cannot be localized. So NamedFormat is still useful.