Регулярное выражение для String.Format-подобной утилиты - PullRequest
2 голосов
/ 18 сентября 2009

Я пишу класс с именем 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.

Это прекрасно работает, за исключением случаев, когда есть двойные скобки, которые являются escape-последовательностью. Желаемое поведение (которое совпадает с String.Format) описано ниже:

  • "Hello {Name}" следует отформатировать как "Hello World"
  • "Hello {{Name}}" должен быть отформатирован как "Hello {Name}"
  • "Hello {{{Name}}}" следует отформатировать как "Hello {World}"
  • "Hello {{{{Name}}}" " должен быть отформатирован как " Hello {{Name}} "

И так далее ...

Но мое текущее регулярное выражение не определяет escape-последовательность и всегда рассматривает подстроку в скобках как заполнитель, поэтому я получаю такие вещи, как "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);
    }
}

Ответы [ 4 ]

3 голосов
/ 18 сентября 2009

Четность, как правило, очень легко определить с помощью регулярных выражений. Например, это выражение, которое соответствует любой строке с четным числом A с, но не с нечетным числом:

(AA)*

Итак, все, что вам нужно сделать, это найти выражение, которое соответствует нечетному числу { с и } с.

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

(несмотря на символы) Так что добавление этой идеи к вашему текущему выражению даст что-то вроде

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

Однако это не соответствует количеству скобок с обеих сторон. Другими словами, {{{ будет соответствовать }, потому что они оба странные. Регулярные выражения не могут считать вещи, поэтому вы не сможете найти выражение, которое соответствует количеству элементов, как вы хотите.

На самом деле, вам следует выполнить синтаксический анализ строк с помощью специального синтаксического анализатора, который читает строку и считает экземпляры {, но не экземпляры {{, чтобы сопоставить их с экземплярами }, но не }} на другой стороне. Я думаю, вы поймете, что именно так String форматеры в .NET работают за кулисами, так как регулярные выражения не подходят для анализа вложенных структур любого типа.

Или вы можете использовать обе идеи совместно: сопоставьте потенциальные токены с регулярным выражением, а затем проверьте баланс их фигурных скобок, быстро проверив полученное совпадение. Это, вероятно, в конечном итоге будет запутанным и косвенным. Обычно вам лучше написать собственный синтаксический анализатор для такого сценария.

3 голосов
/ 18 сентября 2009

Это вполне возможно при использовании регулярных выражений, но я совсем не уверен, что это будет самое простое решение для поддержки. Учитывая, что вы действительно только интересуетесь здесь скобками и двоеточиями (я думаю), я бы лично избегал использования регулярных выражений.

Я бы построил последовательность токенов, каждый из которых был бы литералом или строкой формата. Создайте это, просто прогуливаясь по струне и замечая открывающую и закрывающую скобки. Затем для оценки последовательности нужно просто объединить токены, отформатировать каждый из них, где это необходимо.

Опять же, я никогда не был большим поклонником регулярных выражений - просто иногда они прекрасны, но большую часть времени они чувствуют себя излишними. Может быть, есть какой-нибудь умный способ заставить их делать то, что вы хотите в этом случае ...

Кстати, вам нужно определить, что вы хотите, чтобы произошло в случаях, когда скобки не соответствуют должным образом, например,

{{Name} foo
1 голос
/ 19 сентября 2009

Вы можете использовать регулярное выражение для сопоставления сбалансированной пары, а затем выяснить, что делать с фигурными скобками. Помните, что регулярные выражения .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();
        }

    }

}
0 голосов
/ 19 сентября 2009

В конце концов я использовал технику, аналогичную предложенной Гэвином.

Я изменил регулярное выражение, чтобы оно совпадало со всеми фигурными скобками вокруг заполнителя:

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}{{{{{1}}}{2}}}{3}", open, key, format, close);
                }
            }
            return m.Value;
        };

Я полагаюсь на String.Format, чтобы бросить FormatException при необходимости. Я сделал несколько юнит-тестов, и пока все работает нормально ...

Спасибо всем за помощь!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...