Форматирование предложений в строке с помощью C#

У меня есть строка с несколькими предложениями. Как сделать первую букву первого слова в каждом предложении заглавной. Что-то вроде форматирования абзаца в word.

например, "это какой-то код. Код на C#". Вывод должен быть "Это какой-то код. Код на C#".

одним из способов было бы разделить строку на основе '.' а затем сделайте первую букву заглавной, а затем воссоединитесь.

Есть ли лучшее решение?


person AlwaysAProgrammer    schedule 25.01.2010    source источник
comment
Я бы сказал - преобразовать в массив символов, пройти с помощью цикла while, использовать заглавные буквы, когда это необходимо, сохранить обратно в строку. Несколько строк кода, но это должно быть быстро.   -  person Hamish Grubijan    schedule 26.01.2010
comment
@Hamish: это неплохой ответ; это, безусловно, намного лучше, чем многократное манипулирование строкой. Однако я думаю, что StringBuffer будет проще.   -  person Steven Sudit    schedule 26.01.2010
comment
Не забудьте '?', '!', ':' и, возможно, '\n' (если они пропускают пунктуацию)   -  person Andres    schedule 26.01.2010
comment
На самом деле это намного сложнее, если вы действительно хотите сделать это правильно. Подумайте о следующем: я родился 1 января 1999 года. Мой адрес электронной почты — [email protected]. Итак, для вашего примера обязательно используйте разделение и соединение, но я думаю, что на самом деле вы, по крайней мере, смотрите на сложное регулярное выражение.   -  person Greg Roberts    schedule 26.01.2010
comment
@Greg Я попробовал ваш образец с предложенным мной решением регулярного выражения. Вроде всё нормально, схема не сложная. Он просто действует на первое слово, найденное после знака препинания, за которым следует пробел (или начало строки).   -  person Ahmad Mageed    schedule 26.01.2010


Ответы (5)


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

Я бы использовал перегрузку Regex.Replace, которая принимает входную строку, шаблон регулярного выражения и делегат MatchEvaluator. MatchEvaluator — это функция, которая принимает объект Match в качестве входных данных и возвращает замену строки.

Вот код:

public static string Capitalise(string input)
{
  //now the first character
  return Regex.Replace(input, @"(?<=(^|[.;:])\s*)[a-z]",
    (match) => { return match.Value.ToUpper(); });
}

Регулярное выражение использует конструкцию (?‹=) (положительный просмотр назад с нулевой шириной), чтобы ограничить захват только символами az, которым предшествует начало строки, или нужными вам знаками препинания. В бит [.;:] вы можете добавить дополнительные, которые хотите (например, [.;:?."], чтобы добавить символы ? и ".

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

Все остальное, упомянутое одним из других респондентов об использовании RegexOptions.Compiled, также актуально с точки зрения производительности. Однако статический метод Regex.Replace предлагает очень похожие преимущества в производительности (есть только дополнительный поиск по словарю).

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

ИЗМЕНИТЬ

Сопоставили это решение с решением Ахмада, поскольку он совершенно справедливо указал, что осмотр может быть менее эффективным, чем делать это по-своему.

Вот грубый тест, который я сделал:

public string LowerCaseLipsum
{
  get
  {
    //went to lipsum.com and generated 10 paragraphs of lipsum
    //which I then initialised into the backing field with @"[lipsumtext]".ToLower()
    return _lowerCaseLipsum;
  }
 }
 [TestMethod]
 public void CapitaliseAhmadsWay()
 {
   List<string> results = new List<string>();
   DateTime start = DateTime.Now;
   Regex r = new Regex(@"(^|\p{P}\s+)(\w+)", RegexOptions.Compiled);
   for (int f = 0; f < 1000; f++)
   {
     results.Add(r.Replace(LowerCaseLipsum, m => m.Groups[1].Value
                      + m.Groups[2].Value.Substring(0, 1).ToUpper()
                           + m.Groups[2].Value.Substring(1)));
   }
   TimeSpan duration = DateTime.Now - start;
   Console.WriteLine("Operation took {0} seconds", duration.TotalSeconds);
 }

 [TestMethod]
 public void CapitaliseLookAroundWay()
 {
   List<string> results = new List<string>();
   DateTime start = DateTime.Now;
   Regex r = new Regex(@"(?<=(^|[.;:])\s*)[a-z]", RegexOptions.Compiled);
   for (int f = 0; f < 1000; f++)
   {
     results.Add(r.Replace(LowerCaseLipsum, m => m.Value.ToUpper()));
   }
   TimeSpan duration = DateTime.Now - start;
   Console.WriteLine("Operation took {0} seconds", duration.TotalSeconds);
 }

В релизной сборке мое решение было примерно на 12% быстрее, чем у Ахмада (1,48 секунды против 1,68 секунды).

Интересно, однако, что если это было сделано с помощью статического метода Regex.Replace, оба они были примерно на 80% медленнее, и мое решение было медленнее, чем у Ахмада.

person Andras Zoltan    schedule 25.01.2010
comment
Я подозреваю, что даже с предварительно скомпилированным регулярным выражением он не будет таким быстрым, как StringBuilder. - person Steven Sudit; 26.01.2010
comment
В любом случае Regex использует Stringbuilder внутри, но я думаю, что единственный способ узнать это - сравнить различные решения. Пока мы этого не сделаем, все остальное будет чистой догадкой :) - person Andras Zoltan; 26.01.2010
comment
Андрас: Спасибо за ответ. Это не сработает, если у нас есть знаки препинания типа '?'. Я думаю, ответ Ахмада ниже подходит близко. Мне еще предстоит полностью оценить его. - person AlwaysAProgrammer; 26.01.2010
comment
Йогендра: Конечно, будет - просто добавьте вопросительный знак к биту [.;:] в регулярном выражении - т.е. измените его на '[.;:?]'. Действительно, вы можете добавить все отдельные знаки препинания, которые вам нужны, чтобы заключить их в эти две квадратные скобки. Я также отредактировал ответ, потому что '.' не требует ведущего '\' внутри []. - person Andras Zoltan; 26.01.2010
comment
Я должен сказать, что вы можете использовать это регулярное выражение так же, как у Ахмада - просто замените блок [punctuation_characters] классом знаков препинания. Помимо этого, структура этого регулярного выражения лучше, потому что не требует операций «a + b + c» и SubString в MatchEvaluator и, следовательно, будет намного быстрее. - person Andras Zoltan; 26.01.2010
comment
@Андрас: ты их сравнивал? :) Интересно, как они будут сравниваться, учитывая использование осмотра. Приятно видеть еще одно решение для регулярных выражений! - person Ahmad Mageed; 26.01.2010
comment
+1 извините, у меня не было времени самому настроить тест прямо сейчас, но спасибо, что приложили дополнительные усилия. - person Ahmad Mageed; 26.01.2010
comment
Также спасибо - за то, что заставили меня больше думать о решении для осмотра; также на данный момент обнаружил, что статическое regex.replace, несмотря на то, что оно скомпилировано и кэшировано, значительно медленнее по сравнению с скомпилированным экземпляром регулярного выражения! - person Andras Zoltan; 26.01.2010

Вот решение регулярного выражения, которое использует категорию пунктуации, чтобы избежать необходимости указывать .!?" и т. д., хотя вы обязательно должны проверить, соответствует ли оно вашим потребностям или установить их явно. Прочтите категорию «P» в разделе «Поддерживаемые общие категории Unicode». ", расположенный на странице классов символов MSDN.

string input = @"this is some code. the code is in C#? it's great! In ""quotes."" after quotes.";
string pattern = @"(^|\p{P}\s+)(\w+)";

// compiled for performance (might want to benchmark it for your loop)
Regex rx = new Regex(pattern, RegexOptions.Compiled);

string result = rx.Replace(input, m => m.Groups[1].Value
                                + m.Groups[2].Value.Substring(0, 1).ToUpper()
                                + m.Groups[2].Value.Substring(1));

Если вы решите не использовать класс \p{P}, вам придется указать символы самостоятельно, например:

string pattern = @"(^|[.?!""]\s+)(\w+)";

EDIT: ниже приведен обновленный пример, демонстрирующий 3 шаблона. Первый показывает, как все знаки препинания влияют на регистр. Во втором показано, как выбирать определенные категории пунктуации с помощью вычитания классов. Он использует все знаки препинания, удаляя определенные группы пунктуации. Третий похож на второй, но использует другие группы.

В ссылке MSDN не указано, к чему относятся некоторые категории пунктуации, поэтому вот разбивка:

  • P: все знаки препинания (включает все перечисленные ниже категории)
  • ПК: подчеркивание _
  • Pd: тире -
  • Ps: открывающая скобка, скобки и фигурные скобки ( [ {
  • Pe: закрывающие круглые скобки, скобки и фигурные скобки ) ] }
  • Pi: начальные одинарные/двойные кавычки (MSDN говорит, что это "может вести себя как Ps/Pe в зависимости от использования")
  • Pf: конечные одинарные/двойные кавычки (применяется примечание MSDN Pi)
  • Po: другие знаки препинания, такие как запятые, двоеточия, точки с запятой и косые черты ,, :, ;, \, /

Внимательно сравните, как эти группы влияют на результаты. Это должно дать вам большую степень гибкости. Если это кажется нежелательным, вы можете использовать определенные символы в классе символов, как показано ранее.

string input = @"foo ( parens ) bar { braces } foo [ brackets ] bar. single ' quote & "" double "" quote.
dash - test. Connector _ test. Comma, test. Semicolon; test. Colon: test. Slash / test. Slash \ test.";

string[] patterns = { 
    @"(^|\p{P}\s+)(\w+)", // all punctuation chars
    @"(^|[\p{P}-[\p{Pc}\p{Pd}\p{Ps}\p{Pe}]]\s+)(\w+)", // all punctuation chars except Pc/Pd/Ps/Pe
    @"(^|[\p{P}-[\p{Po}]]\s+)(\w+)" // all punctuation chars except Po
};

// compiled for performance (might want to benchmark it for your loop)
foreach (string pattern in patterns)
{
    Console.WriteLine("*** Current pattern: {0}", pattern);
    string result = Regex.Replace(input, pattern,
                            m => m.Groups[1].Value
                                 + m.Groups[2].Value.Substring(0, 1).ToUpper()
                                 + m.Groups[2].Value.Substring(1));
    Console.WriteLine(result);
    Console.WriteLine();
}

Обратите внимание, что «Dash» не пишется с заглавной буквы в последнем шаблоне и находится на новой строке. Один из способов сделать его заглавным — использовать опцию RegexOptions.Multiline. Попробуйте приведенный выше фрагмент с этим, чтобы увидеть, соответствует ли он желаемому результату.

Кроме того, для примера я не использовал RegexOptions.Compiled в приведенном выше цикле. Чтобы использовать обе опции ИЛИ их вместе: RegexOptions.Compiled | RegexOptions.Multiline.

person Ahmad Mageed    schedule 25.01.2010
comment
+1 - Хороший улов с классом символов пунктуации, но наличие всех этих дополнений строк и подстрок в MatchEvaluator не дает наилучших результатов из StringBuilder, который будет использовать операция Regex.Replace. В моем решении используются захваты нулевой ширины для битов, которые идентифицируют «первый» символ, что означает, что OP просто возвращает match.Value.ToUpper(). - person Andras Zoltan; 26.01.2010
comment
Ахмад, как вы и предложили, добавил к моему ответу грубый, но справедливый ориентир. Мой работает быстрее, когда оба регулярных выражения скомпилированы в экземпляры Regex с помощью RegexOptions.Compiled. Ваш метод работает быстрее при использовании статического метода Regex.Replace, но при этом производительность снижается в обоих случаях (я больше никогда не буду использовать статический метод!) :) - person Andras Zoltan; 26.01.2010

У вас есть несколько вариантов:

  1. Ваш подход к разделению строки, использованию заглавных букв и повторному объединению
  2. Использование регулярных выражений для замены выражений (что может быть немного сложно для случая)
  3. Напишите итератор C#, который перебирает каждый символ и возвращает новый IEnumerable<char> с первой буквой после точки в верхнем регистре. Может предложить преимущество потокового решения.
  4. Перебирайте каждый символ и вводите в верхний регистр те, которые появляются сразу после точки (пробелы игнорируются) — StringBuffer может упростить эту задачу.

В приведенном ниже коде используется итератор:

public static string ToSentenceCase( string someString )
{
  var sb = new StringBuilder( someString.Length );
  bool wasPeriodLastSeen = true; // We want first letter to be capitalized
  foreach( var c in someString )
  {
      if( wasPeriodLastSeen && !c.IsWhiteSpace ) 
      {
          sb.Append( c.ToUpper() );
          wasPeriodLastSeen = false;         
      }        
      else
      {
          if( c == '.' )  // you may want to expand this to other punctuation
              wasPeriodLastSeen = true;
          sb.Append( c );
      }
  }

  return sb.ToString();
}
person LBushkin    schedule 25.01.2010
comment
ЛБушкин: ToTitleCase сделает заглавной первую букву каждого слова строки. В моем случае вывод будет «Это какой-то код». Код на C#. - person AlwaysAProgrammer; 26.01.2010
comment
Стивен: Проблема с производительностью, потому что метод вызывается в цикле. - person AlwaysAProgrammer; 26.01.2010
comment
Вы правы, я просмотрел документацию, и это пословно. Я обновлю свой пост, чтобы отразить правильную реализацию. - person LBushkin; 26.01.2010

Не знаю почему, но я решил попробовать доходность, основываясь на том, что предложил Л. Бушкин. Просто для удовольствия.

static IEnumerable<char> CapitalLetters(string sentence)
        {
            //capitalize first letter
            bool capitalize = true;
            char lastLetter;
            for (int i = 0; i < sentence.Length; i++)
            {
                lastLetter = sentence[i];
                yield return (capitalize) ? Char.ToUpper(sentence[i]) : sentence[i];


                if (Char.IsWhiteSpace(lastLetter) && capitalize == true)
                    continue;

                capitalize = false;
                if (lastLetter == '.' || lastLetter == '!') //etc
                    capitalize = true;
            }
        }

Чтобы использовать его:

string sentence = new String(CapitalLetters("this is some code. the code is in C#.").ToArray());
person Stan R.    schedule 25.01.2010

  1. Делайте свою работу в StringBuffer.
  2. В нижнем регистре все это.
  3. Цикл и прописные начальные символы.
  4. Вызов ToString.
person Steven Sudit    schedule 25.01.2010
comment
Это может привести к непреднамеренным последствиям перевода другого текста в нижний регистр, который должен оставаться в верхнем регистре, например, имен. - person LBushkin; 26.01.2010
comment
@LBushkin: Тогда пропустите шаг 2, если вы уверены, что это нормально. - person Steven Sudit; 26.01.2010
comment
Поскольку не так много мест, где можно извлечь выгоду, вероятно, можно было бы эффективно использовать StringBuilder, но дьявол кроется в деталях. Хотите вставить код? - person Hamish Grubijan; 26.01.2010
comment
Было бы забавно написать об этом, но я не имею права делать это в данный момент. Прости. - person Steven Sudit; 26.01.2010