Як я можу виконати чутливу до культури операцію "починаючи з" із середини рядка?


106

У мене є вимога , яке є відносно неясним, але він відчуває , як це повинно бути можливо з допомогою BCL.

Для контексту я аналізую рядок дати / часу в Noda Time . Я підтримую логічний курсор для свого положення у вхідному рядку. Таким чином, хоча повна рядок може бути "3 січня 2013 року", логічний курсор може бути на "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))

але:

  • Це вимагає, щоб я створив підрядку, якої я б дуже хотів уникати. (Я переглядаю Noda Time настільки ефективно, як системна бібліотека; аналіз ефективності може бути важливим для деяких клієнтів.)
  • Це не говорить мені, як далеко рухати курсор згодом

Насправді я дуже підозрюю, що це не дуже часто виникає ... але я дуже хотів би зробити це правильно. Я також дуже хотів би це зробити, не ставши експертом Unicode або не реалізуючи це сам :)

(Визначено як помилка 210 за Noda Time, якщо хтось хоче виконати будь-який можливий висновок.)

Мені подобається ідея нормалізації. Мені потрібно детально перевірити наявність а) правильності та б) виконання. Припускаючи, що я можу змусити його працювати правильно, я все ще не впевнений, як варто було б змінитись над усім - це та річ, яка, ймовірно, ніколи не з’явиться в реальному житті, але може зашкодити роботі всіх моїх користувачів: (

Я також перевірив 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" добре аналізує.

Гаразд, ще кілька точок даних:

  • Вартість використання Substringта IsPrefixзначна, але не жахлива. На вибірці "П’ятниця 12 квітня 2013 20:28:42" на моєму ноутбуці для розробки він змінює кількість операцій розбору, які я можу виконати за секунду, приблизно від 460K до приблизно 400K. Я б скоріше уникав цього уповільнення, якщо це можливо, але це не дуже погано.

  • Нормалізація менш здійсненна, ніж я думав - тому що вона не доступна в бібліотеках портативних класів. Я потенційно міг би використовувати його лише для збірок, що не належать до PCL, дозволяючи збірці PCL бути менш правильними. Результативність тестування на нормалізацію ( string.IsNormalized) знижує продуктивність приблизно до 445 КЛ в секунду, з чим я можу жити. Я все ще не впевнений, що він робить все, що мені потрібно - наприклад, назва місяця, що містить "ß", повинна відповідати "ss" у багатьох культурах, я вважаю ... і нормалізація цього не робить.


Хоча я розумію ваше бажання уникнути хіба на ефективність створення підрядка, можливо, найкраще це зробити, але раніше в грі, переклавши все на обрану форму нормалізації унікоду ПЕРШИЙ, а потім знаючи, що ви можете ходити "по пункту ". Ймовірно, D-форма.
Ідентифікаційний номер

@IDisposable: Так, мені це було цікаво. Очевидно, я можу заздалегідь нормалізувати самі назви місяців. Принаймні, я можу зробити нормалізацію лише один раз. Цікаво, чи перевіряється процедура нормалізації, чи потрібно щось робити спочатку. Я не маю великого досвіду щодо нормалізації - безумовно, один проспект, на який слід звернути увагу.
Джон Скіт

1
Якщо ваш textне занадто довгий, ви можете зробити if (compareInfo.IndexOf(text, candidate, position, options) == position). msdn.microsoft.com/en-us/library/ms143031.aspx Але якщо textце дуже довго, це витратить багато часу на пошук за межами, де це потрібно.
Джим Мішель

1
Просто обходьте використання Stringкласу взагалі в цьому випадку та використовуйте Char[]безпосередньо. Ви закінчите писати більше коду, але це те, що відбувається, коли ви хочете отримати високу продуктивність ... або, можливо, вам слід програмувати на C ++ / CLI ;-)
intrepidis

1
Чи не порівняєOptions.IgnoreNonSpace не подбає про це автоматично для вас? Вона дивиться на мене (від docco, не в змозі перевірити з цього IPad шкода!) , Ніби це могло б бути ( ?) Варіант використання для цієї опції. « Вказує , що порівняння рядків повинні ігнорувати непробельний поєднують символи, такі як діакритичні знаки. »
Sepster

Відповіді:


41

Я розгляну проблему багатьох <-> одного / багатьох кейс-файлів спочатку та окремо від обробки різних форм нормалізації.

Наприклад:

x heiße y
  ^--- cursor

Відповідає, heisseале потім занадто сильно переміщує курсор 1. І:

x heisse y
  ^--- cursor

Відповідає, heißeале потім переміщує курсор 1 занадто менше.

Це стосується будь-якого символу, який не має простого відображення один на один.

Вам потрібно було б знати довжину підрядки, яка насправді відповідала. Але Compare, IndexOf.. і т.д. викиньте цю інформацію. Це може бути можливим з регулярними виразами , але реалізація не робить повний випадок складання і тому не відповідає , ßщоб 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, а потім скласти справа. Зробіть двійкові порівняння з оброблюваними рядками та переміщуйте курсор у той момент, коли ви переміщуєте його в даний час.


Дякую за це - мені потрібно детальніше розглянути форму нормалізації С, але це великі вказівки. Я думаю, що я можу жити з тим, що "він працює не зовсім коректно під PCL" (що не забезпечує нормалізації). Використання сторонньої бібліотеки для складання справ було б надмірним тут - у нас зараз немає сторонніх залежностей, і запровадити її лише для кутового випадку, з яким навіть BCL не справляється, було б болем. Імовірно, складовий регістр є чутливим до культури, до речі (наприклад, по-турецьки)?
Джон Скіт

2
@JonSkeet так, тюркський заслуговує власного режиму в картографічних картах: P Див. Розділ формату у заголовку CaseFolding.txt
Esailija

Ця відповідь, мабуть, має фундаментальний недолік, оскільки передбачає, що персонажі відображають лігатури (і навпаки) лише при складенні регістру. Це не так; існують лігатури, які вважаються рівними символам незалежно від корпусу. Наприклад, за ан-американською культурою æдорівнює aeі дорівнює ffi. C-нормалізація взагалі не обробляє лігатури, оскільки дозволяє лише відображення сумісності (які, як правило, обмежуються комбінуванням символів).
Дуглас

KC- і KD-нормалізація обробляють деякі лігатури, такі як , але пропускають інші, наприклад æ. Питання погіршується розбіжностями між культурами - æдорівнює aeза EN-US, але не за да-DK, як обговорювалося в документації MSDN для рядків . Таким чином, нормалізація (у будь-якій формі) та картографічне відображення не є достатнім рішенням цієї проблеми.
Дуглас

Невелика поправка до мого попереднього коментаря: нормалізація С дозволяє лише канонічних відображень (наприклад, для поєднання символів), а не сумісності (наприклад, для лігатур).
Дуглас

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.Compareвиконує лише один раз, sourceрозпочатий з 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.


Я б справді не хотів би так цикли. Справді, з раннім періодом потрібно буде циклічно лише тоді, коли щось знайдено, але я все одно не повинен робити 8 порівняння рядків, щоб, наприклад, відповідати "лютому". Таке враження, що повинен бути кращий шлях. Крім того, початкова IndexOfоперація повинна переглядати весь рядок із вихідного положення, що було б болем під час виконання, якщо введення рядка довге.
Джон Скіт

@JonSkeet: Дякую Можливо, щось можна додати, щоб виявити, чи можна зменшити цикл. Я подумаю про це.
Кен Кін

@JonSkeet: Чи можете ви використати рефлексію? З тих пір, як я простежив методи, вони неподалік викликають рідні методи.
Кен Кін

3
Справді. Noda Time не хоче вступати у справу деталей Unicode :)
Джон Скіт

2
Я колись подібну проблему вирішив подібну (виділення рядка пошуку в HTML). Я це робив аналогічно. Ви можете налаштувати цикл і стратегію пошуку таким чином, щоб це було завершено дуже швидко, попередньо перевіривши ймовірні випадки. Приємно в цьому те, що це, здається, є абсолютно правильним і ніякі деталі Unicode не просочуються у ваш код.
usr

9

Це реально можливо без нормалізації та без використання IsPrefix.

Нам потрібно порівняти однакову кількість текстових елементів на відміну від тієї ж кількості символів, але все ж повернути кількість відповідних символів.

Я створив копію MatchCaseInsensitiveметоду з ValueCursor.cs в Noda Time і трохи змінив її, щоб її можна було використовувати в статичному контексті:

// 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;
}

Цей метод використовує наступні два помічники:

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. Джерельний рядок (стог сіна)
  2. Вихідне положення у джерелі.
  3. Рядок відповідника (голка).
  4. Очікувана тривалість матчу.

Запуск цих тестів за трьома методами дає такий результат:

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

Останні два тести перевіряють випадок, коли початковий рядок коротший, ніж рядок відповідності. У цьому випадку оригінальний метод (час Noda) також вдасться.


Дуже дякую за це. Мені потрібно детально переглянути це, щоб побачити, наскільки добре воно працює, але це виглядає як чудова відправна точка. Більше знає Unicode (у самому коді), ніж я сподівався, що знадобиться, але якщо платформа не зробить те, що потрібно, я не можу з цим зробити :(
Джон Скіт,

@JonSkeet: Радий допомогти! І так, відповідність підрядків з підтримкою Unicode, безумовно, повинна бути включена в рамки ...
Mårten Wikström,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.