String.Format类实用程序的正则表达式

时间:2009-09-18 16:19:47

标签: c# regex formatting templates

我正在编写一个名为StringTemplate的类,它允许使用String.Format来格式化对象,但是使用名称而不是占位符的索引。这是一个例子:

string s = StringTemplate.Format("Hello {Name}. Today is {Date:D}, and it is {Date:T}.",
                                 new { Name = "World", Date = DateTime.Now });

为了实现这个结果,我寻找占位符并用索引替换它们。然后,我将生成的格式字符串传递给String.Format

这种方法很好,除非有双重括号,这是一个转义序列。所需的行为(与String.Format相同)如下所述:

  • “Hello {Name}”应格式化为“Hello World”
  • “Hello {{Name}}”的格式应为“Hello {Name}”
  • “Hello {{{Name}}}”的格式应为“Hello {World}”
  • “Hello {{{{Name}}}}”的格式应为“Hello {{Name}}”

等等......

但是我当前的正则表达式没有检测到转义序列,并且始终将括号中的子字符串视为占位符,因此我得到的内容类似于“Hello {0}”

这是我目前的正则表达式:

private static Regex _regex = new Regex(@"{(?<key>\w+)(?<format>:[^}]+)?}", RegexOptions.Compiled);

如何修改此正则表达式以忽略转义大括号?看起来真的很难的是我应该检测占位符取决于括号的数量是奇数还是偶数......我不能想想用正则表达式做一个简单的方法,它甚至可能吗?


为了完整性,这里是StringTemplate类的完整代码:

public class StringTemplate
{
    private string _template;
    private static Regex _regex = new Regex(@"{(?<key>\w+)(?<format>:[^}]+)?}", RegexOptions.Compiled);

    public StringTemplate(string template)
    {
        if (template == null)
            throw new ArgumentNullException("template");
        this._template = template;
    }

    public static implicit operator StringTemplate(string s)
    {
        return new StringTemplate(s);
    }

    public override string ToString()
    {
        return _template;
    }

    public string Format(IDictionary<string, object> values)
    {
        if (values == null)
        {
            throw new ArgumentNullException("values");
        }

        Dictionary<string, int> indexes = new Dictionary<string, int>();
        object[] array = new object[values.Count];
        int i = 0;
        foreach (string key in values.Keys)
        {
            array[i] = values[key];
            indexes.Add(key, i++);
        }

        MatchEvaluator evaluator = (m) =>
        {
            if (m.Success)
            {
                string key = m.Groups["key"].Value;
                string format = m.Groups["format"].Value;
                int index = -1;
                if (indexes.TryGetValue(key, out index))
                {
                    return string.Format("{{{0}{1}}}", index, format);
                }
            }
            return string.Format("{{{0}}}", m.Value);
        };

        string templateWithIndexes = _regex.Replace(_template, evaluator);
        return string.Format(templateWithIndexes, array);
    }

    private static IDictionary<string, object> MakeDictionary(object obj)
    {
        Dictionary<string, object> dict = new Dictionary<string, object>();
        foreach (var prop in obj.GetType().GetProperties())
        {
            dict.Add(prop.Name, prop.GetValue(obj, null));
        }
        return dict;
    }

    public string Format(object values)
    {
        return Format(MakeDictionary(values));
    }

    public static string Format(string template, IDictionary<string, object> values)
    {
        return new StringTemplate(template).Format(values);
    }


    public static string Format(string template, object values)
    {
        return new StringTemplate(template).Format(values);
    }
}

5 个答案:

答案 0 :(得分:3)

使用正则表达式很可能 - 但我完全不相信它是最容易维护的解决方案。鉴于你真的对这里的大括号和冒号感兴趣(我认为),我个人会避免使用正则表达式。

我会构造一个标记序列,每个标记都是文字或格式字符串。通过沿着弦走,并注意到开口和闭合括号来构造它。然后评估序列只是连接令牌的问题,在适当的时候格式化每个令牌。

然后我再也没有像常规表达的粉丝那样 - 偶尔他们很精彩,但很多时候他们觉得有点过分。也许有一些聪明的方法可以让他们在这种情况下做你想做的事......

是的,你需要定义在大括号不匹配的情况下你想要发生什么,例如。

{{Name} foo

答案 1 :(得分:3)

使用正则表达式通常很容易决定奇偶校验。例如,这是一个表达式,匹配任何具有偶数个A s的字符串,但不是奇数:

(AA)*

所以你需要做的就是找到只匹配奇数{} s的表达式。

{({{)*
}(}})* 

(尽管逃脱了角色)。因此,将此想法添加到当前表达式将产生类似

的内容
{({{)*(?<key>\w+)(?<format>:[^}]+)?}(}})*

然而,这与双方牙箍的基数不符。换句话说,{{{将匹配},因为它们都是奇数。正则表达式无法统计事物,因此您无法找到与您想要的基数相匹配的表达式。

真的,您应该做的是使用自定义解析器解析字符串,该解析器读取字符串并计算{的实例但不计算{{的实例,以便将它们与{{}的实例进行匹配1}}而不是另一方}}。我想你会发现这就是.NET格式化程序在幕后工作的方式,因为正则表达式不适合解析任何类型的嵌套结构。

或者您可以同时使用这两种想法:将潜在的令牌与正则表达式匹配,然后使用快速检查结果匹配来验证其大括号的平衡。但这可能最终会让人感到困惑和间接。你通常最好为这种情况编写自己的解析器。

答案 2 :(得分:1)

您可以使用正则表达式匹配平衡对,然后找出如何处理大括号。请记住,.NET正则表达式不是“常规”。

class Program {
    static void Main(string[] args) {
        var d = new Dictionary<string, string> { { "Name", "World" } };
        var t = new Test();
        Console.WriteLine(t.Replace("Hello {Name}", d));
        Console.WriteLine(t.Replace("Hello {{Name}}", d));
        Console.WriteLine(t.Replace("Hello {{{Name}}}", d));
        Console.WriteLine(t.Replace("Hello {{{{Name}}}}", d));
        Console.ReadKey();
    }
}

class Test {

    private Regex MatchNested = new Regex(
        @"\{ (?>
                ([^{}]+)
              | \{ (?<D>)
              | \} (?<-D>)
              )*
              (?(D)(?!))
           \}",
             RegexOptions.IgnorePatternWhitespace
           | RegexOptions.Compiled 
           | RegexOptions.Singleline);

    public string Replace(string input, Dictionary<string, string> vars) {
        Matcher matcher = new Matcher(vars);
        return MatchNested.Replace(input, matcher.Replace);
    }

    private class Matcher {

        private Dictionary<string, string> Vars;

        public Matcher(Dictionary<string, string> vars) {
            Vars = vars;
        }

        public string Replace(Match m) {
            string name = m.Groups[1].Value;
            int length = (m.Groups[0].Length - name.Length) / 2;
            string inner = (length % 2) == 0 ? name : Vars[name];
            return MakeString(inner, length / 2);
        }

        private string MakeString(string inner, int braceCount) {
            StringBuilder sb = new StringBuilder(inner.Length + (braceCount * 2));
            sb.Append('{', braceCount);
            sb.Append(inner);
            sb.Append('}', braceCount);
            return sb.ToString();
        }

    }

}

答案 3 :(得分:0)

我最终使用的技术与加文所建议的相似。

我更改了正则表达式,使其匹配占位符周围的所有大括号:

private static Regex _regex = new Regex(@"(?<open>{+)(?<key>\w+)(?<format>:[^}]+)?(?<close>}+)", RegexOptions.Compiled);

我更改了MatchEvaluator的逻辑,以便它正确处理转义大括号:

        MatchEvaluator evaluator = (m) =>
        {
            if (m.Success)
            {
                string open = m.Groups["open"].Value;
                string close = m.Groups["close"].Value;
                string key = m.Groups["key"].Value;
                string format = m.Groups["format"].Value;

                if (open.Length % 2 == 0)
                    return m.Value;

                open = RemoveLastChar(open);
                close = RemoveLastChar(close);

                int index = -1;
                if (indexes.TryGetValue(key, out index))
                {
                    return string.Format("{0}{{{1}{2}}}{3}", open, index, format, close);
                }
                else
                {
                    return string.Format("{0}{{{String.Format}{2}}}{3}", open, key, format, close);
                }
            }
            return m.Value;
        };

如有必要,我依靠FormatException投掷{{1}}。我进行了一些单元测试,到目前为止似乎工作正常......

感谢大家的帮助!

答案 4 :(得分:0)

我遇到了类似的问题。就我而言,键是纯数字的,没有格式选项。以下正则表达式正在解决问题:

Regex r = new Regex(@"
  (?<! { ) 
    { (?<before> (?: {{ )* ) 
      (?<key> \d+) 
    } (?<after>  (?: }} )* )
  (?! } )
", RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);

它使我可以简单地替换一个给定的数字键,并用大括号括起来,如下所示:

s = r.Replace(s, "${before}_replacement_${after}"));