文字列の途中からカルチャに依存した「starts-with」操作を実行するにはどうすればよいですか?


106

比較的あいまいな要件がありますが、BCLを使用して可能であると思われます。

コンテキストとして、日付/時刻文字列を野田時間で解析しています。入力文字列内の自分の位置に論理カーソルを保持しています。したがって、完全な文字列は「2013年1月3日」である可能性がありますが、論理カーソルは「J」にある可能性があります。

ここで、月名を解析し、カルチャーのすべての既知の月名と比較する必要があります。

  • 文化に敏感
  • 大文字と小文字を区別しない
  • カーソルのポイントから(後でではなく、カーソルが候補月の名前を「見ている」かどうかを確認したい)
  • 早く
  • ...そして、後で何文字使用したかを知る必要があります

これを行う現在のコードは、通常、を使用して機能しCompareInfo.Compareます。これは効果的に次のようになります(一致する部分についてだけです-実際にはより多くのコードがありますが、一致には関係ありません)。

internal bool MatchCaseInsensitive(string candidate, CompareInfo compareInfo)
{
    return compareInfo.Compare(text, position, candidate.Length,
                               candidate, 0, candidate.Length, 
                               CompareOptions.IgnoreCase) == 0;
}

しかし、それは候補者と私たちが比較する地域が同じ長さであることに依存しています。ほとんどの場合問題ありませんが、一部の特殊なケースで問題ありません。次のようなものがあるとします。

// U+00E9 is a single code point for e-acute
var text = "x b\u00e9d y";
int position = 2;
// e followed by U+0301 still means e-acute, but from two code points
var candidate = "be\u0301d";

今私の比較は失敗します。私は使うことができましたIsPrefix

if (compareInfo.IsPrefix(text.Substring(position), candidate,
                         CompareOptions.IgnoreCase))

だが:

  • そのため、部分文字列を作成する必要がありますが、これは避けたいと思います。(私は野田時間を効果的にシステムライブラリと見なしています。一部のクライアントにとっては、パフォーマンスの解析が重要になる場合があります。)
  • 後でカーソルをどれだけ進めるかはわかりません

現実には、私は強く、これは非常に頻繁に出てくるではないだろう疑う...しかし、私は本当にしたいのように、ここで正しいことを行うために。また、Unicodeの専門家になったり、自分で実装したりせずにできるようになりたいです:)

(最終的に結論を出したい場合に備えて、野田時間のバグ210として発生します。)

正規化の考え方が好きです。a)正確性およびb)パフォーマンスについて詳細に確認する必要があります。私それを正しく機能させることができると仮定して、それがすべてを交換する価値があるかどうかはまだわかりません-これ実際に実際には決して起こらないようなものですが、すべてのユーザーのパフォーマンスを損なう可能性があります: (

また、BCLも確認しました。これも適切に処理されていないようです。サンプルコード:

using System;
using System.Globalization;

class Test
{
    static void Main()
    {
        var culture = (CultureInfo) CultureInfo.InvariantCulture.Clone();
        var months = culture.DateTimeFormat.AbbreviatedMonthNames;
        months[10] = "be\u0301d";
        culture.DateTimeFormat.AbbreviatedMonthNames = months;

        var text = "25 b\u00e9d 2013";
        var pattern = "dd MMM yyyy";
        DateTime result;
        if (DateTime.TryParseExact(text, pattern, culture,
                                   DateTimeStyles.None, out result))
        {
            Console.WriteLine("Parsed! Result={0}", result);
        }
        else
        {
            Console.WriteLine("Didn't parse");
        }
    }
}

カスタムの月の名前を「bEd」のテキスト値を持つ「bed」に変更すると、正常に解析されます。

さて、さらにいくつかのデータポイント:

  • 使用してのコストSubstringIsPrefix有意であるが恐ろしいではありません。開発用ラップトップの「金曜日2013年4月12日20:28:42」のサンプルでは、​​1秒間に実行できる解析操作の数が約460Kから約400Kに変更されています。可能な場合はそのような減速を回避したいのですが、それほど悪くはありませ

  • 正規化は思ったよりも実行可能ではありません。ポータブルクラスライブラリでは利用できないためです。PCL以外のビルドにのみ使用できる可能性があるため、PCLビルドの精度が少し低くなります。正規化テスト(string.IsNormalized)のパフォーマンスヒットにより、パフォーマンスは1秒あたり約445Kコールに低下します。私はまだそれが私が必要とするすべてを実行するかどうかわかりません-たとえば、「ß」を含む月の名前は多くの文化で「ss」と一致するはずです...そして正規化はそれを行いません。


部分文字列を作成することによるパフォーマンスへの影響を回避したいという要望を理解していますが、そうするのが最善の方法かもしれませんが、ゲームの初期に、すべてを選択したユニコード正規化形式に最初にシフトし、「ポイントごとに」歩くことができることを知っています。 」おそらくDフォーム。
IDisposable 2013

@IDisposable:はい、私はそれについて疑問に思いました。もちろん、私は事前に月名自体を正規化できます。少なくとも私は一度だけ正規化を行うことができます。正規化の手順では、最初に何かを行う必要があるかどうかを確認するのでしょうか。私は正規化の経験があまりありません-調べるべき1つの道です。
Jon Skeet、2013

1
あなたtextが長すぎなければ、あなたはそうすることができますif (compareInfo.IndexOf(text, candidate, position, options) == position)msdn.microsoft.com/en-us/library/ms143031.aspx しかし、text非常に長い場合は、必要な場所を超えて検索することに多くの時間を費やすことになります。
ジムミッシェル2013

1
このインスタンスStringクラスをまったく使用せずに、Char[]直接使用するだけです。あなたはより多くのコードを書くことになりますが、それはあなたが高パフォーマンスが欲しいときに起こることです...あるいは多分あなたはC ++ / CLIでプログラミングするべきです;-)
intrepidis

1
ウィルCompareOptions.IgnoreNonSpaceは自動的にあなたのためにこれの世話をしませんか?これは、(doccoからではなく、このiPadからテストまでの位置に残念で!)私には見えます、これは(かもしれないかのようにそのオプション用?)ユースケース。" 文字列の比較では、発音区別符号などの非スペース結合文字を無視する必要があることを示します。 "
Sepster

回答:


41

最初に、さまざまな正規化形式の処理とは別に、多くの<-> 1 /多のケースマッピングの問題を検討します。

例えば:

x heiße y
  ^--- cursor

一致しますheisseが、カーソル1を移動しすぎます。そして:

x heisse y
  ^--- cursor

一致しますheißeが、カーソル1の移動が少なすぎます。

これは、単純な1対1のマッピングを持たないすべてのキャラクターに適用されます。

実際に一致した部分文字列の長さを知る必要があります。しかしCompareIndexOf.. etcはその情報を捨てます。これは、正規表現で可能かもしれないが、実装は、完全なケースの折りたたみを行いませんので、一致していない ßss/SSも関わらず、大文字と小文字を区別しないモードに.Compareして.IndexOfください。そして、とにかくすべての候補者のために新しい正規表現を作成することはおそらくコストがかかるでしょう。

これに対する最も簡単な解決策は、大文字と小文字を折りたたんだ形式で文字列を内部的に格納し、大文字と小文字を折りたたんだ候補とのバイナリ比較を行うことです。次に.Length、カーソルが内部表現用であるため、カーソルを正しく移動できます。また、使用しないことで失われたパフォーマンスのほとんどを回復できますCompareOptions.IgnoreCase

残念ながら、内蔵なしの場合折り機能はありませんし、何のフルケースマッピングが存在しないので、貧乏人の場合の折りたたみはどちらか動作しません- ToUpperメソッドが回らないßにはSS

たとえば、これはJavaで(さらにはJavaScriptでも)動作し、正規形Cの文字列が与えられます。

//Poor man's case folding.
//There are some edge cases where this doesn't work
public static String toCaseFold( String input, Locale cultureInfo ) {
    return input.toUpperCase(cultureInfo).toLowerCase(cultureInfo);
}

Javaの大文字と小文字を区別しない比較では、C#のように完全な大文字と小文字を区別しないことに注意してくださいCompareOptions.IgnoreCase。つまり、Javaは完全なケースマッピングを行いますが、単純なケースフォールディングを行います。C#は単純なケースマッピングを行いますが、完全なケースフォールディングを行います。

そのため、使用する前に文字列を大文字に変換するサードパーティのライブラリが必要になる可能性があります。


何かを行う前に、文字列が通常の形式Cであることを確認する必要があります。ラテン文字用に最適化されたこの予備的なクイックチェックを使用できます。

public static bool MaybeRequiresNormalizationToFormC(string input)
{
    if( input == null ) throw new ArgumentNullException("input");

    int len = input.Length;
    for (int i = 0; i < len; ++i)
    {
        if (input[i] > 0x2FF)
        {
            return true;
        }
    }

    return false;
}

これは偽陽性を与えますが、偽陰性を与えません。すべての文字列で実行する必要がある場合でも、ラテンスクリプト文字を使用すると、460kの解析/秒が遅くなるとは思いません。偽陽性を使用IsNormalizedすると、真の陰性/陽性を取得するために使用し、その後必要に応じて正規化します。


したがって、結論として、処理は最初に正規形Cを保証し、次に大文字小文字を変換することです。処理された文字列とバイナリ比較を行い、現在移動しているときにカーソルを移動します。


これをありがとう-正規化フォームCをさらに詳しく調べる必要がありますが、これらは素晴らしい指針です。「PCLでは正常に機能しない」(正規化を提供しない)でも問題ないと思います。ケース折りたたみにサードパーティのライブラリを使用することはここではやりすぎです。現在、サードパーティの依存関係はなく、BCLでも処理できない隅のケースのためだけにサードパーティの依存関係を導入するのは面倒です。おそらく、大文字小文字の変換はカルチャに依存します。
Jon Skeet、2013

2
@JonSkeetはい、Turkicはケースフォールドマッピングで独自のモードに値します:P CaseFolding.txt
Esailija

この回答には根本的な欠陥があるようです。つまり、大文字と小文字を区別する場合にのみ、文字が合字に(またはその逆に)マップされることを意味します。これはそうではありません; 大文字小文字に関係なく、文字と同等と見なされる合字があります。たとえば、en-USカルチャーでæは、はに等しくaeはに等しくなりffiます。C正規化は合字をまったく処理しません。これは、互換性マッピングのみを許可するためです(これは通常、文字の組み合わせに限定されます)。
ダグラス

KCおよびKDの正規化では、などの一部の合字は処理さæますが、などのその他の合字は処理されません。文字列に関するMSDNドキュメントで説明されているように、問題は文化間の不一致によってさらに悪化します-en -USではæ同等ですaeが、da-DKでは同等ではありません。したがって、(任意の形式への)正規化とケースマッピングは、この問題の十分な解決策ではありません。
ダグラス

以前のコメントに対する小さな修正:C正規化では、正規マッピング(文字の結合など)のみが許可され、互換性マッピング(合字など)は許可されません。
ダグラス

21

これが要件を満たしているかどうかを確認してください。

public static partial class GlobalizationExtensions {
    public static int IsPrefix(
        this CompareInfo compareInfo,
        String source, String prefix, int startIndex, CompareOptions options
        ) {
        if(compareInfo.IndexOf(source, prefix, startIndex, options)!=startIndex)
            return ~0;
        else
            // source is started with prefix
            // therefore the loop must exit
            for(int length2=0, length1=prefix.Length; ; )
                if(0==compareInfo.Compare(
                        prefix, 0, length1, 
                        source, startIndex, ++length2, options))
                    return length2;
    }
}

compareInfo.Comparesource始まるのは1回だけprefixです。そうでない場合は、をIsPrefix返します-1。それ以外の場合は、で使用される文字の長さsource

ただし、次のケースでインクリメントlength2する以外はわかりません1

var candidate="ßssß\u00E9\u0302";
var text="abcd ssßss\u0065\u0301\u0302sss";

var count=
    culture.CompareInfo.IsPrefix(text, candidate, 5, CompareOptions.IgnoreCase);

更新

パフォーマンスを少し改善しようとしましたが、次のコードにバグがあるかどうかは証明されていません。

public static partial class GlobalizationExtensions {
    public static int Compare(
        this CompareInfo compareInfo,
        String source, String prefix, int startIndex, ref int length2, 
        CompareOptions options) {
        int length1=prefix.Length, v2, v1;

        if(0==(v1=compareInfo.Compare(
            prefix, 0, length1, source, startIndex, length2, options))
            ) {
            return 0;
        }
        else {
            if(0==(v2=compareInfo.Compare(
                prefix, 0, length1, source, startIndex, 1+length2, options))
                ) {
                ++length2;
                return 0;
            }
            else {
                if(v1<0||v2<0) {
                    length2-=2;
                    return -1;
                }
                else {
                    length2+=2;
                    return 1;
                }
            }
        }
    }

    public static int IsPrefix(
        this CompareInfo compareInfo,
        String source, String prefix, int startIndex, CompareOptions options
        ) {
        if(compareInfo.IndexOf(source, prefix, startIndex, options)
                !=startIndex)
            return ~0;
        else
            for(int length2=
                    Math.Min(prefix.Length, source.Length-(1+startIndex)); ; )
                if(0==compareInfo.Compare(
                        source, prefix, startIndex, ref length2, options))
                    return length2;
    }
}

私は特定のケースでテストを行い、比較を約3に下げました。


私はしたい本当に、むしろこのようなループを持っていません。確かにアーリーアウトでは、何かが見つかった場合にのみループする必要がありますが、たとえば「2月」に一致させるためだけに8つの文字列比較を行う必要はありません。より良い方法があるはずだと感じています。また、初期IndexOf操作では文字列全体を開始位置から調べる必要があるため、入力文字列が長い場合はパフォーマンスが低下します。
Jon Skeet、2013

@JonSkeet:ありがとう。ループを減らすことができるかどうかを検出するために追加できるものがあるかもしれません。それについて考えます。
ケンキン

@JonSkeet:リフレクションの使用を検討しますか?私はメソッドをたどったので、それほど遠くないところにネイティブメソッドの呼び出しに陥っています。
ケンキン

3
確かに。野田タイムは、Unicodeの詳細のビジネスに入りたくありません:)
Jon Skeet 2013

2
私はこのような同様の問題を一度解決しました(HTMLでの検索文字列の強調表示)。私も同じようにしました。可能性のあるケースを最初にチェックすることで、ループと検索の戦略を調整して、非常に迅速に完了することができます。これの良い点は、それが完全に正しいように見え、Unicodeの詳細がコードにリークしないことです。
usr

9

これは、正規化やを使用しなくても実際に可能IsPrefixです。

同じ文字数ではなく同じ数のテキスト要素を比較する必要がありますが、一致する文字数を返します。

野田時間でValueCursor.csMatchCaseInsensitiveからメソッドのコピーを作成し、静的なコンテキストで使用できるように少し変更しました。

// Noda time code from MatchCaseInsensitive in ValueCursor.cs
static int IsMatch_Original(string source, int index, string match, CompareInfo compareInfo)
{
    unchecked
    {
        if (match.Length > source.Length - index)
        {
            return 0;
        }

        // TODO(V1.2): This will fail if the length in the input string is different to the length in the
        // match string for culture-specific reasons. It's not clear how to handle that...
        if (compareInfo.Compare(source, index, match.Length, match, 0, match.Length, CompareOptions.IgnoreCase) == 0)
        {
            return match.Length;
        }

        return 0;
    }
}

(参照用に含まれているだけで、ご存じのように正しく比較されないコードです)

そのメソッドの次のバリアントは、フレームワークによって提供されるStringInfo.GetNextTextElementを使用します。テキスト要素をテキスト要素ごとに比較して一致を見つけ、見つかった場合はソース文字列内の一致する文字の実際の数を返すという考え方です。

// Using StringInfo.GetNextTextElement to match by text elements instead of characters
static int IsMatch_New(string source, int index, string match, CompareInfo compareInfo)
{
    int sourceIndex = index;
    int matchIndex = 0;

    // Loop until we reach the end of source or match
    while (sourceIndex < source.Length && matchIndex < match.Length)
    {
        // Get text elements at the current positions of source and match
        // Normally that will be just one character but may be more in case of Unicode combining characters
        string sourceElem = StringInfo.GetNextTextElement(source, sourceIndex);
        string matchElem = StringInfo.GetNextTextElement(match, matchIndex);

        // Compare the current elements.
        if (compareInfo.Compare(sourceElem, matchElem, CompareOptions.IgnoreCase) != 0)
        {
            return 0; // No match
        }

        // Advance in source and match (by number of characters)
        sourceIndex += sourceElem.Length;
        matchIndex += matchElem.Length;
    }

    // Check if we reached end of source and not end of match
    if (matchIndex != match.Length)
    {
        return 0; // No match
    }

    // Found match. Return number of matching characters from source.
    return sourceIndex - index;
}

この方法は、(:基本的にはちょうどあなたが提供してきた文字列のバリアントのカップルをテストしている私のテストケースに応じて、少なくともだけで正常に動作している"b\u00e9d""be\u0301d")。

ただし、GetNextTextElementメソッドは各テキスト要素の部分文字列を作成するため、この実装では多くの部分文字列の比較が必要であり、パフォーマンスに影響を与えます。

そこで、GetNextTextElementを使用しない別のバリアントを作成しましたが、代わりにUnicodeの結合文字をスキップして、実際の一致の長さを文字で検索しました。

// This should be faster
static int IsMatch_Faster(string source, int index, string match, CompareInfo compareInfo)
{
    int sourceLength = source.Length;
    int matchLength = match.Length;
    int sourceIndex = index;
    int matchIndex = 0;

    // Loop until we reach the end of source or match
    while (sourceIndex < sourceLength && matchIndex < matchLength)
    {
        sourceIndex += GetTextElemLen(source, sourceIndex, sourceLength);
        matchIndex += GetTextElemLen(match, matchIndex, matchLength);
    }

    // Check if we reached end of source and not end of match
    if (matchIndex != matchLength)
    {
        return 0; // No match
    }

    // Check if we've found a match
    if (compareInfo.Compare(source, index, sourceIndex - index, match, 0, matchIndex, CompareOptions.IgnoreCase) != 0)
    {
        return 0; // No match
    }

    // Found match. Return number of matching characters from source.
    return sourceIndex - index;
}

このメソッドは、次の2つのヘルパーを使用します。

static int GetTextElemLen(string str, int index, int strLen)
{
    bool stop = false;
    int elemLen;

    for (elemLen = 0; index < strLen && !stop; ++elemLen, ++index)
    {
        stop = !IsCombiningCharacter(str, index);
    }

    return elemLen;
}

static bool IsCombiningCharacter(string str, int index)
{
    switch (CharUnicodeInfo.GetUnicodeCategory(str, index))
    {
        case UnicodeCategory.NonSpacingMark:
        case UnicodeCategory.SpacingCombiningMark:
        case UnicodeCategory.EnclosingMark:
            return true;

        default:
            return false;
    }
}

ベンチマーキングはまだ行っていないので、高速な方法が実際に高速であるかどうかはわかりません。また、拡張テストも行っていません。

しかし、これは、Unicode結合文字を含む可能性のある文字列に対して、カルチャに敏感な部分文字列マッチングを実行する方法に関するあなたの質問に答えるはずです。

これらは私が使用したテストケースです:

static Tuple<string, int, string, int>[] tests = new []
{
    Tuple.Create("x b\u00e9d y", 2, "be\u0301d", 3),
    Tuple.Create("x be\u0301d y", 2, "b\u00e9d", 4),

    Tuple.Create("x b\u00e9d", 2, "be\u0301d", 3),
    Tuple.Create("x be\u0301d", 2, "b\u00e9d", 4),

    Tuple.Create("b\u00e9d y", 0, "be\u0301d", 3),
    Tuple.Create("be\u0301d y", 0, "b\u00e9d", 4),

    Tuple.Create("b\u00e9d", 0, "be\u0301d", 3),
    Tuple.Create("be\u0301d", 0, "b\u00e9d", 4),

    Tuple.Create("b\u00e9", 0, "be\u0301d", 0),
    Tuple.Create("be\u0301", 0, "b\u00e9d", 0),
};

タプル値は次のとおりです。

  1. ソース文字列(haystack)
  2. ソースの開始位置。
  3. 一致文字列(針)。
  4. 予想される一致の長さ。

3つのメソッドでこれらのテストを実行すると、次の結果が得られます。

Test #0: Orignal=BAD; New=OK; Faster=OK
Test #1: Orignal=BAD; New=OK; Faster=OK
Test #2: Orignal=BAD; New=OK; Faster=OK
Test #3: Orignal=BAD; New=OK; Faster=OK
Test #4: Orignal=BAD; New=OK; Faster=OK
Test #5: Orignal=BAD; New=OK; Faster=OK
Test #6: Orignal=BAD; New=OK; Faster=OK
Test #7: Orignal=BAD; New=OK; Faster=OK
Test #8: Orignal=OK; New=OK; Faster=OK
Test #9: Orignal=OK; New=OK; Faster=OK

最後の2つのテストは、ソース文字列が一致文字列よりも短い場合のテストです。この場合、元の(野田時間)メソッドも成功します。


本当にありがとうございました。パフォーマンスを確認するには、詳細に調べる必要がありますが、すばらしい出発点のようです。(コード自体の)Unicodeについての知識は、私が期待したよりも多く必要ですが、プラットフォームが必要なことを実行しない場合、それについてできることは多くありません:(
Jon Skeet

@JonSkeet:お役に立てて嬉しいです!そして、はい、Unicodeサポートとサブストリングマッチングは間違いなく...フレームワークに含まれている必要があります
マーテンWikström
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.