Длина подстроки, согласованная с чувствительным к культуре методом 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!");
}