Упрощенный сценарий
TextElementEnumerator очень полезен и эффективен:
private static List<SoundCount> CountSounds(IEnumerable<string> words)
{
Dictionary<string, SoundCount> soundCounts = new Dictionary<string, SoundCount>();
foreach (var word in words)
{
TextElementEnumerator graphemeEnumerator = StringInfo.GetTextElementEnumerator(word);
while (graphemeEnumerator.MoveNext())
{
string grapheme = graphemeEnumerator.GetTextElement();
SoundCount count;
if (!soundCounts.TryGetValue(grapheme, out count))
{
count = new SoundCount() { Sound = grapheme };
soundCounts.Add(grapheme, count);
}
count.Count++;
}
}
return new List<SoundCount>(soundCounts.Values);
}
Вы также можете сделать это с помощью регулярного выражения: (Из документации TextElementEnumerator обрабатывает несколько случаев, которые не выполняются в приведенном ниже выражении, особенно дополнительные символы, но это довольно редко, и в любом случае не требуется для моего приложения .)
private static List<SoundCount> CountSoundsRegex(IEnumerable<string> words)
{
var soundCounts = new Dictionary<string, SoundCount>();
var graphemeExpression = new Regex(@"\P{M}\p{M}*");
foreach (var word in words)
{
Match graphemeMatch = graphemeExpression.Match(word);
while (graphemeMatch.Success)
{
string grapheme = graphemeMatch.Value;
SoundCount count;
if (!soundCounts.TryGetValue(grapheme, out count))
{
count = new SoundCount() { Sound = grapheme };
soundCounts.Add(grapheme, count);
}
count.Count++;
graphemeMatch = graphemeMatch.NextMatch();
}
}
return new List<SoundCount>(soundCounts.Values);
}
Производительность: В ходе моего тестирования я обнаружил, что TextElementEnumerator был примерно в 4 раза быстрее, чем регулярное выражение.
Реалистичный сценарий
К сожалению, нет способа «подправить», как перечисляет TextElementEnumerator, так что класс будет бесполезен в реалистическом сценарии.
Одним из решений является настройка нашего регулярного выражения:
[\P{M}\P{Lm}] # Match a character that is NOT a character intended to be combined with another character or a special character that is used like a letter
(?: # Start a group for the combining characters:
(?: # Start a group for tied characters:
[\u035C\u0361] # Match an under- or over- tie bar...
\P{M}\p{M}* # ...followed by another grapheme (in the simplified sense)
) # (End the tied characters group)
|\p{M} # OR a character intended to be combined with another character
|\p{Lm} # OR a special character that is used like a letter
)* # Match the combining characters group zero or more times.
Возможно, мы могли бы также создать наш собственный IEnumerator , используя CharUnicodeInfo.GetUnicodeCategory, чтобы восстановить нашу производительность, но мне кажется, что это слишком большая работа и дополнительный код для обслуживания. (Кто-нибудь еще хочет пойти?) Для этого созданы регулярные выражения.