Есть ли лучший способ подсчета заполнителей формата строки в строке в C #? - PullRequest
11 голосов
/ 04 июня 2009

У меня есть строка шаблона и массив параметров, которые поступают из разных источников, но должны быть сопоставлены для создания новой «заполненной» строки:

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 ]

17 голосов
/ 04 июня 2009

Подсчет заполнителей не помогает - рассмотрим следующие случаи:

"{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 в наиболее общем случае (то есть с length и / или formatString).

И даже если вы не используете length или formatString сегодня, будущий разработчик может подумать, что это безобидное изменение - добавить его - было бы стыдно за то, что это сломало ваш код.

Я бы попытался имитировать код в StringBuilder.AppendFormat (который вызывается String.Format), даже если он немного уродливый - используйте Lutz Reflector, чтобы получить этот код. В основном перебираем строку, ища спецификаторы формата, и получаем значение индекса для каждого спецификатора.

8 голосов
/ 04 июня 2009

Слияние ответов Дамовизы и Джо. Я обновил ответ после комментариев Айдсмана над активами.

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}" => count = 3

"{4} aa {0} bb {0}, {0}" => count = 5

"{0} {3}, {{7}}" => count = 4

7 голосов
/ 04 июня 2009

Вы всегда можете использовать Regex:

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 голосов
/ 23 января 2013

Существует проблема с предложенным выше регулярным выражением в том, что оно будет соответствовать "{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;
    }

}
3 голосов
/ 11 сентября 2012

Ответ Marqus не выполняется, если в строке шаблона нет заполнителей.

Добавление условных выражений .DefaultIfEmpty() и m==null решает эту проблему.

Regex.Matches(templateString, @"(?<!\{)\{([0-9]+).*?\}(?!})")
     .Cast<Match>()
     .DefaultIfEmpty()
     .Max(m => m==null?-1:int.Parse(m.Groups[1].Value)) + 1;
3 голосов
/ 04 июня 2009

На самом деле не ответ на ваш вопрос, а возможное решение вашей проблемы (хотя и не совсем элегантное); Вы можете добавить в свою коллекцию dataItems несколько экземпляров string.Empty, поскольку string.Format не заботится о лишних элементах.

2 голосов
/ 05 июня 2009

Возможно, вы пытаетесь сломать орех кувалдой?

Почему бы просто не поставить try / catch вокруг вашего вызова String.Format.

Это немного уродливо, но решает вашу проблему так, что требует минимальных усилий, минимального тестирования и гарантированно сработает, даже если есть что-то еще о форматировании строк, которые вы не учитывали (например, {{literals, или более сложные строки формата с нечисловыми символами внутри: {0: $ #, ## 0,00; ($ #, ## 0,00); ноль})

(И да, это означает, что вы не обнаружите больше элементов данных, чем спецификаторов формата, но разве это проблема? Предположительно, пользователь вашего программного обеспечения заметит, что он урезал свои выходные данные и исправит их строку формата?)

1 голос
/ 13 июля 2016

Очень поздно к вопросу, но случилось по этому с другой касательной.

String.Format проблематичен даже при модульном тестировании (т. Е. Отсутствует аргумент). Разработчик вставляет неправильный позиционный заполнитель или отформатированная строка редактируется и хорошо компилируется, но используется в другом месте кода или даже в другой сборке, и вы получаете исключение FormatException во время выполнения. В идеале модульный тест или интеграционные тесты должны это уловить.

Хотя это не решение вопроса, это обходной путь. Ты можешь сделать вспомогательный метод, который принимает отформатированную строку и список (или массив) объектов. Внутри вспомогательного метода добавьте в список предопределенную фиксированную длину, которая будет превышать количество заполнителей в ваших сообщениях. Так, например, ниже предположим, что 10 заполнителей достаточно. Элемент заполнения может быть нулевым или содержать строку типа «[Missing]».

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()));

Это идеал? Нет, но для ведения журнала или некоторых случаев использования это приемлемая альтернатива для предотвращения исключения времени выполнения. Вы даже можете заменить нулевой элемент на «[Нет значения]» и / или добавить позиции массива, затем проверить значение «Нет значения» в отформатированной строке и зарегистрировать его как проблему.

1 голос
/ 04 июня 2009

Поскольку у меня нет полномочий редактировать сообщения, я предложу свою более короткую (и правильную) версию ответа Маркуса:

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

Я использую регулярное выражение, предложенное Айдсманом, но не проверял его.

0 голосов
/ 28 мая 2019

Вы можете "злоупотребить" 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, сосчитать, найти «дыры» и т. Д. *


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

...