Сворачивание / нормализация лигатур (например, от Æ до ae) с использованием (Core)Foundation

Я пишу помощник, который выполняет ряд преобразований для входной строки, чтобы создать удобное для поиска представление этой строки.

Подумайте о следующем сценарии:

  • Полнотекстовый поиск по немецкому или французскому тексту
  • Записи в вашем хранилище данных содержат
    1. Müller
    2. Großmann
    3. Çingletòn
    4. Bjørk
    5. Æreogramme
  • Поиск должен быть нечетким, в этом
    1. ull, Üll и т. д. соответствуют Müller
    2. Gros, groß и т. д. соответствуют Großmann
    3. cin и т. д. соответствуют Çingletòn
    4. bjö, bjo и т. д. соответствуют Bjørk
    5. aereo и т. д. соответствуют Æreogramme

До сих пор я был успешным в случаях (1), (3) и (4).

Что я не могу понять, так это как обращаться с (2) и (5).

До сих пор я пробовал следующие методы безрезультатно:

CFStringNormalize() // with all documented normalization forms
CFStringTransform() // using the kCFStringTransformToLatin, kCFStringTransformStripCombiningMarks, kCFStringTransformStripDiacritics
CFStringFold() // using kCFCompareNonliteral, kCFCompareWidthInsensitive, kCFCompareLocalized in a number of combinations -- aside: how on earth do I normalize simply _composing_ already decomposed strings??? as soon as I pack that in, my formerly passing tests fail, as well...

Я пролистал руководство пользователя ICU для преобразований, но не вкладывал в него слишком много средств… по очевидным причинам.

Я знаю, что могу поймать case (2), преобразовав его в прописные, а затем обратно в строчные, что будет работать в рамках данного конкретного приложения. Однако я заинтересован в том, чтобы решить эту проблему на более фундаментальном уровне, и, надеюсь, учесть и регистрозависимые приложения.

Любые советы будут с благодарностью!

2 ответа

Поздравляем, вы нашли один из самых болезненных моментов обработки текста!

Прежде всего, NamesList.txt и CaseFolding.txt являются необходимыми ресурсами для подобных вещей, если вы их еще не видели.

Частично проблема заключается в том, что вы пытаетесь сделать что-то почти правильное, работающее на всех языках / локалях, которые вас интересуют, тогда как Unicode больше заботится о том, чтобы делать правильные вещи при отображении строк в одной языковой локали.

Для (2), ß канонически сложен в ss начиная с самого раннего CaseFolding.txt, который я могу найти ( http://www.unicode.org/Public/3.0-Update1/CaseFolding-2.txt). CFStringFold() а также -[NSString stringByFoldingWithOptions:] должен делать правильные вещи, но если нет, то "независимый от локали" s.upper().lower() кажется, дает разумный ответ на все входные данные (а также обрабатывает печально известное "турецкое я").

Для (5) вам немного не повезло: Unicode 6.2, по-видимому, не содержит нормативного сопоставления от Æ до AE и изменил с "буквы" на "лигатуры" и обратно (U+00C6 LATIN CAPITAL LETTER A E в 1.0, LATIN CAPITAL LIGATURE AE в 1.1 и LATIN CAPITAL LETTER AE в 2.0). Вы можете найти в NamesList.txt слово "ligature" и добавить несколько особых случаев.

Заметки:

  • CFStringNormalize() не делает то, что вы хотите. Вы хотите нормализовать строки перед добавлением их в индекс; Я предлагаю NFKC в начале и в конце другой обработки.
  • CFStringTransform() тоже не совсем то, что ты хочешь; все сценарии "латинские"
  • CFStringFold() зависит от порядка: объединение ypogegrammeni и prosgegrammeni удаляется kCFCompareDiacriticInsensitive но преобразуется в строчную йоту kCFCompareCaseInsensitive, Похоже, что "правильным" является то, что сначала следует выполнить сгиб, а затем другие, хотя его лингвистическое обоснование может иметь больший смысл.
  • Вы почти наверняка не хотите использовать kCFCompareLocalized если вы не хотите перестраивать поисковый индекс каждый раз при изменении локали.

Читатели с других языков. Примечание: убедитесь, что используемая вами функция не зависит от текущей локали пользователя! Пользователи Java должны использовать что-то вроде s.toUpperCase(Locale.ENGLISH).NET пользователи должны использовать s.ToUpperInvariant(), Если вы действительно хотите текущую локаль пользователя, укажите ее явно.

Я использовал следующее расширение для String, которое, кажется, работает хорошо.

/// normalized version of string for comparisons and database lookups.  If normalization fails or results in an empty string, original string is returned.
var normalized: String? {
    // expand ligatures and other joined characters and flatten to simple ascii (æ => ae, etc.) by converting to ascii data and back
    guard let data = self.data(using: String.Encoding.ascii, allowLossyConversion: true) else {
        print("WARNING: Unable to convert string to ASCII Data: \(self)")
        return self
    }
    guard let processed = String(data: data, encoding: String.Encoding.ascii) else {
        print("WARNING: Unable to decode ASCII Data normalizing stirng: \(self)")
        return self
    }
    var normalized = processed

    //  // remove non alpha-numeric characters
    normalized = normalized.replacingOccurrences(of: "?", with: "") // educated quotes and the like will be destroyed by above data conversion
    // strip appostrophes
    normalized = normalized.replacingCharacters(in: "'", with: "")
    // replace non-alpha-numeric characters with spaces
    normalized = normalized.replacingCharacters(in: CharacterSet.alphanumerics.inverted, with: " ")
    // lowercase string
    normalized = normalized.lowercased()

    // remove multiple spaces and line breaks and tabs and trim
    normalized = normalized.whitespaceCollapsed

    // may return an empty string if no alphanumeric characters!  In this case, use the raw string as the "normalized" form
    if normalized == "" {
        return self
    } else {
        return normalized
    }
}
Другие вопросы по тегам