NUnit - как сравнить строки, содержащие составные символы Unicode? - PullRequest
2 голосов
/ 27 февраля 2012

Я использую NUnit v2.5 для сравнения строк, содержащих составные символы Юникода.
Хотя само сравнение работает нормально, знак, указывающий на первое различие, кажется неуместным.

UPD: Я закончил с переопределенным EqualConstraint, который в свою очередь вызывает пользовательский TextMessageWriter, поэтому мне больше не нужен ответ.См. Решение ниже.

Вот фрагмент:

string s1 = "ใช้งานง่าย";
string s2 = "ใช้งานงาย";
Assert.That(s1, Is.EqualTo(s2));

Вот вывод:

Expected: "ใช้งานงาย"
But was:  "ใช้งานง่าย"
------------------^

Стрелка, указывающая на первый другой символ, кажется, не в 2 позициях (столько, сколько есть тоновые метки выше).Для более длинных струн это становится настоящей болью.
Я попытался String.Normalize(), но это тоже не сработало.

Как мне решить эту проблему? Спасибо за вашу помощь.Смотрите мой ответ ниже.

Ответы [ 3 ]

1 голос
/ 29 февраля 2012

Когда вы сравниваете строки Unicode, вы всегда должны нормализовать обе стороны сравнения, и одинаково.Недостаточно выполнить двоичное сравнение s1 и s2, потому что канонически эквивалентные строки не будут проверять двоичный эквивалент.

Наличие четырех тривиальных функций нормализации, по одной для каждой из четырех нормализацийформы, вы хотите проверить NFD(s1) для двоичного равенства до NFD(s2).Не имеет значения, используете ли вы NFD или NFC, но вы должны сделать то же самое для обеих строк.

Для функций k-compat, NFKD и NFKD, они полезны при выполнениипоиск строк, потому что они улучшают отзыв за счет некоторой точности.Например, NFKD("™") будет равно NFKD("TM").Это то, что делает Adobe Reader, например, когда вы запускаете поиск по документам: он всегда запускает поиск в режиме k-compat, чтобы у ваших запросов поиска был больше шансов найти что-либо.Однако, в отличие от NFC и NFD, функции k-compat NFKC и NFKD теряют информацию и необратимы.С простыми NFD и NFC вы всегда можете вернуться к другому.

0 голосов
/ 29 июня 2012

Я думаю, что не могу найти лучшего ответа, поэтому отвечаю на свой вопрос.

Причина.
Есть много языков, использующих модификаторы без пробелов для символов. Для европейских языков есть замены, например, "u" (U+0075) + "¨" (U+00A8) = "ü" (U+00FC). В этом случае решения @tchrist вполне достаточно.

Однако для сложных систем записи нет замены модификаторов без пробелов. Поэтому TextMessageWriter.WriteCaretLine(int mismatch) в NUnit обрабатывает параметр mismatch как смещение байта , в то время как экранное представление тайской строки может быть на короче , чем длина строки каретки ("-----^").

Решение.
Принудительно WriteCaretLine(int mismatch) учитывает модификаторы без пробелов, уменьшая значение mismatch до количества модификаторов без интервалов, имевших место до этого смещения.
Реализуйте все дополнительные классы, которые действительно нужны только для запуска вашего нового кода.

Наряду с тайским я проверил его на деванагари и тибетском. Работает как положено.

Еще одна ловушка. Если вы используете NUnit с Visual Studio через ReSharper, как я, вам нужно настроить шрифты вашего Internet Explorer (им нельзя управлять с помощью R #), чтобы он использовал правильные моноширинные шрифты для тайского, деванагари и т. Д.

Осуществление.

  1. Наследовать TextMessageWriter и переопределять его DisplayStringDifferences;
  2. Реализуйте свои собственные ClipExpectedAndActual и FindMismatchPosition - здесь уважаемые модификаторы без пробелов; Требуется правильное отсечение, так как оно может также повлиять на вычисление непространственных элементов.
  3. Наследовать EqualConstraint и переопределять его WriteMessageTo(MessageWriter writer), чтобы ваш MessageWriter использовался;
  4. При необходимости создайте пользовательскую оболочку для простого вызова настраиваемого ограничения.

Исходный код приведен ниже. Около 80% кода не делает ничего полезного, но он включен из-за уровней доступа в исходном коде.

// Step 1.
public class ThaiMessageWriter : TextMessageWriter
{
    /// <summary>
    /// This method is merely a copy of the original method taken from NUnit sources,
    /// except that it changes meaning of <paramref name="mismatch"/> before the caret line is displayed.
    /// <remarks>
    /// Originally passed <paramref name="mismatch"/> contains byte offset, while proper display of caret requires
    /// it position to be calculated in character placeholder units. They are different in case of
    /// over- or under-string Unicode characters like acute mark or complex script (Thai)
    /// </remarks> 
    /// </summary>
    /// <param name="clipping"></param>
    public override void DisplayStringDifferences(string expected, string actual, int mismatch, bool ignoreCase, bool clipping)
    {
        // Maximum string we can display without truncating
        int maxDisplayLength = MaxLineLength
                               - PrefixLength   // Allow for prefix
                               - 2;             // 2 quotation marks

        int mismatchOffset = mismatch;

        if (clipping)
            MsgUtils2.ClipExpectedAndActual(ref expected, ref actual, maxDisplayLength, mismatchOffset);

        expected = MsgUtils.EscapeControlChars(expected);
        actual = MsgUtils.EscapeControlChars(actual);

        // The mismatch position may have changed due to clipping or white space conversion
        int mismatchInCharPlaceholders = MsgUtils2.FindMismatchPosition(expected, actual, 0, ignoreCase);

        Write(Pfx_Expected);
        WriteExpectedValue(expected);
        if (ignoreCase)
            WriteModifier("ignoring case");
        WriteLine();
        WriteActualLine(actual);
        //DisplayDifferences(expected, actual);
        if (mismatch >= 0)
            WriteCaretLine(mismatchInCharPlaceholders);

    }

    // Copied due to private
    /// <summary>
    /// Write the generic 'Actual' line for a constraint
    /// </summary>
    /// <param name="constraint">The constraint for which the actual value is to be written</param>
    private void WriteActualLine(Constraint constraint)
    {
        Write(Pfx_Actual);
        constraint.WriteActualValueTo(this);
        WriteLine();
    }

    // Copied due to private
    /// <summary>
    /// Write the generic 'Actual' line for a given value
    /// </summary>
    /// <param name="actual">The actual value causing a failure</param>
    private void WriteActualLine(object actual)
    {
        Write(Pfx_Actual);
        WriteActualValue(actual);
        WriteLine();
    }

    // Copied due to private
    private void WriteCaretLine(int mismatch)
    {
        // We subtract 2 for the initial 2 blanks and add back 1 for the initial quote
        WriteLine("  {0}^", new string('-', PrefixLength + mismatch - 2 + 1));
    }
}

// Step 2.
public static class MsgUtils2
{
    private static readonly string ELLIPSIS = "...";

    /// <summary>
    ///  Almost a copy of MsgUtil.ClipExpectedAndActual method
    /// </summary>
    /// <param name="expected"></param>
    /// <param name="actual"></param>
    /// <param name="maxDisplayLength"></param>
    /// <param name="mismatch"></param>
    public static void ClipExpectedAndActual(ref string expected, ref string actual, int maxDisplayLength, int mismatch)
    {
        // Case 1: Both strings fit on line
        int maxStringLength = Math.Max(expected.Length, actual.Length);
        if (maxStringLength <= maxDisplayLength)
            return;

        // Case 2: Assume that the tail of each string fits on line
        int clipLength = maxDisplayLength - ELLIPSIS.Length;
        int clipStart = maxStringLength - clipLength;

        // Case 3: If it doesn't, center the mismatch position
        if (clipStart > mismatch)
            clipStart = Math.Max(0, mismatch - clipLength / 2);

        // shift both clipStart and maxDisplayLength if they split non-placeholding symbol
        AdjustForNonPlaceholdingCharacter(expected, ref clipStart);
        AdjustForNonPlaceholdingCharacter(expected, ref maxDisplayLength);

        expected = MsgUtils.ClipString(expected, maxDisplayLength, clipStart);
        actual = MsgUtils.ClipString(actual, maxDisplayLength, clipStart);
    }

    private static void AdjustForNonPlaceholdingCharacter(string expected, ref int index)
    {

        while (index > 0 && CharUnicodeInfo.GetUnicodeCategory(expected[index]) == UnicodeCategory.NonSpacingMark)
        {
            index--;
        }
    }

    static public int FindMismatchPosition(string expected, string actual, int istart, bool ignoreCase)
    {
        int length = Math.Min(expected.Length, actual.Length);

        string s1 = ignoreCase ? expected.ToLower() : expected;
        string s2 = ignoreCase ? actual.ToLower() : actual;

        int iSpacingCharacters = 0;
        for (int i = 0; i < istart; i++)
        {
            if (CharUnicodeInfo.GetUnicodeCategory(s1[i]) != UnicodeCategory.NonSpacingMark)
                iSpacingCharacters++;
        }
        for (int i = istart; i < length; i++)
        {
            if (s1[i] != s2[i])
                return iSpacingCharacters;
            if (CharUnicodeInfo.GetUnicodeCategory(s1[i]) != UnicodeCategory.NonSpacingMark)
                iSpacingCharacters++;
        }

        //
        // Strings have same content up to the length of the shorter string.
        // Mismatch occurs because string lengths are different, so show
        // that they start differing where the shortest string ends
        //
        if (expected.Length != actual.Length)
            return length;

        //
        // Same strings : We shouldn't get here
        //
        return -1;
    }
}

// Step 3.
public class ThaiEqualConstraint : EqualConstraint
{
    private readonly string _expected;

    // WTF expected is private?
    public ThaiEqualConstraint(string expected) : base(expected)
    {
        _expected = expected;
    }

    public override void WriteMessageTo(MessageWriter writer)
    {
        // redirect output to customized MessageWriter
        var myMessageWriter = new ThaiMessageWriter();
        base.WriteMessageTo(myMessageWriter);
        writer.Write(myMessageWriter);
    }
}

// Step 4.
public static class ThaiText
{
    public static EqualConstraint IsEqual(string expected)
    {
        return new ThaiEqualConstraint(expected);
    }
}
0 голосов
/ 29 февраля 2012

Вы должны иметь возможность использовать код из этого ответа , чтобы преобразовать каждую строку в экранированную версию исходной строки. Составные символы станут единым экранированным кодом \u, а комбинированные символы будут серией таких экранированных символов. Затем запустите Assert на этих экранированных версиях строки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...