2012-02-27 22 views
2

私はNUnit v2.5を使用して複合Unicode文字を含む文字列を比較しています。
比較自体はうまく機能しますが、最初の違いを示すキャレットは間違っているようです。NUnit - 合成Unicode文字を含む文字列を比較する方法は?

UPD:私は順番にカスタムTextMessageWriterを呼び出し、オーバーライドEqualConstraintになってしまったんきたので、私はもはや答えを必要とします。下記のソリューションを参照してください。ここで

はスニペットです:

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

ここでは出力です:

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

最初の異なる文字を示す矢印は、(音が上記のマークがあるなど、多くのように)2位オフのようです。長い弦の場合、それは本当の痛みになります。
私はString.Normalize()を試みましたが、どちらもうまくいかないでしょう。

どうすればこの問題を解決できますか?ご協力ありがとうございます。下の私の答えを見てください。

答えて

0

私は自分自身の質問に答えるために、より良い答えが見つからないと思います。

原因。
スペースを使用しない修飾子を使用する言語は、多くの場合、文字にはです。ヨーロッパ言語の場合、置換がある。 "u" (U+0075) + "¨" (U+00A8) = "ü" (U+00FC)。この場合、@ tchristによる解決策で十分です。

しかし、複雑な書込みシステムでは、非スペーシング修飾子に代わるものはありません。したがって、NUnitのTextMessageWriter.WriteCaretLine(int mismatch)は、mismatchパラメータをオフセットとしてとして扱いますが、タイ語の文字列の画面表現は、と短いキャレットライン("-----^")のです。

解決策。
WriteCaretLine(int mismatch)に非スペーシング修飾子を遵守させるには、このオフセットの前にスペップされたスペップ修飾子の数を減らすことが必要です。
新しいコードを呼び出すためにのみ必要な補足クラスをすべて実装します。

タイと並んで、私はDevanagariとTibetanでテストしました。それは期待どおりに動作します。

さらに別の落とし穴。 ReSharperを使ってVisual StudioでNUnitを使用している場合は、Internet Explorerのフォント(R#で管理することはできません)を設定して、タイ語、デーバナガリなどに適切なモノスペースフォントを使用するようにしてください。

実装。

  1. TextMessageWriterを継承し、そのDisplayStringDifferencesをオーバーライドします。
  2. 独自のClipExpectedAndActualFindMismatchPositionを実装します。ここには非スペーシング修飾子があります。スペーシングのない要素の計算にも影響する可能性があるため、適切なクリッピングが必要です。
  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

this answerのコードを使用して、各文字列を元の文字列のエスケープされたバージョンに変換することができます。コンポジット文字は単一のエスケープされた\uコードポイントになりますが、文字を組み合わせることは一連のエスケープになります。次に、これらのエスケープされたバージョンの文字列でAssertを実行します。

+0

残念ながら、これはオプションではありません。六角ダンプの真ん中を指している_correctly_は、元のテキストを指している_misplaced_矢印と比較して、解釈がさらに困難です。 – bytebuster

1

Unicode文字列を比較する場合は、常に比較の両面を同じ方法で正規化する必要があります。正規表現で同等の文字列はバイナリ相当をテストしないので、s1s2のバイナリ比較は十分ではありません。

4つの正規化形式ごとに1つずつ、4つの正規化関数が存在することを確認すると、NFD(s1)をバイナリeqalityとしてNFD(s2)にテストすることをお勧めします。 NFDまたはNFCのどちらを使用するかは関係ありませんが、両方の文字列に同じことを行う必要があります。

k-compat関数の場合、NFKDとNFKDは、文字列検索を行うときに便利です。なぜなら、それらはある精度でリコールを改善するからです。例えば、NFKD("™")は、NFKD("TM")に等しくなります。これはAdobe Readerの機能です。たとえば、ドキュメントの検索を実行すると、k-compatモードで検索が実行されるため、検索結果が見つかる可能性が高くなります。ただし、NFCおよびNFDと異なり、k互換機能NFKCおよびNFKDは情報を失い、元に戻すことはできません。 NFDNFCの単純な場合でも、いつでも他のものに戻ることができます。

+0

アイデアをありがとう。比較自体は期待通りに機能します。私はそれが常にバイナリであると仮定します。 この問題は、矢印を示すに過ぎません。 NUnitはchar offsetに基づいて長さを計算しているようです。 "ใช้"と "ใช"の両方に2つのプレースホルダがあり、文字列の長さはそれぞれ3と2です。 また、この違いがさらにはっきりしていますが、この単語(サンプルの最初の単語)も誤ったオフセットに寄与しています。 – bytebuster

+0

@bytebuster文字列の1つにU + 0348「THAI CHARACTER MAI EK」があり、もう1つは欠けています。この文字は '\ p {Diacritic}'と '\ p {Nonspacing_Mark} '文字の両方です。そこにある文字列は、もはや* cafe *や*niño*が* cafe *や* nino *と同じではないものと同じになることはもうありません。 Unicodeは実際にはこの違いに耐性のある型の等価性テストを持っていますが、正規化ではありません。むしろ、Unicode Collat​​ion Algorithmを使用しているだけで、プライマリ強度でそれらを比較しています。したがって、UCA1(s1)とUCA1(s2)をバイナリ比較する必要があります。 – tchrist

+0

私はUnicodeに関して何を意味しているのか理解していますが、これがどのようにNUnitに役立つとは思いません。 2つの言葉:私が必要とするのは、矢印が2文字短いことです。 '---- ^'の代わりに ' - ^'を使います。あなたはあなたの提案を詳しく教えてもらえますか? – bytebuster

関連する問題