Swift countElements () возвращает неверное значение при подсчете эмодзи флага

let str1 = "????????????????????????????????????????"
let str2 = "????????.????????.????????.????????.????????."

println("\(countElements(str1)), \(countElements(str2))")

Результат: 1, 10

Но не должно ли str1 иметь 5 элементов?

Кажется, ошибка возникает только тогда, когда я использую эмодзи флага.


person ZYiOS    schedule 11.11.2014    source источник
comment
Да, это странно. Я пробовал разные смайлики, и причиной этого являются только смайлы-флажки. Это вызывает даже использование разных флагов без пробела.   -  person Fogmeister    schedule 11.11.2014
comment
Мне это кажется ошибкой. ???????? представляет собой расширенный кластер графем, состоящий из БУКВЫ D СИМВОЛА РЕГИОНАЛЬНОГО ИНДИКАТОРА и БУКВЫ E СИМВОЛА РЕГИОНАЛЬНОГО ИНДИКАТОРА, и считается одним символом, но ???????????????????????????????????????? должно состоять из 5 символов. Даже ???????????????????????????????????????????????????????? дает счетчик символов один. Возможно, кто-нибудь, лучше знакомый со стандартом Unicode, сможет это объяснить.   -  person Martin R    schedule 11.11.2014
comment
Кажется, что произвольная последовательность букв региональных индикаторов рассматривается как единый кластер графем. Например, let str1 = "\u{1F1E6}\u{1F1E7}\u{1F1E8}\u{1F1E9}\u{1F1EA}\u{1F1EB}" печатается как ????????????????????????, но считается как один символ.   -  person Martin R    schedule 11.11.2014
comment
Интересно, что str1.startIndex.successor() == str1.endIndex   -  person rintaro    schedule 11.11.2014
comment
Я не могу этого понять, но вот спецификации границ кластера графемы   -  person rintaro    schedule 11.11.2014
comment
@rintaro: Спасибо за ссылку. Я добавил тег [unicode], возможно, это привлечет какого-нибудь специалиста по этой теме.   -  person Martin R    schedule 11.11.2014


Ответы (2)


Обновление для Swift 4 (Xcode 9)

Начиная с Swift 4 (протестированного с бета-версией Xcode 9) кластеры графем прерываются после каждого второго символа регионального индикатора, как это предусмотрено стандартом Unicode 9:

let str1 = "????????????????????????????????????????"
print(str1.count) // 5
print(Array(str1)) // ["????????", "????????", "????????", "????????", "????????"]

Также String - это набор его символов (опять же), поэтому можно получить количество символов с помощью str1.count.


(Старый ответ для Swift 3 и старше :)

Из «Границы трех кластеров графем» в «Стандартном приложении № 29 СЕГМЕНТАЦИЯ ТЕКСТА UNICODE. ": (курсив добавлен):

Унаследованный кластер графем определяется как основа (например, A или カ), за которой следует ноль или более продолжающихся символов. Один из способов представить это как последовательность символов, образующих «стек».

Основой могут быть отдельные символы или любая последовательность символов хангыль-джамо, образующая слог хангыль, как определено в D133 в стандарте Unicode, или любая последовательность символов Regional_Indicator (RI). Символы RI используются парами для обозначения символов национального флага Emoji, соответствующих кодам стран ISO. Последовательности из более чем двух символов RI должны быть разделены другими символами, например U + 200B ZWSP.

(Спасибо @rintaro за ссылку).

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

Вы можете разделить «флаги» НУЛЕВОЙ ШИРИНОЙ, НЕ СОЕДИНЯЮЩЕЙ:

let str1 = "????????\u{200C}????????"
print(str1.characters.count) // 2

или вставьте ПРОБЕЛ НУЛЕВОЙ ШИРИНЫ:

let str2 = "????????\u{200B}????????"
print(str2.characters.count) // 3

Это также решает возможные неоднозначности, например должно быть «???? ???? ???? ????» быть «???? ???????? ????» или «???????? ????????»?

См. Также Как узнать, есть ли два смайлика будет отображаться как один смайлик? о возможном методе подсчета количества «составных символов» в строке Swift, который вернет 5 для вашего let str1 = "????????????????????????????????????????".

person Martin R    schedule 11.11.2014
comment
Хорошо поймал! Теперь было бы интересно узнать, почему они так спроектировали, ИМХО это бородавка. - person DarkDust; 11.11.2014
comment
Теперь вопрос в том, что ... countElements("????????" + "????????") должно быть 1, 2 или 3 в этой семантике? :( - person rintaro; 11.11.2014
comment
@rintaro: 1, потому что в результирующей строке нет символа-разделителя между двумя флагами. - person DarkDust; 11.11.2014
comment
@rintaro: Другой пример: let a = "J\u{1F1EF}"; let b = "\u{1F1F5}P". Тогда 3 = countElements(a+b) < countElements(a) + countElements(b) = 2 + 2. - Моя интерпретация: c = a + b объединяет скалярные значения Unicode строк, а не символов. Следовательно, c может иметь кластеры графемы, которых не было в a или b. - person Martin R; 11.11.2014
comment
Это отличный ответ, но в конечном итоге, как выразился DarkDust, это бородавка на Swift. Ваше приложение может получить текстовую строку, введенную пользователем, которая содержит смежные символы флага (или другие символы Unicode / Emoji с двойной кодовой точкой), и вы ничего не можете с этим поделать. - person SafeFastExpressive; 19.05.2015
comment
Рэнди, это не вина Swift, поскольку он правильно реализует стандарт Unicode. Проблема, с которой столкнулся консорциум Unicode, заключалась в том, как определить разрывы между региональными показателями. Есть три основных варианта: перейти к первому индикатору и считать каждые два (потенциально медленно); склейте два индикатора между собой невидимым персонажем; или разделить пары индикаторов невидимым символом. В итоге они выбрали вариант 3. - person Martin Winter; 14.08.2015
comment
@MartinWinter Но если я открываю текстовый документ в TextEdit, набираю ????????????????, сохраняю, а затем шестнадцатеричный дамп, я вижу, что он не вставляет пробел нулевой ширины. Точно так же я собираю твиты из API Twitter и не вижу пробелов нулевой ширины между флагами. То же самое, если я отправляю ???????????????? в сообщениях в OS X, а затем проверяю базу данных отправленных сообщений SQLite. Все просто нарушают стандарты? Я всегда придерживаюсь варианта 1. Кажется, что ничего не соответствует стандарту. - person sudo; 29.06.2016
comment
@sudo Обратите внимание, что в стандарте сказано: «следует разделять другими символами». Другими словами, никто не нарушает стандарт, опуская такие разделители. В большинстве случаев в этом все равно нет необходимости. Однако это помогает с двусмысленностями. У меня нет времени исследовать это, но я, кажется, помню, что текстовая система Какао имеет свою собственную логику для определения границ символов, т.е. е. отдельно от (или в дополнение к) NSString и, конечно же, от Swift String. - person Martin Winter; 29.06.2016
comment
Стоит отметить, что начиная с Unicode 9.0.0 и версии 29 из Приложение № 29 к стандарту Unicode, правила изменились. В последовательности региональных индикаторных символов графемы разрываются после каждого второго регионального индикаторного символа. Я не знаю, реализовал ли Swift новые правила. - person user2357112 supports Monica; 30.09.2016
comment
@ user2357112: По-видимому, нет, print("????????????????".characters.count) по-прежнему печатает 1 с помощью Swift 3 (Xcode 8). Но большое спасибо за информацию, возможно, я напишу отчет об ошибке. - person Martin R; 30.09.2016
comment
Как упоминалось в @ user2357112, правила GB12 и GB13 (новые в версии 29) гласят, что только последовательности из двух символов RI образуют графемный кластер (устаревший или расширенный). Как ни странно, это противоречит абзацу, цитируемому в ответе, который все еще является частью редакции 29. Я предполагаю, что правила были изменены без обновления текста, поэтому я отправил отчет об ошибке в консорциум Unicode. - person nwellnhof; 08.03.2017
comment
Я сейчас мобильный, но мне интересно, не разделяет ли Zero-Width Non-Joiner их на 2 символа - person Ky Leggiero; 21.04.2017
comment
@Supuhstar: Действительно, спасибо за отзыв! - person Martin R; 21.04.2017
comment
@ user2357112: Судя по всему, Swift 4 теперь реализует новые правила (проверено с бета-версией Xcode 9). - person Martin R; 06.06.2017

Вот как я решил эту проблему для Swift 3:

let str = "????????????????????????????????????????" //or whatever the string of emojis is
let range = str.startIndex..<str.endIndex
var length = 0
str.enumerateSubstrings(in: range, options: NSString.EnumerationOptions.byComposedCharacterSequences) { (substring, substringRange, enclosingRange, stop) -> () in
        length = length + 1
    }
print("Character Count: \(length)")

Это решает все проблемы с подсчетом символов и смайликами, и это самый простой метод, который я нашел.

person James Arnold    schedule 07.03.2017
comment
Сравните stackoverflow.com/a/39104563/1187415 (на который я ссылался в своем ответе) :) - person Martin R; 08.03.2017