Длина подстроки, согласованная с чувствительным к культуре методом String.IndexOf
Я попытался написать метод замены строк, поддерживающий культуру:
public static string Replace(string text, string oldValue, string newValue)
{
int index = text.IndexOf(oldValue, StringComparison.CurrentCulture);
return index >= 0
? text.Substring(0, index) + newValue + text.Substring(index + oldValue.Length)
: text;
}
Однако он зажимает символы Unicode, комбинируя символы:
// \u0301 is Combining Acute Accent
Console.WriteLine(Replace("déf", "é", "o")); // 1. CORRECT: dof
Console.WriteLine(Replace("déf", "e\u0301", "o")); // 2. INCORRECT: do
Console.WriteLine(Replace("de\u0301f", "é", "o")); // 3. INCORRECT: dóf
Чтобы исправить мой код, мне нужно знать, что во втором примере String.IndexOf
соответствует только одному символу (é
), хотя он искал два (e\u0301
). Точно так же мне нужно знать, что в третьем примере String.IndexOf
соответствует двум символам (e\u0301
), хотя он искал только один (é
).
Как определить фактическую длину подстроки, которая соответствует String.IndexOf
?
ПРИМЕЧАНИЕ: Выполнение нормализации Юникода на text
и oldValue
(как было предложено Джеймсом Кизи) будет вмещать сочетание символов, но лигатуры все равно будут проблемой:
Console.WriteLine(Replace("œf", "œ", "i")); // 4. CORRECT: if
Console.WriteLine(Replace("œf", "oe", "i")); // 5. INCORRECT: i
Console.WriteLine(Replace("oef", "œ", "i")); // 6. INCORRECT: ief
Ответы
Ответ 1
Вам необходимо напрямую вызвать FindNLSString или FindNLSStringEx. String.IndexOf
использует FindNLSStringEx, но вся необходимая информация доступна в FindNLSString.
Вот пример того, как переписать метод Replace, который работает против ваших тестовых случаев. Обратите внимание, что я использую текущую локализацию пользователя, прочитав документацию API, если вы хотите использовать языковой стандарт или предоставить свой собственный. Я также передаю в 0 для флагов, что означает, что он будет использовать параметры сравнения строк по умолчанию для языкового стандарта, опять же документация может помочь вам предоставить различные параметры.
public const int LOCALE_USER_DEFAULT = 0x0400;
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
internal static extern int FindNLSString(int locale, uint flags, [MarshalAs(UnmanagedType.LPWStr)] string sourceString, int sourceCount, [MarshalAs(UnmanagedType.LPWStr)] string findString, int findCount, out int found);
public static string ReplaceWithCombiningCharSupport(string text, string oldValue, string newValue)
{
int foundLength;
int index = FindNLSString(LOCALE_USER_DEFAULT, 0, text, text.Length, oldValue, oldValue.Length, out foundLength);
return index >= 0 ? text.Substring(0, index) + newValue + text.Substring(index + foundLength) : text;
}
Ответ 2
Я говорил слишком рано (и никогда раньше этого не видел), но есть альтернатива. Вы можете использовать метод StringInfo.ParseCombiningCharacters(), чтобы получить начало каждого фактического символа и использовать его для определения длины заменяемой строки.
Вам нужно будет нормализовать обе строки до того, как вы вызовете Индекс. Это гарантирует, что исходная и целевая строки имеют одинаковую длину.
См. страницу справки String.Normalize(), в которой описывается эта точная проблема.
Ответ 3
Использование следующих методов работает для ваших примеров. Он работает, сравнивая значения, пока не найдет, сколько символов потребуется в исходной строке, чтобы равняться oldValue
, и используя это вместо простого oldValue.Length
.
public static string Replace(string text, string oldValue, string newValue)
{
int index = text.IndexOf(oldValue, StringComparison.CurrentCulture);
if (index >= 0)
return text.Substring(0, index) + newValue +
text.Substring(index + LengthInString(text, oldValue, index));
else
return text;
}
static int LengthInString(string text, string oldValue, int index)
{
for (int length = 1; length <= text.Length - index; length++)
if (string.Equals(text.Substring(index, length), oldValue,
StringComparison.CurrentCulture))
return length;
throw new Exception("Oops!");
}