12

Is it possible to count the number of expected args/params in a string for String.Format()?

For example: "Hello {0}. Bye {1}" should return a count of 2.

I need to display an error before the string.Format() throws an exception.

Thanks for your help.

Divi
  • 7,621
  • 13
  • 47
  • 63
  • 2
    Why not simply catch the exception and display the error then? – Marcelo Cantos Feb 14 '11 at 05:30
  • Sorry, but why do you need to do this? :P I see no way to do this short of parsing out the string. What should "{0} {0}" return? Wouldn't it be easier to just catch the exception? – Jonathan Wood Feb 14 '11 at 05:31
  • I hate to play the blame game and say that someone else wrote this code, but they did. There are a number of validations that are happening on the way that error messages are formatted and one of them is comparing the number of arguments passed with the number of expected arguments. So there already is a solution. I'm just trying to find a better way of doing it. – Divi Feb 14 '11 at 05:33
  • 1
    Possible duplicate of http://stackoverflow.com/questions/2156497/string-has-how-many-parameters – nybbler Feb 14 '11 at 05:34
  • 2
    *"Please don't ask me why I need to do this"* - http://blogs.msdn.com/b/oldnewthing/archive/2006/03/23/558887.aspx – Ed S. Feb 14 '11 at 06:04
  • 1
    possible duplicate of [Is there a better way to count string format placeholders in a string in C#?](http://stackoverflow.com/questions/948303/is-there-a-better-way-to-count-string-format-placeholders-in-a-string-in-c) – Joe Feb 14 '11 at 06:48
  • I did try searching before posting the question here. But thanks for the link – Divi Feb 14 '11 at 06:54
  • The correct way would be as Marcelo Cantos states (catch exception). What if a value is reused, for example the string format is "{0}={1},{0}" so 3{} but 2 values. All that regex code when you could just try/catch. –  Feb 08 '12 at 15:59
  • 1
    I would take another approach. But the answer to your question is that you'd just need to parse the string. You need to handle cases like "{{0}}", which does not count. You also need to handle cases like "{0:#,##0}". That's your answer. Again, I wouldn't take this approach. – Jonathan Wood Feb 14 '11 at 05:40
  • @EdS. Excellent article. Link is dead - here's the Archive.org version: https://web.archive.org/web/20100806024009/http://blogs.msdn.com/b/oldnewthing/archive/2006/03/23/558887.aspx – lucrativelucas Mar 02 '20 at 16:04

2 Answers2

16

You could use a regex, something like {(.*?)} and then just count the matches. If you need to handle cases like {0} {0} (which I guess should return 1) then that makes it a little more difficult, but you could always put all of the matches in a list an do a Linq select distinct on it. I'm thinking something like the code below:

var input = "{0} and {1} and {0} and {2:MM-dd-yyyy}";
var pattern = @"{(.*?)}";
var matches = Regex.Matches(input, pattern);
var totalMatchCount = matches.Count;
var uniqueMatchCount = matches.OfType<Match>().Select(m => m.Value).Distinct().Count();
Console.WriteLine("Total matches: {0}", totalMatchCount);
Console.WriteLine("Unique matches: {0}", uniqueMatchCount);

EDIT:

I wanted to address some of the concerns raised in the comments. The updated code posted below handles the cases where there are escaped bracket sequences (i.e., {{5}}), where no parameters are specified, and also returns the value of the highest parameter + 1. The code assumes that the input strings will be well formed, but that trade off may be acceptable in some cases. For example, if you know that the input strings are defined in an application and not generated by user input, then handling all of the edge cases may not be necessary. It might also be possible to test all of the error messages to be generated using a unit test. The thing I like about this solution is that it will most likely handle the vast majority of the strings that are thrown at it, and it is a simpler solution than the one identified here (which suggests a reimplementation of string.AppendFormat). I would account for the fact that this code might not handle all edge cases by using a try-catch and just returning "Invalid error message template" or something to that effect.

One possible improvement for the code below would be to update the regex to not return the leading "{" characters. That would eliminate the need for the Replace("{", string.Empty). Again, this code might not be ideal in all cases, but I feel it adequately addresses the question as asked.

const string input = "{0} and {1} and {0} and {4} {{5}} and {{{6:MM-dd-yyyy}}} and {{{{7:#,##0}}}} and {{{{{8}}}}}";
//const string input = "no parameters";
const string pattern = @"(?<!\{)(?>\{\{)*\{\d(.*?)";
var matches = Regex.Matches(input, pattern);
var totalMatchCount = matches.Count;
var uniqueMatchCount = matches.OfType<Match>().Select(m => m.Value).Distinct().Count();
var parameterMatchCount = (uniqueMatchCount == 0) ? 0 : matches.OfType<Match>().Select(m => m.Value).Distinct().Select(m => int.Parse(m.Replace("{", string.Empty))).Max() + 1;
Console.WriteLine("Total matches: {0}", totalMatchCount);
Console.WriteLine("Unique matches: {0}", uniqueMatchCount);
Console.WriteLine("Parameter matches: {0}", parameterMatchCount);
Community
  • 1
  • 1
rsbarro
  • 27,021
  • 9
  • 71
  • 75
3

I think this will handle Escape brackets and 0:0000 stuff... will give the greatest bracketed value... so in my example will give 1.

       //error prone to malformed brackets...
        string s = "Hello {0:C} Bye {1} {0} {{34}}";

        int param = -1;
        string[] vals = s.Replace("{{", "").Replace("}}", "").Split("{}".ToCharArray());
        for (int x = 1; x < vals.Length-1; x += 2)
        {
            int thisparam;
            if (Int32.TryParse(vals[x].Split(',')[0].Split(':')[0], out thisparam) && param < thisparam)
                param = thisparam;
        }
        //param will be set to the greatest param now.
deepee1
  • 12,878
  • 4
  • 30
  • 43