有没有更好的方法来计算C#中字符串中的字符串格式占位符?

时间:2009-06-04 02:23:31

标签: c# .net string

我有一个模板字符串和一组来自不同来源的参数但需要匹配才能创建一个新的“填充”字符串:

string templateString = GetTemplate();   // e.g. "Mr {0} has a {1}"
string[] dataItems = GetDataItems();     // e.g. ["Jones", "ceiling cat"}

string resultingString = String.Format(templateString, dataItems);
// e.g. "Mr Jones has a ceiling cat"

使用此代码,我假设模板中字符串格式占位符的数量将等于数据项的数量。在我的情况下,这通常是一个公平的假设,但我希望能够产生resultingString而不会失败,即使假设是错误的。我不介意是否有空格缺失数据。

如果dataItems中的项目过多,String.Format方法处理得很好。如果还不够,我会得到一个例外。

为了解决这个问题,我计算了占位符的数量,并在没有足够的情况下向dataItems数组添加新项目。

要计算占位符,我目前使用的代码是:

private static int CountOccurrences(string haystack)
{
    // Loop through all instances of the string "}".
    int count = 0;
    int i = 0;
    while ((i = text.IndexOf("}", i)) != -1)
    {
        i++;
        count++;
    }
    return count;
}

显然,这假设没有任何关闭花括号没有用于格式占位符。它也只是感觉错误。 :)

有没有更好的方法来计算字符串中的字符串格式占位符?


许多人都正确地指出,我标记为正确的答案在许多情况下都不起作用。主要原因是:

  • 计算占位符数的正则数据不考虑文字括号({{0}}
  • 计算占位符不会考虑重复或跳过的占位符(例如"{0} has a {1} which also has a {1}"

12 个答案:

答案 0 :(得分:17)

计算占位符无济于事 - 请考虑以下情况:

“{0} ... {1} ... {0}” - 需要2个值

“{1} {3}” - 需要4个值,其中两个被忽略

第二个例子不是牵强附会。

例如,您可能在美国英语中有类似的内容:

String.Format("{0} {1} {2} has a {3}", firstName, middleName, lastName, animal);

在某些文化中,可能不会使用中间名,您可能会:

String.Format("{0} {2} ... {3}", firstName, middleName, lastName, animal);

如果你想这样做,你需要寻找具有最大索引的格式说明符 {index [,length] [:formatString]} ,忽略重复的括号(例如{{n} })。重复大括号用于在输出字符串中将大括号插入文字。我将把编码留作练习:) - 但我不认为它可以或应该在最常见的情况下使用Regex(即使用长度和/或formatString)。

即使你今天没有使用length或formatString,未来的开发人员可能会认为添加一个是一个无害的变化 - 这会破坏你的代码是一种耻辱。

我会尝试模仿StringBuilder.AppendFormat(由String.Format调用)中的代码,即使它有点难看 - 使用Lutz Reflector来获取此代码。基本上遍历字符串查找格式说明符,并获取每个说明符的索引值。

答案 1 :(得分:8)

合并Damovisa和Joe的答案。 我已经更新了答案Aydsman的nad activa的评论。

int count = Regex.Matches(templateString, @"(?<!\{)\{([0-9]+).*?\}(?!})")  //select all placeholders - placeholder ID as separate group
                 .Cast<Match>() // cast MatchCollection to IEnumerable<Match>, so we can use Linq
                 .Max(m => int.Parse(m.Groups[1].Value)) + 1; // select maximum value of first group (it's a placegolder ID) converted to int

此方法适用于以下模板:

“{0} aa {2} bb {1}”=&gt; count = 3

“{4} aa {0} bb {0},{0}”=&gt; count = 5

“{0} {3},{{7}}”=&gt; count = 4

答案 2 :(得分:7)

您始终可以使用正则表达式:

using System.Text.RegularExpressions;
// ... more code
string templateString = "{0} {2} .{{99}}. {3}"; 
Match match = Regex.Matches(templateString, 
             @"(?<!\{)\{(?<number>[0-9]+).*?\}(?!\})")
            .Cast<Match>()
            .OrderBy(m => m.Groups["number"].Value)
            .LastOrDefault();
Console.WriteLine(match.Groups["number"].Value); // Display 3

答案 3 :(得分:3)

实际上不是你问题的答案,而是你问题的可能解决方案(虽然不是一个非常优雅的问题);您可以使用多个dataItems个实例填充string.Empty个集合,因为string.Format并不关心多余的项目。

答案 4 :(得分:3)

如果模板字符串中没有占位符,

Marqus的答案将失败。

添加.DefaultIfEmpty()m==null条件可解决此问题。

Regex.Matches(templateString, @"(?<!\{)\{([0-9]+).*?\}(?!})")
     .Cast<Match>()
     .DefaultIfEmpty()
     .Max(m => m==null?-1:int.Parse(m.Groups[1].Value)) + 1;

答案 5 :(得分:3)

上面提到的正则表达式存在一个问题,它会匹配“{0}}”:

Regex.Matches(templateString, @"(?<!\{)\{([0-9]+).*?\}(?!})")
...

问题在于寻找它使用的关闭}。*允许初始}作为匹配。因此,将其更改为停在第一个上}会使后缀检查工作。换句话说,使用它作为正则表达式:

Regex.Matches(templateString, @"(?<!\{)\{([0-9]+)[^\}]*?\}(?!\})")
...

我根据所有这些制作了几个静态函数,也许你会发现它们很有用。

public static class StringFormat
{
    static readonly Regex FormatSpecifierRegex = new Regex(@"(?<!\{)\{([0-9]+)[^\}]*?\}(?!\})", RegexOptions.Compiled);

    public static IEnumerable<int> EnumerateArgIndexes(string formatString)
    {
        return FormatSpecifierRegex.Matches(formatString)
         .Cast<Match>()
         .Select(m => int.Parse(m.Groups[1].Value));
    }

    /// <summary>
    /// Finds all the String.Format data specifiers ({0}, {1}, etc.), and returns the
    /// highest index plus one (since they are 0-based).  This lets you know how many data
    /// arguments you need to provide to String.Format in an IEnumerable without getting an
    /// exception - handy if you want to adjust the data at runtime.
    /// </summary>
    /// <param name="formatString"></param>
    /// <returns></returns>
    public static int GetMinimumArgCount(string formatString)
    {
        return EnumerateArgIndexes(formatString).DefaultIfEmpty(-1).Max() + 1;
    }

}

答案 6 :(得分:2)

也许你正试图用大锤敲打坚果?

为什么不在你对String.Format的调用周围加上 try / catch

它有点难看,但是以一种需要最少的努力,最少的测试的方式解决你的问题,并且即使还有其他关于你没有考虑的格式化字符串的东西(如{{literals,或者更复杂的格式字符串,其中包含非数字字符:{0:$#,## 0.00;($#,## 0.00); Zero})

(是的,这意味着你不会检测到比格式说明符更多的数据项,但这是一个问题吗?假设你的软件用户会注意到他们截断了输出并纠正了他们的格式字符串?)

答案 7 :(得分:1)

由于我没有权限编辑帖子,我会提出我的更短(和正确)版本的Marqus答案:

int num = Regex.Matches(templateString,@"(?<!\{)\{([0-9]+).*?\}(?!})")
             .Cast<Match>()
             .Max(m => int.Parse(m.Groups[0].Value)) + 1;

我正在使用Aydsman提出的正则表达式,但尚未对其进行测试。

答案 8 :(得分:1)

这个问题很晚,但是从另一个切线发生了这个问题。

即使使用单元测试(即缺少参数),String.Format也存在问题。开发人员放入错误的位置占位符或编辑格式化的字符串并编译正常,但它在另一个代码位置或甚至另一个程序集中使用,并且您在运行时获得FormatException。理想情况下,单元测试或集成测试应该抓住这一点。

虽然这不是答案的解决方案,但它是一种解决方法。你(们)能做到 一个辅助方法,它接受格式化的字符串和对象的列表(或数组)。在帮助器方法内部,将列表填充到预定义的固定长度,该长度将超过消息中的占位符数。因此,例如下面假设10个占位符就足够了。 padding元素可以为null或类似于&#34; [Missing]&#34;。

int q = 123456, r = 76543;
List<object> args = new List<object>() { q, r};     

string msg = "Sample Message q = {2:0,0} r = {1:0,0}";

//Logic inside the helper function
int upperBound = args.Count;
int max = 10;

for (int x = upperBound; x < max; x++)
{
    args.Add(null); //"[No Value]"
}
//Return formatted string   
Console.WriteLine(string.Format(msg, args.ToArray()));

这是理想的吗?不,但对于日志记录或某些用例,它是防止运行时异常的可接受替代方法。你甚至可以用&#34; [No Value]&#34;替换null元素。和/或添加数组位置,然后在格式化字符串中测试无值,然后将其记录为问题。

答案 9 :(得分:0)

您可以使用正则表达式来计算仅具有您将在它们之间使用的格式的{}对。除非你使用格式化选项,否则@“\ {\ d + \}”就足够了。

答案 10 :(得分:0)

基于this answer和David White的答案是更新版本:

string formatString = "Hello {0:C} Bye {{300}} {0,2} {34}";
//string formatString = "Hello";
//string formatString = null;

int n;
var countOfParams = Regex.Matches(formatString?.Replace("{{", "").Replace("}}", "") ?? "", @"\{([0-9]+)")
    .OfType<Match>()
    .DefaultIfEmpty()
    .Max(m => Int32.TryParse(m?.Groups[1]?.Value, out n) ? n : -1 )
    + 1;

Console.Write(countOfParams);

注意事项:

  1. 更换是一种更直接的方式来照顾双花括号。这类似于StringBuilder.AppendFormatHelper在内部处理它们的方式。
  2. 正如消除'{{'和'}}'一样,正则表达式可以简化为'{([0-9] +)'
  3. 即使formatString为null
  4. ,这也会有效
  5. 即使格式无效,也可以使用“{3444444456}”。通常这会导致整数溢出。

答案 11 :(得分:0)

您可以“滥用” ICustomFormatter,以收集占位符并将其返回给调用方。这只是重用了内置的解析算法,而不是尝试重新实现它(并且可能偏离内置的算法)。

using System;
using System.Collections.Generic;
using System.Linq;

namespace FormatPlaceholders {

    class Program {

        class FormatSnooper : IFormatProvider, ICustomFormatter {

            public object GetFormat(Type formatType) {
                return this;
            }

            public string Format(string format, object arg, IFormatProvider formatProvider) {
                Placeholders.Add(((int)arg, format));
                return null;
            }

            internal readonly List<(int index, string format)> Placeholders = new List<(int index, string format)>();

        }

        public static IEnumerable<(int index, string format)> GetFormatPlaceholders(string format, int max_count = 100) {

            var snooper = new FormatSnooper();

            string.Format(
                snooper,
                format,
                Enumerable.Range(0, max_count).Cast<object>().ToArray()
            );

            return snooper.Placeholders;

        }

        static void Main(string[] args) {
            foreach (var (index, format) in GetFormatPlaceholders("{1:foo}{4:bar}{1:baz}"))
                Console.WriteLine($"{index}: {format}");
        }

    }

}

哪些印刷品:

1: foo
4: bar
1: baz

然后,您可以轻松找到index的最大值,计数,查找“空洞”等...


我意识到我参加聚会已经晚了几年,但是我需要类似于OP要求的内容,因此我分享了我在这里提出的解决方案,以防万一有人觉得有用... < / p>