Click here to Skip to main content
15,885,546 members
Articles / Programming Languages / C#

Extended string.Format()

Rate me:
Please Sign up or sign in to vote.
4.84/5 (31 votes)
19 Jul 2013CPOL3 min read 38.1K   306   30   15
Here I describe how you can write your own implementation of string.Format method using slightly more readable syntax of format string.

Introduction   

This article describes how new version of string.Format() method could be implemented with new more readable syntax of format string.  

Background   

Personally I like string.Format (or StringBuilder.AppendFormat) very much. I use it frequently and think that it is great if there are not too many arguments in your format string. But if it is not the case things look not so bright.

Lets consider the following code generating some SQL query:

C#
var sql = string.Format("SELECT {0} FROM [{1}].[{2}].[{3}] INNER JOIN [{1}].[{2}].[{4}]{5}{6}",
    GetColumns(),
    GetDatabaseName(),
    GetSchemaName(),
    GetFirstTable(),
    GetSecondTable(),
    GetWhereClause(),
    GetGroupByClause());

For me it looks little messy. It can take some time to understand what corresponds to e.g. the argument #5. But worse thing happens if I need to change the query and add some new argument in the beginning of string. E.g. I'd like to add "TOP" expression. I can do it like this:

C#
var sql = string.Format("SELECT {7}{0} FROM [{1}].[{2}].[{3}] INNER JOIN [{1}].[{2}].[{4}]{5}{6}",
    GetColumns(),
    GetDatabaseName(),
    GetSchemaName(),
    GetFirstTable(),
    GetSecondTable(),
    GetWhereClause(),
    GetGroupByClause(),
    GetTopClause()); 

Now I have argument #7 before argument #0 in my format string. It looks ugly for me. Another approach is to enumerate all arguments, but it is very error prone.

What I want to have is something like this: 

C#
var sql = StringEx.Format("SELECT {TopClause}{Columns} FROM [" + 
  "{Database}].[{Schema}].[{Table1}] INNER JOIN [{Database}]." + 
  "[{Schema}].[{Table2}]{WhereClause}{GroupByClause}",
    new {
        TopClause = GetTopClause(),
        Columns = GetColumns(),
        Database = GetDatabaseName(),
        Schema = GetSchemaName(),
        Table1 = GetFirstTable(),
        Table2 = GetSecondTable(),
        WhereClause = GetWhereClause(),
        GroupByClause = GetGroupByClause()
        });

Let's see how we can do it. 

Using the code 

The basic idea behind the code is simple. I change format string in new format into format string in old format. E.g. something like "{Value} and {Score} or {Value}" I replace with "{0} and {1} or {0}". While doing it one should remember 2 things: 

  1. I should not process format items with double curly brackets: "{{Value}}" 
  2. I should preserve formatting components. It means that strings like "{Value,5:D3}" should be converted into "{0,5:D3}"

Here is the method converting new format into old format:

C#
public ConvertedFormat Convert(string format)
{
    var placeholders = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
 
    var regex = new Regex("{[^{}]+}");
 
    StringBuilder formatBuilder = new StringBuilder(format);
 
    foreach (var match in regex.Matches(format).OfType<Match>().OrderByDescending(m => m.Index))
    {
        if (!ShouldBeReplaced(formatBuilder, match))
        { continue; }
 
        var memberInfo = GetMemberInfo(match);
 
        if (!placeholders.ContainsKey(memberInfo.MemberName))
        {
            placeholders[memberInfo.MemberName] = placeholders.Count;
        }
 
        var memberIndex = placeholders[memberInfo.MemberName];
 
        formatBuilder.Replace(match.Value, string.Format("{{{0}{1}}}", 
           memberIndex, memberInfo.Formatting), match.Index, match.Length);
    }
 
    var convertedFormat = new ConvertedFormat(formatBuilder.ToString(), 
        placeholders.OrderBy(p => p.Value).Select(p => p.Key).ToArray());
 
    return convertedFormat;
}

First of all I find all possible candidates for replacement using regular expression "{[^{}]+}" (it means "something in curly brackets"). I replace them in the initial format string with new format items. To keep correct positions of unprocessed candidates I use OrderByDescending to replace candidates from the end to the beginning. Then method ShouldBeReplaced checks if this is a valid candidate for replacement ("{Value}" not "{{Value}}"). Then method GetMemberInfo extracts from format item with all components ("{Value,5:D3}") name component ("Value") and other components (",5:D3"). After this I check if I already have format item with this name. For this purpose I use dictionary placeholders where for each name component of format items I store position in the array of argument I'll send to string.Format later. And final step is the replacement of candidate itself.

In the end I have format string in old format and array of names of members of my data object.  It is very easy to extract values of these members using Reflection.

One more point of interest is how I determine if a candidate is valid for replacement. For example in the following texts "{Value}" must be replaced: "{Value}", "{{{Value}}}", "{{{{{Value}}}". And in the following must not: "{{Value}}", "{{{{Value}}". Here is the code solving this problem:

C#
private static bool ShouldBeReplaced(StringBuilder formatBuilder, Match match)
{
    var bracketsBefore = 0;
    var index = match.Index - 1;
    while (index >= 0 && formatBuilder[index] == '{')
    {
        bracketsBefore++;
        index--;
    }

    return ((bracketsBefore % 2) == 0);
}

I just count number of curly brackets before format item.

Points of Interest

Although my code only creates new Format method which can be used instead of string.Format, you can easily write same method for StringBuilder class. You may create it in form of extension method to be more convenient.

History

  • Initial revision.

License

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


Written By
Software Developer (Senior) Finstek
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
StanleyJHubert5-Feb-15 22:32
StanleyJHubert5-Feb-15 22:32 
GeneralMy vote of 5 Pin
Halil ibrahim Kalkan18-Aug-13 19:19
Halil ibrahim Kalkan18-Aug-13 19:19 
GeneralMy vote of 5 Pin
  Forogar  16-Aug-13 7:19
professional  Forogar  16-Aug-13 7:19 
GeneralMy vote of 5 Pin
JJVH8430-Jul-13 8:18
JJVH8430-Jul-13 8:18 
QuestionIt's changing awl on soup. Pin
Thornik25-Jul-13 3:32
Thornik25-Jul-13 3:32 
QuestionInteresting, but... Pin
Marc Clifton23-Jul-13 6:04
mvaMarc Clifton23-Jul-13 6:04 
GeneralMy vote of 5 Pin
_Vitor Garcia_22-Jul-13 10:00
_Vitor Garcia_22-Jul-13 10:00 
SuggestionNice idea but... Pin
mrchief_200022-Jul-13 7:34
mrchief_200022-Jul-13 7:34 
GeneralMy vote of 5 Pin
kosmoh19-Jul-13 7:38
kosmoh19-Jul-13 7:38 
GeneralMy vote of 5 Pin
fredatcodeproject19-Jul-13 7:06
professionalfredatcodeproject19-Jul-13 7:06 
GeneralMy vote of 5 Pin
nobodyxxxxx19-Jul-13 5:52
nobodyxxxxx19-Jul-13 5:52 
GeneralVery Interesting - I like it !! Pin
cvogt6145719-Jul-13 5:47
cvogt6145719-Jul-13 5:47 
QuestionBroken/incompatible solution. Pin
Septimus Hedgehog19-Jul-13 5:13
Septimus Hedgehog19-Jul-13 5:13 
AnswerRe: Broken/incompatible solution. Pin
Ivan Yakimov21-Jul-13 20:35
professionalIvan Yakimov21-Jul-13 20:35 
GeneralRe: Broken/incompatible solution. Pin
Septimus Hedgehog21-Jul-13 21:46
Septimus Hedgehog21-Jul-13 21:46 
Ivan, thank you for confirming it should compile okay. The funny thing is I took the zip file home and it compiled 100% but here at work it doesn't. What I will do is create a new project and copy the source files into it. I'm at a loss to explain why I have the problem here. I do like the idea of using it so I thank you very much for posting it and will vote accordingly.Thumbs Up | :thumbsup:

For the record, my name isn't really Leslie. I went through a phase where I was posting duplicate threads and then getting lambasted for it. If you research Leslie Nielsen in The Lounge you'll learn a lot more about "him". Laugh | :laugh:

EDIT: Ivan, I found the problem. I have VS2012 installed at home, but VS2010 here at work. The csproj files were referencing framework 4.5 which I don't have installed here at work. I did a manual edit and changed it to 4.0 and it now loads and compiles successfully. It should have occurred to me earlier. (slaps forehead). Smile | :)
If there is one thing more dangerous than getting between a bear and her cubs it's getting between my wife and her chocolate.


modified 22-Jul-13 4:02am.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.