Форматирование предложений в строке с использованием C # - PullRequest
6 голосов
/ 26 января 2010

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

Например, «это некоторый код. Код находится на C #». Выход должен быть "Это некоторый код. Код находится в C #".

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

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

Ответы [ 5 ]

5 голосов
/ 26 января 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+)";

РЕДАКТИРОВАТЬ: ниже обновленный пример, чтобы продемонстрировать 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();
}

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

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

5 голосов
/ 26 января 2010

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

Я бы использовал перегрузку 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(); });
}

Регулярное выражение использует конструкцию (? <=) (Положительный внешний вид с нулевой шириной), чтобы ограничить захват только символами a-z, начинающимися с начала строки, или нужными знаками препинания. В бите <code>[.;:] вы можете добавить дополнительные, которые вы хотите (например, [.;:?."] добавить? И "символы.

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

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

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

EDIT

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

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

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% медленнее, и мое решение было медленнее, чем решение Ахмада.

4 голосов
/ 26 января 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();
}
2 голосов
/ 26 января 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());
1 голос
/ 26 января 2010
  1. Работайте в StringBuffer.
  2. Строчные все это.
  3. Проходные и заглавные ведущие символы.
  4. Call ToString.
...