Подтвердить что ты не робот

Почему персонажи-эможи, такие как ???? так странно относятся к строкам Swift?

Персонаж 👩👩👧👦 (семья с двумя женщинами, одной девушкой и одним мальчиком) кодируется как таковой:

U+1F469 WOMAN,
‍U+200D ZWJ,
U+1F469 WOMAN,
U+200D ZWJ,
U+1F467 GIRL,
U+200D ZWJ,
U+1F466 BOY

Так что это очень интересно кодируется; идеальная цель для unit test. Однако Свифт, похоже, не знает, как его лечить. Вот что я имею в виду:

"👩‍👩‍👧‍👦".contains("👩‍👩‍👧‍👦") // true
"👩‍👩‍👧‍👦".contains("👩") // false
"👩‍👩‍👧‍👦".contains("\u{200D}") // false
"👩‍👩‍👧‍👦".contains("👧") // false
"👩‍👩‍👧‍👦".contains("👦") // true

Итак, Свифт говорит, что он содержит себя (хорошо) и мальчика (хорошо!). Но тогда он говорит, что в нем нет женщины, девушки или столяра с нулевой шириной. Что здесь происходит? Почему Свифт знает, что в нем есть мальчик, но не женщина или девочка? Я мог бы понять, рассматривал ли он его как одного персонажа и только признавал, что он содержит себя, но тот факт, что он получил один субкомпонент, и никого другого не смущает меня.

Это не меняется, если я использую что-то вроде "👩".characters.first!.


Еще большее недоумение:

let manual = "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}"
Array(manual.characters) // ["👩‍", "👩‍", "👧‍", "👦"]

Даже если я разместил ZWJ там, они не отражаются в массиве символов. Дальше было немного сказано:

manual.contains("👩") // false
manual.contains("👧") // false
manual.contains("👦") // true

Итак, я получаю такое же поведение с массивом символов... что в высшей степени раздражает, так как я знаю, как выглядит массив.

Это также не изменяется, если я использую что-то вроде "👩".characters.first!.

4b9b3361

Ответ 1

Это связано с тем, как тип String работает в Swift и как работает метод contains(_:).

"👩👩👧👦" - это то, что известно как последовательность emoji, которая отображается как один видимый символ в строке. Последовательность состоит из объектов Character, и в то же время она состоит из объектов UnicodeScalar.

Если вы проверите количество символов в строке, вы увидите, что она состоит из четырех символов, а если вы проверите скалярный счет юникода, это покажет вам другой результат:

print("👩‍👩‍👧‍👦".characters.count)     // 4
print("👩‍👩‍👧‍👦".unicodeScalars.count) // 7

Теперь, если вы проанализируете символы и печатаете их, вы увидите, что кажется нормальным символом, но на самом деле три первых символа содержат как emoji, так и столяр в нулевой ширине в их UnicodeScalarView:

for char in "👩‍👩‍👧‍👦".characters {
    print(char)

    let scalars = String(char).unicodeScalars.map({ String($0.value, radix: 16) })
    print(scalars)
}

// 👩‍
// ["1f469", "200d"]
// 👩‍
// ["1f469", "200d"]
// 👧‍
// ["1f467", "200d"]
// 👦
// ["1f466"]

Как вы можете видеть, только последний символ не содержит нулевой ширины, поэтому при использовании метода contains(_:) он работает так, как вы ожидали. Поскольку вы не сравниваете с emoji, содержащими стопоры нулевой ширины, метод не найдет соответствия ни для одного, ни для последнего символа.

Чтобы развернуть это, если вы создаете String, который состоит из символа emoji, заканчивающегося стопором нулевой ширины, и передайте его методу contains(_:), он также будет оцениваться как false. Это связано с тем, что contains(_:) является тем же самым, что и range(of:) != nil, который пытается найти точное соответствие данному аргументу. Так как символы, заканчивающиеся на нулевой ширине, образуют неполную последовательность, метод пытается найти совпадение для аргумента, комбинируя символы, заканчивающиеся на стопоры нулевой ширины, в полную последовательность. Это означает, что метод никогда не найдет соответствия, если:

  • аргумент заканчивается столяром нулевой ширины, а
  • строка для синтаксического анализа не содержит неполной последовательности (т.е. заканчивается стопором нулевой ширины и не сопровождается совместимым символом).

Чтобы продемонстрировать:

let s = "\u{1f469}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}" // 👩‍👩‍👧‍👦

s.range(of: "\u{1f469}\u{200d}") != nil                            // false
s.range(of: "\u{1f469}\u{200d}\u{1f469}") != nil                   // false

Однако, поскольку сравнение только смотрит вперед, вы можете найти несколько других полных последовательностей внутри строки, обратившись назад:

s.range(of: "\u{1f466}") != nil                                    // true
s.range(of: "\u{1f467}\u{200d}\u{1f466}") != nil                   // true
s.range(of: "\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}") != nil  // true

// Same as the above:
s.contains("\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}")          // true

Самое простое решение - предоставить конкретный вариант сравнения для метода range(of:options:range:locale:). Опция String.CompareOptions.literal выполняет сравнение с точным эквивалентным символьным символом. В качестве примечания стороны подразумевается не Swift Character, но представление UTF-16 как строки экземпляра, так и сравнения - однако, поскольку String не допускает искажения UTF-16, это по существу эквивалентно сравнению скалярного представления Unicode.

Здесь я перегрузил метод Foundation, поэтому, если вам нужен оригинальный, переименуйте это или что-то еще:

extension String {
    func contains(_ string: String) -> Bool {
        return self.range(of: string, options: String.CompareOptions.literal) != nil
    }
}

Теперь метод работает как "должен" с каждым символом, даже с неполными последовательностями:

s.contains("👩")          // true
s.contains("👩\u{200d}")  // true
s.contains("\u{200d}")    // true

Ответ 2

Первая проблема заключается в том, что вы соединяетесь с Foundation с помощью contains (Swift String не является Collection), поэтому это поведение NSString, которое я не считаю обработчиками, составленными Emoji так сильно, как Swift. Тем не менее, Swift, я считаю, сейчас внедряет Unicode 8, что также нуждается в пересмотре в этой ситуации в Unicode 10 (так что это может измениться, когда они реализуют Unicode 10, я не вникнул в то, будет это или нет).

Чтобы упростить, позвольте избавиться от Foundation и использовать Swift, который предоставляет более явные представления. Начнем с символов:

"👩‍👩‍👧‍👦".characters.forEach { print($0) }
👩‍
👩‍
👧‍
👦

OK. Это то, чего мы ожидали. Но это ложь. Посмотрим, каковы эти символы.

"👩‍👩‍👧‍👦".characters.forEach { print(String($0).unicodeScalars.map{$0}) }
["\u{0001F469}", "\u{200D}"]
["\u{0001F469}", "\u{200D}"]
["\u{0001F467}", "\u{200D}"]
["\u{0001F466}"]

А... Так что это ["👩ZWJ", "👩ZWJ", "👧ZWJ", "👦"]. Это делает все более понятным. 👩 не является членом этого списка (это "👩ZWJ" ), но 👦 является членом.

Проблема заключается в том, что Character является "кластером графем", который объединяет вещи (например, прикрепление ZWJ). То, что вы действительно ищете, - это сканер unicode. И это работает точно так, как вы ожидаете:

"👩‍👩‍👧‍👦".unicodeScalars.contains("👩") // true
"👩‍👩‍👧‍👦".unicodeScalars.contains("\u{200D}") // true
"👩‍👩‍👧‍👦".unicodeScalars.contains("👧") // true
"👩‍👩‍👧‍👦".unicodeScalars.contains("👦") // true

И, конечно же, мы также можем найти фактический персонаж, который находится там:

"👩‍👩‍👧‍👦".characters.contains("👩\u{200D}") // true

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

Ответ 3

Кажется, что Swift считает, что ZWJ является расширенным кластером графем с характером, непосредственно предшествующим ему. Мы можем видеть это при сопоставлении массива символов с их unicodeScalars:

Array(manual.characters).map { $0.description.unicodeScalars }

Это выводит из LLDB следующее:

▿ 4 elements
  ▿ 0 : StringUnicodeScalarView("👩‍")
    - 0 : "\u{0001F469}"
    - 1 : "\u{200D}"
  ▿ 1 : StringUnicodeScalarView("👩‍")
    - 0 : "\u{0001F469}"
    - 1 : "\u{200D}"
  ▿ 2 : StringUnicodeScalarView("👧‍")
    - 0 : "\u{0001F467}"
    - 1 : "\u{200D}"
  ▿ 3 : StringUnicodeScalarView("👦")
    - 0 : "\u{0001F466}"

Кроме того, .contains группирует расширенные кластеры графемы в один символ. Например, взяв символы хундула , и (которые объединяются, чтобы сделать корейское слово для "одного": 한):

"\u{1112}\u{1161}\u{11AB}".contains("\u{1112}") // false

Это не удалось найти , потому что три кодовых пункта сгруппированы в один кластер, который действует как один символ. Аналогично, \u{1F469}\u{200D} (WOMAN ZWJ) - это один кластер, который действует как один символ.

Ответ 4

В других ответах обсуждается, что делает Swift, но не вдавайтесь в подробности о том, почему.

Ожидаете ли вы, что "Å" равны "Å"? Я ожидаю, что вы это сделаете.

Одна из них - это письмо с объединителем, другое - однострочный символ. Вы можете добавить много разных комбинаторов к базовому персонажу, и человек все равно будет считать его единственным персонажем. Чтобы справиться с таким расхождением, понятие графемы было создано для представления того, что человек будет рассматривать персонажа независимо от используемых кодовых точек.

Теперь службы текстовых сообщений объединяют символы в графические emoji в течение лет :)🙂. Поэтому в Unicode были добавлены различные эможи. Эти сервисы также начали комбинировать эможи вместе с композитными эмози. Разумеется, нет разумного способа кодировать все возможные комбинации в отдельные кодовые точки, поэтому Консорциум Unicode решил расширить концепцию графемов, чтобы охватить эти составные символы.

То, что это сводится к "👩‍👩‍👧‍👦", следует рассматривать как один "кластер графемы", если вы пытаетесь работать с ним на уровне графемы, как это делает Swift по умолчанию.

Если вы хотите проверить, содержит ли он "👦" как часть этого, то вы должны перейти на более низкий уровень.


Я не знаю синтаксиса Swift, поэтому вот несколько Perl 6, которые имеют одинаковый уровень поддержки Unicode.
(Perl 6 поддерживает Unicode версии 9, поэтому могут быть расхождения)

say "\c[family: woman woman girl boy]" eq "👩‍👩‍👧‍👦"; # True

# .contains is a Str method only, in Perl 6
say "👩‍👩‍👧‍👦".contains("👩‍👩‍👧‍👦")    # True
say "👩‍👩‍👧‍👦".contains("👦");        # False
say "👩‍👩‍👧‍👦".contains("\x[200D]");  # False

# comb with no arguments splits a Str into graphemes
my @graphemes = "👩‍👩‍👧‍👦".comb;
say @graphemes.elems;                # 1

Спуститесь вниз на уровень

# look at it as a list of NFC codepoints
my @components := "👩‍👩‍👧‍👦".NFC;
say @components.elems;                     # 7

say @components.grep("👦".ord).Bool;       # True
say @components.grep("\x[200D]".ord).Bool; # True
say @components.grep(0x200D).Bool;         # True

Переход на этот уровень может усложнить некоторые вещи.

my @match = "👩‍👩‍👧‍👦".ords;
my $l = @match.elems;
say @components.rotor( $l => 1-$l ).grep(@match).Bool; # True

Я предполагаю, что .contains в Swift делает это проще, но это не означает, что нет других вещей, которые усложняются.

Работа на этом уровне значительно упрощает случайное разбиение строки в середине составного символа, например.


То, что вы неосторожно спрашиваете, почему это представление более высокого уровня не работает, как представление более низкого уровня. Ответ, конечно, не должен.

Если вы спрашиваете себя: "Почему это должно быть так сложно", ответ, конечно, "люди".

Ответ 5

Обновление Swift 4.0

String получает множество изменений в обновлении Quick 4, как описано в SE-0163. Для этой демонстрации используются две emoji, представляющие две разные структуры. Оба сочетаются с последовательностью emoji.

👍🏽 представляет собой комбинацию двух emoji, 👍 и 🏽

👩‍👩‍👧‍👦 представляет собой комбинацию из четырех emoji, с соединением нулевой ширины. Формат 👩‍joiner👩‍joiner👧‍joiner👦

1. Графы

В быстрой версии 4.0. emoji считается графемным кластером. Каждый элемент emoji считается равным 1. Свойство count также доступно для строки. Таким образом, вы можете напрямую называть это следующим образом.

"👍🏽".count  // 1. Not available on swift 3
"👩‍👩‍👧‍👦".count // 1. Not available on swift 3

Массив символов строки также учитывается как кластеры grapheme в swift 4.0, поэтому оба следующих кода печатают 1. Эти два emoji являются примерами последовательностей emoji, где несколько emoji объединены вместе или без объединения с нулевой шириной \u{200d} между ними. В swift 3.0 массив символов такой строки разделяет каждый emoji и выводит массив с несколькими элементами (emoji). Столяр игнорируется в этом процессе. Однако в быстром 4.0 массив символов видит все эможи как одну часть. Так что из любого emoji всегда будет 1.

"👍🏽".characters.count  // 1. In swift 3, this prints 2
"👩‍👩‍👧‍👦".characters.count // 1. In swift 3, this prints 4

unicodeScalars остается неизменным в swift 4. Он предоставляет уникальные символы Unicode в данной строке.

"👍🏽".unicodeScalars.count  // 2. Combination of two emoji
"👩‍👩‍👧‍👦".unicodeScalars.count // 7. Combination of four emoji with joiner between them

2. Содержит

В swift 4.0 метод contains игнорирует стоп файл нулевой ширины в emoji. Таким образом, он возвращает true для любого из четырех компонентов emoji из "👩‍👩‍👧‍👦" и возвращает false, если вы проверяете столяр. Однако в swift 3.0 столяр не игнорируется и сочетается с emoji перед ним. Поэтому, когда вы проверяете, содержит ли "👩‍👩‍👧‍👦" первые три компонента emoji, результат будет false

"👍🏽".contains("👍")       // true
"👍🏽".contains("🏽")       // true
"👩‍👩‍👧‍👦".contains("👩‍👩‍👧‍👦")      // true
"👩‍👩‍👧‍👦".contains("👩")      // true. In swift 3, this prints false
"👩‍👩‍👧‍👦".contains("\u{200D}") // false
"👩‍👩‍👧‍👦".contains("👧")      // true. In swift 3, this prints false
"👩‍👩‍👧‍👦".contains("👦")      // true